# AI框架介绍-行为机（Behavior Machine）
## 什么是行为机
顾名思义，类比状态机每个节点是一个状态，行为机每个节点是描述一种行为。行为机每个节点之间是互斥的，并且节点相互之间完全不用关心是怎么切换的。这里就不讲状态机跟行为树是怎么做ai的了，这里只讲用行为机怎么做一个ai。举个例子 mmo中的小怪策划案，大致会这么写:   

小怪在出生点周围巡逻。发现周围有玩家则选择一个玩家做目标，追击该目标玩家，追到目标玩家则攻击目标玩家，发现距离出生点太远则返回，返回到出生点则继续巡逻

### 1.定义ai的各种行为
我们首先定义好怪物有哪些行为。很简单，我们直接根据策划案中的字面意思，怪物大致有这么几种行为：  
a.巡逻  
b.选择一个玩家追击并且攻击  
e.返回出生点。  

注意很多状态机会把移动作为一种状态，这在行为机中是不对的，因为巡逻，追击，返回都会有移动，移动只是玩家行为节点中的一个部分,移动跟巡逻，追击，返回并不是互斥的。  

节点不要拆的太细，因为每个行为是个协程，我们可以在行为节点中写十分复杂的逻辑，比如有些同学可能会把 选择一个玩家追击并且攻击 这一个节点拆成 a 选择目标 b 追击目标 c 攻击目标. 

甚至还有人会把巡逻拆的更细，拆成a.寻找一个点 b.移动 c.等待一定时间。这都是状态机跟行为树的思维，因为状态机跟行为树可能希望移动节点可以共用。这都是增加了麻烦，不合理，不要用状态机跟行为树的思维去想行为机。行为机中，节点只是描述一种行为，并不需要共用，共用的永远是各种函数。 

### 2.填充满足行为的条件
我们把每个行为确定好对应的条件，一旦条件满足则会进入该行为，取消上一个行为（协程）  
a. 巡逻的条件：身上没有玩家目标，周围没有玩家，距离出生点 < 10米  
b. 选择一个玩家追击并且攻击: 周围有玩家，距离出生点 < 10米  
c. 返回出生点: 距离出生点 > 20米  
其实条件一旦列出，那么节点中的Check方法自然也就实现了  

### 3.实现行为
a. 巡逻的伪代码:
```csharp
while (true)
{
    pos = 出生点周围找一个点
    bool ret = await MoveToAsync(pos，cancelToken);
    if (!ret) // false表示协程取消, 则需要return，停止整个协程
    {
        return;
    }
    // 移动到了，随机等待2-4秒
    randomTime = RandomHelper.Random(2000, 4000);
    bool ret = await TimeComponent.Instance.Wait(randomTime, cancelToken)
    if (!ret) // false表示协程取消, 则需要return，停止整个协程
    {
        return;
    }
}
```
这样，如果b c条件不满足的话，怪物就永远在巡逻节点协程中，不停的找一个点移动，等待，移动，等待

b. 选择目标追击并且攻击目标节点的伪代码:
```csharp
while (true)
{
    target = SelectTarget()
    while (true)
    {
        while (true)
        {
            // 追击目标
            pos = 计算离目标0.2米的一个点
            // 这里不能以目标作为移动目标，因为怪物要距离玩家稍远一点
            await MoveToAsync(pos， cancelToken);
            if (!ret) // false表示协程取消, 则需要return，停止整个协程
            {
                return;
            }
            // 距离玩家 < 0.5米，表示追到了玩家，就不需要追了
            if (距离玩家<0.5米)
            {
                break;
            }            
        }
        // 追击到了，攻击玩家
        while (true)
        {
            // 
            spellId = SelectSpell();
            if (spellId == 0) // 可能技能在cd，等待500ms再试
            {
                bool ret = await TimeComponent.Instance.Wait(500, cancelToken)
                if (!ret) // false表示协程取消, 则需要return，停止整个协程
                {
                    return;
                }
                continue;
            }
            await CastSpell(target);
            // 攻击完成后停止一段时间
            bool ret = await TimeComponent.Instance.Wait(1000, cancelToken)
            if (!ret) // false表示协程取消, 则需要return，停止整个协程
            {
                return;
            }

            // 距离玩家 > 0.5米, 距离玩家远了，break攻击循环，继续追击
            if (距离玩家<0.5米)
            {
                break;
            }    
        }
    }
    

    // 这里加个time，防止上面两个while循环没有进入，结果就会导致一直执行 target = 选择一个目标 这句话，会导致死循环
    bool ret = await TimeComponent.Instance.Wait(100, cancelToken)
    if (!ret) // false表示协程取消, 则需要return，停止整个协程
    {
        return;
    }
}
```

c. 返回出生点伪代码:
```csharp
while (true)
{
    // 整个返回过程是无敌的
    using (Buff buff = AddBuff(无敌))
    {
        pos = 找到离出生点10米的点
        bool ret = await MoveToAsync(pos，cancelToken);
        if (!ret) // false表示协程取消, 则需要return，停止整个协程
        {
            return;
        }
        // 移动到了, buff会删除，或者切换成其它状态，协程退出也会删除无敌buff
    }
}
```

其实巡逻跟返回两个节点也可以合并成一个节点，这个大家自己去尝试尝试。可以看出，行为机编写是非常简单，代码是非常易读的。就这么一个怪物的逻辑，用行为树来编写，节点就很多了，而且并不好阅读跟重构。整个逻辑可能还有些瑕疵，不过意思应该很明白了。

# 总结
1. 行为机节点并不需要共用，行为机的节点只是表示一段逻辑，可以做的非常非常非常庞大，比如做机器人的时候，一个做任务，只有一个节点。里面代码调用了无数的协程方法。
2. 行为机共用的是函数，不是节点，不要想着这个节点应该抽出来共用，这个想法是错误的。
3. 行为机永远只关注当前行为，永远不需要关心上一个行为，当满足行为条件就直接打断上个协程，执行当前节点的协程即可。所以只需要定义好玩家有哪些行为，ai自然而然的就写出来了。
4. 节点不要拆的太细，没有必要
