考虑到成本与风险问题, 绝大多数游戏中的AI其实并不属于真正意义上的AI, 其中并没有对应机器学习算法的设计和AI的训练, 而是按照一定规则写死的, 因此游戏AI中最常见的是有限状态机和行为树.
有限状态机(Finite State Machine)定义了某个对象有限个状态, 通过各种条件或事件来实现状态的转换. 有限状态机的优点是理解简单, 每一种状态都是一个节点, 发生的任何事情都是节点的转换. 缺点是模块性封装性差, 当状态变多之后, 状态之间的转换变得十分复杂, 同时维护就变得极其艰难.
这时, 行为树(Behavior Tree)就显示出了一定优势. 行为树是包含了几种节点类型的树状结构, 每一个节点都对应一个行为, 同时每一个子节点执行后都会有一个结果, 这个结果会返回到其父节点, 并且由父节点决定后续执行, 不会与其他节点进行转换, 节点的逻辑性和模块性都被大大增强. 同时树状结构制作出的的AI编辑器逻辑更加直观, 方便查看和维护.
行为树中的每个子节点都会有一个返回值, 返回值分为三种类型: 成功, 运行中, 失败. 其中成功与失败表示该节点运行结果, 运行中表示该节点还在运行, 下一次调用行为树时会继续运行该节点直到返回成功或失败.
一个最简单的行为树, 一个小兵移动到怪物身边, 攻击:
Root: 根节点, 没有父节点, 只有子节点, 是一个行为树的入口, 每一次进行行为判断时都需要从这里开始
Seq: 顺序节点, 属于逻辑节点,有父节点和子节点, 后面再详细说
Move, Attack: 动作节点, 都属于行为节点, 只有父节点, 没有子节点
执行行为树后, 小兵直接调用Move节点中的行为方法, 如果返回成功, 则继续执行Attack中的行为方法, 如果也返回成功则Seq向根节点返回成功, 进行了一次成功的移动攻击. 如果Move返回了失败, 则不会执行Attack, Seq直接向根节点返回失败. 如果Move不是直接返回成功或失败, 而是需要移动一段时间, 则会返回运行, 下一次调用行为树时直接继续执行Move中的行为方法.
简单的伪代码来了!
dis = 0
Update()
{
//每帧调用BT
ExecuteBT();
}
Move()
{
dis++
if (dis == 3)
print("Arrived!")
return success
else
print("Moving!")
return running
}
Attack()
{
print("Attacking!")
return success
}
Frame1: Moving!
Frame2: Moving!
Frame3: Arrived!
Attcking!
行为树的节点其实都是根据游戏的需求自行设计, 下面只介绍一些比较常用的逻辑和行为节点的设计思路
顺序执行子节点, 如果其中某个子节点返回失败, 则停止执行后面的子节点并返回失败, 如果所有子节点都执行成功则返回成功
顺序执行子节点, 如果其中某个子节点返回成功, 则停止执行后面的子节点并返回成功, 如果所有子节点都执行失败则返回失败
"同时"执行所有子节点, 根据所有子节点的返回值自行设计返回结果. 同时只是该节点的表现形式, 其实该节点依旧是单线程运行, 同时体现在执行过程不受子节点的返回值影响, 只有在全部子节点执行完毕后才会判断最终返回结果
类似一个定时器, 在规定时间过后才开始执行子节点
类似一个定时器, 子节点在规定时间过后如果还在运行则返回失败
循环执行子节点规定次数或自定循环截止条件(until success…)
执行一个具体的行为动作, 如上图中的移动, 攻击等
执行一个if判断语句, 结果为true时返回success, 为false时返回failure