https://www.cnblogs.com/zhanlang96/p/4793511.html
人工智能遵循着:感知->思考->行动
决策方法:有限状态机(Finite-State Machines),分层状态机(Hierarchical Finite-State Machines),行为树(Behavior Trees),效用系统(Utility Systems),目标导向型行动计划(Goal-Oriented Action Planners),分层任务网络(Hierarchical Task Networks)
有限状态机
有限状态机是目前游戏AI中最常见的行为模型。状态机的代码简单又快速,使用上强大、灵活、计算开销小。
状态机的一个好处是可以可视化,如下图所示:
图中有四个状态:巡逻(patrol),查看(investigate),攻击(attack),逃走(flee),我们把实心圆当做初始状态。
简要过程:假设NPC士兵正在保卫他的阵地,当前状态为巡逻,当他听到什么动静时就会转到查看状态,跑到声音源去查看,如果看到敌人就转到攻击状态,如果没看到过一段时间又会回到巡逻状态。在攻击状态中如果血值低下就会进入逃跑状态。如果击败了敌人,又会回到巡逻状态。
状 态机状态类的一个主要结构如下,onEnter函数就相当于unity中的Start()函数,在类开始时调用,作为对旧状态的过度和新状态产生的开始, 比如当从巡逻转向攻击状态时,可以在攻击状态的开始让NPC大喊“发现敌人!进攻!”等等。onUpdate()就相当于unity中的 Update(),你可以让它每帧都执行,或者几秒钟执行一次,是循环执行的,每次执行时间间隔由你来决定。onExit()就是在退出一个状态之前要执 行的,比如,杀死敌人之后由攻击状态转向巡逻状态之前,让NPC做一个欢呼手势并大叫胜利了。FSMTransition列表为将要转到的所有可能的状 态。
class FSMState
{
virtual void onEnter();
virtual void onUpdate();
virtual void onExit();
list<FSMTransition> transitions;
};
每个状态还存储着FSMTransition的类,代表能从当前状态可以转到的状态
class FSMTransition
{
virtual bool isValid();
virtual FSMState* getNextState();
virtual void onTransition();
}
当转换条件满足时isValid()返回true,比如当发现敌人NPC就从巡逻状态转到攻击,getNextState()返回将要转到的状态,onTransition()是状态之间转换的过渡,和上面说的onEnter()差不多。
最后是有限状态机类FiniteStateMachine
class FiniteStateMachine
{
void update();
list<FSMState> states;
FSMState* initialState;
FSMState* activeState;
}
有限状态机类包含一个包含所有状态的列表states,initialState为初始状态,activeState为当前状态。
伪代码如下:
在 activeState.transtitions中循环调用isValid(),检测是否符合达到下一状态的条件
如果符合转换条件
调用activeState.on Exit(),退出当前状态
设置activeState 为 validTransition.getNextState(),把当前状态赋值为下一状态
调用activeState.onEnter(),下一状态的开始
如果不符合转换条件,调用activeState.onUpdate(),让NPC执行当前状态需要做的事
在编写有限状态机的代码只前最好画一个上面的草图,这样既可以明确转换关系,又可以不漏掉该有的状态。
分层有限状态机
有限状态机虽然好,但是它有很大的缺点,当状态少的时候可以运用自如,当状态多的时候10个以上就已经结构非常复杂,而且容易出错。
如果我们让NPC巡逻两个地方,比如安全的室内,和门口
如 果我们想在一个状态上附加一些状况,例如当NPC在巡逻时,让他接一个电话然后再恢复巡逻,此时如果使用有限状态机的话我们必须要新建一个打电话的状态来 做过渡,但是此时的巡逻有两个,所以能接到电话的状态也有两个,然后加了两个相同的状态,很多这样的重复的小状态使得状态机越来越复杂。如下图
这时,我们可以用分层有限状态机来解决这个问题,把多个状态机归为一层,如下图,把巡逻安全处和门口归为看守建筑,这样我们只需要有一个打电话状态就可以了。
分 层有限状态机增加了一个滞后,在有限状态机中并没有,在一个普通的有限状态机中,是从初始状态开始的,在分层有限状态机中是一个嵌套的状态。注意上图有H 的圈,代表历史状态(history state),当我们第一次进入嵌套状态->看守建筑时,历史状态H表示为初始状态,之后历史状态H表示为最近处在的一个状态。
在我们的例 子中:初始状态就是看守建筑,,然后进入看到手机按住这个嵌套,巡逻安全处是初始状态。当从巡逻安全处转换到巡逻门口这个状态时,H历史状态就转变为巡逻 门口状态,此时来电话了,转换到接电话状态,接电话结束,我们回到嵌套状态中的历史状态,此时为巡逻门口,可见H历史状态就是一个临时的,便于嵌套外的状 态返回到之前的嵌套内的小状态,以不至于出错,或者换回了别的状态,如果接完电话回到巡逻安全处,那就出大错了。
分层有限状态机,就这样避免了重复状态,可以实现更大的更复杂的状态。
实例:
Halo2使用了这一技术,如下图
可见:把使用手雷、掩蔽、防御归为自卫,交战部分使用了多层嵌套,但是原理是一样的,向尸体设计和搜查尸体归为战后处理。在撤退和闲置部分只有一个行为被嵌套,但是日后可以继续添加行为,可扩展性良好。
至于如何在嵌套的层里对行为进行选择,可以就按这个顺序执行,也可以加上权重优先级,或者你想让他执行哪个通过代码来控制。
行为树
行为树是树型结构的,每个节点都代表了一个行为,每个行为都可以有子行为。
所有行为都有一个先决条件,就是产生的这些行为的条件。
整 个算法先从树的根部开始,然后开始检查每一个先决条件。树的每一层只可以执行一个行为,所以当一个行为正在执行,它的兄弟节点都不会被检查,但是它们的子 节点还是要检查的。相反如果一个行为的先决条件当前并不满足,则跳过判断它的子节点,继续判断它的兄弟节点。一个树全部检查完毕之后,决定执行优先级最大 的,然后再依次执行每个动作。
伪代码:
使根节点为当前节点
当存在当前节点
判断当前节点的先决条件
如果先决条件返回true
把节点加到执行清单
使子节点为当前节点
否则
使兄弟节点为当前节点
执行执行清单上的所有行为
不同于状态机,行为树是无状态的,不需要记下之前执行的行为,只是判断行为该不该执行。
行为树的节点之间是不相关的,删除或增加节点,对其他节点都无影响。所以,可扩展性也是行为树的一个优势。另外还可以为决策树添加灵活性与随机性,父节点可以随机决定是否检查子节点。
缺点:决策树做的选择并不一定是最优的,结果也不一定是我们想要的。而且决策每次都要从根部往下判断选择行为节点,比状态机要耗费时间。每次决策都要经过大量的条件判断语句,会变得非常慢。
另外还有一个问题,例如:一个农民要收割作物,敌人出现了,农民逃跑,逃出了距离敌人的一定范围之后,又回去收割作物,走到敌人的范围又逃出,这样来回往复,是一个弊端,,可以根据情况来写代码避免,否则会被玩家***的。
效用系统
人工智能的逻辑->电脑的逻辑,是基于简单的bool问题,比如:“我能看到敌人吗?”,“我有弹药吗”,是简单的是或者不是的问题,所以做出的行为通常是极端化的,一个单一的行动。比如:
if (CanSeeEnemy())
{
AttackEnemy();
}
if (OutOfAmmo())
{
Reload();
}
即时有多条件的行为,bool判断带来的也是一个单一的行动。
if (OutOfAmmo() && CanSeeEnemy())
{
Hide();
}
所 以有些情况,只是做这些布尔判断是不合适的,会遗漏很多情况,判断也不妥当。比如:我们可能需要同时考虑与敌人的距离、有多少弹药、饥饿程度、HP值,等 等。这些判断条件能映射出许多动作,比我们单一的判断做不做这个动作要好很多。utility-based system,基于效用的系统,会根据权重、比率、队列和许多需要考虑的事项来做出最优选择,使AI比普通的行为树更有头脑。根据上面的例子,使用效用系 统我们的AI可以做出我们想要的动作,并根据当前情况做出不同强度的动作,使AI真实、更具可能性,也不再是只有一个正确的选择了。决策树就是对AI说, “只是你将要做的一个行为”,效用系统就是对AI说:“这些是你可能要做的行为”
Sims模拟人生的人工智能就是使用的效用系统(sims的人工 智能让我膜拜至今),在sims中,小人结合当前环境和自身的状态,来做出行动的选择。例如:小人“非常饿”结合环境“没有食物”会比只有“有一点饿”更 加吸引人的眼球。如果“有一点饿”小人会以接近“美食”为第一执行行为。注意,这里的“美食(的美味程度)”、“食物很少(食物储备程度)”、“一点饿 (饿的程度)”,都是一个有范围的数值(常用的是0-1的浮点值)。
当需要选择新的行为时,我们通过分数(上面说的各种程度)来选择相对最优的选择,或者加上一个随机值再选择,使得接近优选的几个选择都有一定几率(几率可根据所加随机值决定)被选中。
目标导向型行动计划
GOAP 来源于STRIPS方法,这两种都是让AI创造他们自己的方法去解决问题,我们提供给它一系列可能的动作作为对这个世界的描述,和每个动作使用的先决条 件,和行动带来的影响。AI拥有一个初始状态和他需要达到的目标。有一组目标,AI可以通过优先级或当前状态选择一个。计划系统决定一个动作序列来满足当 前目标,计划出一个像路径一样的能最简单达到目标状态的动作序列。
GOAP是一个反向链接搜索,从要实现的目标开始,找到什么动作能实现目标,在寻找刚才动作的先决条件,一直往前推,知道达到你的当前(初始)状态。这种反向链接搜索替代了启发式的前向链接搜索。
伪代码:
把目标加到未解决事件列表
对于每个为解决事件(for)
移除这个为解决事件
找到达成事件的动作
如果动作的先决条件已经满足
增加动作到计划中
往回推需要达到先决条件的动作到计划中
否则
添加该先决条件到未解决时间中
例 如:我们建立一个NPC士兵,把它的目标设为杀死其他敌人,我们设置它的目标为Target.Dead。为了让目标去死,NPC必须要有一个武器用来射 击,这是一个先决条件,但是现在NPC并没有正在装备的武器,NPC就需要执行找到武器这个动作,如果NPC有武器库,他就会从武器库中拿一个,如果病没 有武器库,就需要寻路去找一个武器装备了。得到武器装备之后就要找到敌人,实现方式多种多样,徒步寻找、或者NPC周围有车也可以开着车去寻找。我么发 现,我们给NPC大量的动作选择,让NPC自己决定该做什么,因而产生动态不可预知又有趣的行为,而且表现得很自然,比开发者创建行为好多了。
这里有一个简单的例子:http://gamerboom.com/archives/83622
分层任务网络
HTN 也是寻找一个计划来让AI执行,不同之处在于怎样找出这个计划。开始拥有一个初始状态和一个跟任务代表我们需要解决的问题。原理是*的任务分解成更小 的任务再继续分解直到我们解决问题。每个高级任务都有很多方式被完成,当前世界状态决定高级任务要分解成哪组小任务。HTN与GOAP相反,HTN是前向 链接搜索,是从当前状态一直推到目标状态,向前推直到问题解决。世界状态分散成几种属性,它的HP、精力,敌人的HP、相距距离,计划根据这些来制定。
我 们有两种任务:原始任务和复合任务。原始任务是可以只解决问题的任务,也就是可以直接达到目标的任务。在游戏中,它可以为开火、装填子弹、移动到掩蔽物。 这些人物可以影响世界状态,开火这个任务需要先有子弹,并执行装填子弹这个任务。复合任务是高级别的任务,可以看作方法。一个方法是一组任务可以完成复合 任务,这一组任务是由先决条件决定的。复合任务让HTN推断出世界并且决定该做什么动作。
使用复合任务,我们就能构建一个HTN域,这个域是一大层任务,代表我们解决问题的方法。
伪代码:
增加根复合任务到分解列表中
对于每个在我们分解列表中的任务(for)
移除任务
如果任务是复合任务
找到满足当前条件状态并且能处理该复合任务的方法
如果该方法找到了,增加方法的任务到分解列表中
如果没找到,恢复到之前分解任务的状态中
如果任务是原始任务
在当前状态下执行任务
增加任务到最终计划列表
HTN就是从*的根任务分解更小的任务再分解成更更小,分解是需要判断当前状态和条件的。当我们终于分解为原始任务,我们把原始任务加到最终计划中,每一个原始任务都是一个可操作步骤,我们可以直接执行它。
Unity AI planner: https://www.bilibili.com/video/av67436639