Unity3D人工智能架构模型
- 感知:是AI角色与游戏世界的接口,负责在游戏运行过程中不断感知周围环境,读取游戏状态和数据,为思考和决策收集信息。(for example:是否有敌人接近等。)
- 思考:利用感知的结果选择行为,在多种可能性之间切换。(such as:战斗还是逃跑?躲到哪里?)
一般来说,这是决策系统的任务,有时也可能简单地与感知合二为一。 - 行动:发出命令、更新状态、寻路、播放声音动画,也包括生命值减少等。这是运动系统、动画系统和物理系统的任务,而动画和物理系统由游戏引擎提供支持。
游戏AI的架构模型
对AI需求的三种基本能力: (根据游戏种类和需求进行细化或增删) 运动层:导航+寻路,决定角色的移动路径,具体的移动行为需要动画层的配合(有许多行为可以直接由动画层处理)。运动层包含的算法能够把上层做出的决策转化为运动。 决策层:决定角色在下一时间步该做什么。 战略层:一组角色的总体行为,团队协作时用。小组中的每个角色可以有它们自己的决策层和运动算法,但总体上,它们的决策层会受到团队战略的影响。
FPS/TPS游戏中的AI解析
FPS(第一人称视角射击游戏):以玩家的主观视角来进行射击的游戏。 TPS(第三人称视角射击游戏):玩家控制的游戏人物在屏幕上是可见的,更加强调动作感。
运动层:确定角色如何在游戏世界中移动。(寻最优路) 决策层:确定角色当前目标、命令、状态和当前目的地,并与其他层通信,使角色协调地运动到一个指定的目标地点。(决定AI的执行行为)
战斗控制器:
- 感知部分:
- 视觉子系统:考虑距离、视场角度、当前的可视级别(例如光线、雾和障碍等)。感知受伤害、碰撞等。
- 听觉子系统
- 策略子系统
- 动画部分:负责控制角色的骨骼关节。
选择动画,选择动画参数,播放角色运动序列。
实现AI角色的自主移动——操控行为
操控行为:操作控制角色,让它们能以模拟真实的方式在游戏世界中移动。 它的工作方式是通过产生一定大小和方向的操控力,使角色以某种方式运动。 基本行为中的每一个行为,都产生相应的操控力,将这些操控力以一定的方式组合起来(将基本行为进行不同组合),就能够得到更复杂的“行为”,从而实现更为高级的目标。
无论群体中有多少个体,对于每个个体,计算复杂性有限,通过这种简单计算,可以产生逼真效果。 (两个相似的鸟群,即使是飞过相同的路线,行为也是不同的)
缺点:可能会出现无法预测的行为,在使用时许多参数需要通过实验调整。
Unity3D操控行为编程的主要基类
将AI角色抽象成一个质点——Vehicle类
控制AI角色移动——AILocomotion类
是Vehicle的派生类,能真正控制AI角色的移动。(包括计算每次移动的距离,播放动画等)
各种操控行为的基类——Steering类
是所有操控行为的基类,包含操控行为共有的变量和方法。 (操控AI角色的寻找、逃跑、追逐、躲避、徘徊、分离、队列、聚集等都可由此派生)
个体AI角色的操控行为
靠近and离开
靠近:指定一个目标位置,根据当前的运动速度向量,返回一个操控AI角色到达该目标位置的“操控力”,使AI角色自动向该位置移动。 离开:与前者正好相反,产生一个操控AI角色离开目标的力。 小三角:AI角色 预期速度:从AI角色的当前位置到目标位置的向量 操控向量:预期速度与AI角色当前速度的差(大小随当前位置变化而变化)
抵达
AI角色减速并停到目标位置,避免冲过目标。
当角色在停止半径之外,以最大速度移动。 当角色进入停止半径之内,逐渐减小预期速度,直到为0。
追逐and逃避
追逐:与靠近行为很相似,只是目标由静止不动变为可移动角色。
预测猎物的未来位置,然后向着未来位置的方向追去,才能在最短时间内追上猎物。
实现:
- 使用一个简单的预测期,在每一帧重新计算它的值。
(假设采用一个线性预测期,又假设在预测间隔T时间内角色不会转向,角色经过时间T之后的未来位置可以用当前速度乘T来确定)
一些情况下,追逐可能会提前结束。 例如,如果逃避者在前面,几乎面对追逐者,那么追逐者应该直接向逃避者当前位置移动。 二者之间的关系可以通过计算逃避者朝向向量与AI角色朝向向量的点积得到。
逃避:使猎物躲避捕猎者。 (比如鹿被狼追逐,鹿要不断变换逃跑方向,试图逃离狼预测的追逐方向)
随机徘徊
在游戏场景中随机放置目标,让角色靠近目标。如果每隔一定时间就改变目标的位置,这样角色就永远靠近目标而又不能到达目标(即使到达,目标也会再次移动)。
But角色有时会突然掉头,因为目标移动到了它的后面。
路径跟随
产生一个操控力,使AI角色沿着预先设置的轨迹,构成路径的一系列路点移动。
最简单的跟随路径方式: 将当前路点设置为路点列表中的第1个路点,用靠近行为产生操控力来靠近这个路点,直到非常接近这个点; 然后寻找下一个路点,设置为当前路点,再次接近它。 重复这样的过程直到到达路点列表中的最后一个路点,再根据需要决定是回到第一个路点还是停止在这最后一个路点上。
- 封闭路径:循环,永不结束。需要回到起点重新开始。
- 开放路径:AI角色需要减速(利用抵达行为)停到最后一个路点上。
在实现路径跟随行为时,需要设置一个“路点半径”参数(会引起路径形状的变化)。 即当AI角色距离当前路点多远时,可以认为它已经到达当前路点,从而继续向下一个路点前进。
避开障碍
操控AI角色避开路上的障碍物。
当AI角色的行进路线上发现比较近的障碍时,产生一个“排斥力”,使AI角色远离这个障碍物。 当前方发现多个障碍物时,只产生躲避最近的障碍物的操控力。
区别: 寻路算法:事先根据障碍物的位置,计算出一条静态可行走路线。 障碍躲避:让AI角色在环境中移动,根据移动情况动态地躲避障碍,最终形成一条动态路线。(缺点:对于躲避“L”或“T”形状的障碍物时工作得不太好。因为最终决定AI角色运动状态的是合力,有时无法完全避开障碍)
群体的操控行为
组行为:
检测附近的AI角色
组行为的每种操控行为都决定角色对相邻的其他角色做出何种反应。
为了实现组行为,首先需要用一个雷达脚本实现检测位于当前AI角色“邻域”中的其他AI角色。
一个角色的邻域由一个距离和一个角度来定义,如图灰色区域。 这个灰色区域可以认为是AI角色的可视范围。 (有时为了简化,不考虑AI角色的可见范围,只用一个圆来定义)
与群中邻居保持适当距离——分离
使角色与周围的其他角色保持一定的距离,避免多个角色相互挤到一起。
图中黑色箭头表示在三个邻居的“排斥”作用下,AI角色所受到的操控力。
- 实现:
搜索指定邻域内的其他邻居。 对每个邻居,计算AI角色到达该邻居的向量r,将向量r归一化( r / |r| , |r|表示向量r的长度),得到排斥力方向。 由于排斥力的大小是与距离成反比的,因此还需要除以|r|,得到该邻居对它的排斥力,即 r / |r|^2。 把来自所有邻居的排斥力相加,得到分离行为的总操控力。
与群中邻居朝向一致——队列
试图保持AI角色的运动朝向与邻居一致。
当前运动方向:尖端的灰色线指示的方向 邻居的平均朝向:尖端的深色线指示的方向 深色线(目标朝向)与灰色线(当前朝向)之间的差值是操控力的方向,即操控向量。
- 实现:
通过迭代所有邻居,可以求出AI角色朝向向量的平均值以及速度向量的平均值,得到想要的朝向。 然后减去AI角色的当前朝向,就可以得到队列操控力。
成群聚集在一起——聚集
聚集行为产生一个使AI角色移向邻居的质心的操控力。
与四个邻居都有连接线的小点是这四个邻居位置的平均值。 从黑色小三角指向这个小点的箭头表示作用于AI角色的操控力。
- 实现:
迭代所有邻居求出AI角色位置的平均值,然后利用靠近行为,将这个平均值作为目标位置。
个体与群体的操控行为组合
最简单的组合多个行为方式:直接把各个行为所产生的操控力加到一起,这样得到的总操控力会反映出这些行为。
由于AI角色有最大操控力maxForce的限制,不能简单地把所有的操控力加起来,需要以某种方式截断这个总和,确保得到的值不超过最大力的限制。
操控行为的快速实现——使用Unity3D开源库UnitySteer
下载地址:https://github.com/ricardojmendez/UnitySteer
靠近、离开、抵达、追逐、逃避等个体行为可以用其他方法来实现,例如寻路算法。
操控行为的乐趣
- 可以更好地模拟随机徘徊行为
- 具有较高的效率
- 与A* 寻路相比,操控行为更适合于仿真大的群体的呈现一定个性的行为。
(A* 寻路总是会寻找最优路径,而且当要寻路的AI角色很多时,效率会受到很大影响,并且结果很规则,可以预期。而操控行为与之相较,代价更小,更为生动)
寻找最短路径并避开障碍物——A*寻路
实现A*寻路的三种工作方式
A*寻路相关术语:
创建基于单元的导航图
基于单元的导航图是将游戏地图划分为多个正方形单元或六边形单元组成的规则网格,网格点或网格单元的中心可以看作是节点。
网格越小越精细越易得好路径,但相对需要存储和搜索打量的节点,对内存要求高,很影响效率。
白色部分表示可行走区域,深灰色区域是不可行走区域。
优点: 易于理解和使用 易于动态更新(如动态增加建筑物或其他障碍等),因为结构很规则
适合在塔防游戏、即时战略游戏或其他频繁动态更新场景的游戏中使用。
缺点:
创建可视点导航图(路点图)
手工放置一些“路径点”,如果两点之间无障碍物遮挡(即两点之间相互能“看到”),那么可以用一条线段把这两个点连接起来,生成一条“边”(可能对“边”施加一些限制),最后,这些“路径点”和“边”就组成了可视点导航图。
(对边的长度施加了限制)
优点:灵活。 分散在各处的路径点是精心选择的,能覆盖绝大部分可行走区域。 还可以将其他一些重要位置的点包含进去,同时增加相应的信息存储。 利用这些可用信息,可以高效地实现战术寻路,计算出某个位置的战略信息。
缺点: 当场景很大时,手工放置路径点很繁琐且易出错。 它只是一些点和线段的集合,无法表示出实际的二维可行走区域,角色只能沿着那些边运动。当起始点或终点既不是路径点也不在边上时,只能先找到距离最近的路径点,然后再进行寻路,这样很可能会得到一条Z字形路线,看上去很不自然。甚至发生更坏的情况。且由于只能沿着边行走,很难保证生成路径的质量。 组合爆炸问题。(如果设置1000个路径点,要测试999*1000条路径)
创建导航网格
将游戏场景中的可行走区域划分成凸多边形。(实现中可以限制多边形种类)
表示出了可行走区域的真实几何关系,是一个非均匀网格。 (Unity3D自带的寻路系统就建立在导航网格的基础上)
在这个三角形网格上进行A*寻路时,每个节点对应于一个三角形,所以相邻节点即为与这个三角形相邻的其他三角形。 估计g和h时,导航网格方式也采用了不同的方法。(例如,可以用三角形质心之间的线段长度作为节点之间的路径代价g,也可以用三角形的边的中点之间的距离作为g值等)
将可行走区域划分成四边形,寻路用到的节点位于四边形的中央,利用A*寻路算法,可以找到路径所经过的那些多边形。 如果直接把这些多边形的中心连接起来,就会得到从起始点到目标点的一条路径。 由于形成的这条灰色曲线路径显然不是很理想,最好进一步对它进行处理。 可以采用“视线确定”方法,通过向前跳到视线所及的最远途经点,使路径变得更加平滑而自然。(每当到达路径内的一个路径点时,就检查列表中的下几个路径点,跳过一些多余的视线内的途经点)
优点: 可以进行精确的点到点移动。(在同一个三角形中的任意两个点都是直接可达的) 非常高效。(由于多边形面积可以任意大,只需要较少数量的多边形就可以表示出很大的可行走区域,占用内存较小,搜索路径速度有很大提高) 由于游戏场景本身就是由多边形构成的,因此,通过事先设计好的算法,就能够自动地将可行走区域划分成多边形,生成导航网格,而不需要人工干预。
缺点: 生成导航网格需要较长时间,在地形经常发生动态变化(如经常添加、移除建筑物等)的场景中很少使用,多用于静态场景中。
A*寻路算法是如何工作的
在A* 算法中,使用了两个状态表,分别称为Open表和Closed表。 Open表由待考查的节点组成,而Closed表由已经考查过的节点组成。 (对于一个节点来说,当算法已经检查过与它相邻的所有节点,计算出了这些相邻节点的f,g和h值,并把它们放入Open表以待考查,那么,这个节点就是“已考查过”的节点。)
开始时,Closed表为空,而Open表仅包括起始节点。 每次迭代中,A*算法将Open表中具有最小代价值(即f值最小)的节点取出进行检查,如果这个节点不是目标节点,那么考虑该节点的所有8个相邻节点。
对于每个相邻节点按下列规则处理: (1)如果它既不在Open表中,也不在Closed表中,则将它加入Open表中; (2)如果它已经在Open表中,并且新的路径具有更低的代价值,则更新它的信息; ( 3)如果它已经在Closed表中,那么检查新的路径是否具有更低的代价值。如果是,那么将它从Closed表中移出,加入到Open表中,否则忽略。
重复上述步骤,直到到达目标节点。 如果在到达目标之前,Open表就已经变空,便意味着在起始位置和目标位置之间没有可达的路径。
计算代价: f(总移动代价) = g(从起始节点移动到当前节点的移动代价) + h(从当前节点移动到目标节点所需的移动代价) g: 取其父节点g值,按照相对于父节点的连接方式,增加相应的值。 欧式距离(欧几里得距离):连接两个点的线段的长度,使用勾股定理从这些点的笛卡尔坐标计算距离。(如果是对角线连接+1.414,如果是直角连接+1) h: 由于AI角色还没有到达目标位置,因此只能对这个代价做一个大致的估算,采用启发的方法。(具体应用中,可以采用欧几里得距离,也可以采用曼哈顿距离,或采用其他的启发方法。) 曼哈顿距离:通常称为出租车距离或城市街区距离,用来计算实值向量之间的距离。想象一下均匀网格棋盘上的物体,如果它们只能移动直角,曼哈顿距离是指两个向量之间的距离,在计算距离时不涉及对角线移动。
用A*算法实现战术寻路
为不同区域赋予不同的代价值。 A* 寻路的目标就是要寻找一条从起点到终点的总代价最小的路径。 只需增加危险或困难的区域(例如强火力区域或沼泽等)的代价值。 A* 就会尽量避开这些区域,而选择更安全或容易的路径。
规则1:如果这条边的两个顶点都位于敌人火力攻击范围内,那么将这条边的行走代价加50; 规则2:如果这条边只有一一个顶点位于敌人火力攻击范围内,那么将这条边的行走代价加25; 规则3:其他边的代价保持原值不变。
A* Pathfinding Project 插件的使用
免费版下载地址:http://arongranberg.com/astar/download
A*寻路的适用性
A* 寻路很好用,但它不是万能的。 选择哪种寻路方法要充分考虑到游戏的要求,而不是希望它永远好用。 一般说来,我们通常都是在设计中建立AI能够处理的情景,而不是对AI做出过高的期待,让它去处理任意复杂的场景。
(1) A*寻路算法在游戏中具有十分广泛的应用,利用它可以找到一条从起点到终点的最佳路径,它的效率在同类算法中也很高,对于大多数路径寻找问题,它是最佳的选择。
(2)有一些A*寻路不太适用的场合。例如,如果起点和终点之间没有障碍物,终点直接在视线范围内,就完全不必采用它。 另外,这个算法虽然高效,但寻路具有较大的工作量,需要多帧才能完成。如果CPU计算能力较弱,或者需要为大地形寻找路径,那么计算起来就比较困难了。而且,它也有一定的使用限制。
(3)如果游戏设计者正在为一个Android平台下的手机游戏选择寻路算法,就更需要做好权衡。与PC相比,手机的内存资源要珍贵得多,如果需要在很大的空间中进行寻路,最好选择其他算法,并且,估价算法的开销也可能会成为瓶颈。 因此,在手机游戏中需要针对不同的寻路要求,选择不同的实现方法,例如采用深度优先算法、广度优先算法、遗传算法等。
(4)在战斗游戏中,往往希望AI角色能快速从一个地方跑动到另一个地方。绝大多数情况下,想要的路径并不是最短路径。
试想,如果一个敌人AI角色试图逃离玩家的枪弹,结果却是从玩家指挥的角色身边跑过去。显然,路过玩家角色身边是一个很坏的选择,应该采用更好的路径搜索策略。
为了在战场上做出好的决策,就需要获取高质量的信息。这些信息来自地形分析、路径搜索、视距和许多其他系统。它们的开销很大,为了找到可靠的战斗位置,往往需要评估许多不同的可能性,因此,这些信息的获取过程对系统的效率会有很大的影响。
在实际设计中,除了创建更高效的低层系统,更快速地提供信息之外,设计者还需要能够利用更少的信息,做出更好的AI角色,并且在开发过程中,要始终意识到每一部分信息的代价。
AI角色对游戏世界的感知
AI角色感知信息中视线查询是必不可少的。在Unity3D中,Raycast调用可以实现视线查询,But速度相对较慢,当场景中有大量物体时进行调用,或调用过于频繁时,开销很大。
AI角色对环境信息的感知方式
轮询方式
主动查找感兴趣的信息的方式。
缺点: 当可能感兴趣的事件数量增加时,AI角色就要花大量的时间用于查询,并且查询返回的大部分信息都是无用信息,而且很难调试。
让基于轮询的感知系统更易维护的方式:建立一个轮询中心,在这里进行所有的查询。
优点: 适合事件少的。 (如果AI角色想检测玩家是否接近,采用轮询直接查询玩家的当前位置是最好的选择。)
事件驱动方式
在事件驱动的感知系统中,有一个中心检测系统(事件管理器),它查找角色感兴趣的事件是否发生。当发生事件时,它会通知每个角色,可以看作是某种事件传递机制。
优点: 事件管理器记录每个AI角色所感兴趣的事件,并负责检查、处理和分发事件。由于条件和检查是集中完成的,因此可以很方便地记录和显示相关信息,非常有利于调试。
实现: 利用Unity3D的物理引擎,为AI角色(或它的子物体)添加一个大半径(这个半径与AI角色自身尺寸无关,而取决于它的感知范围)的Collider组件,选中isTrigger,当Unity3D的物理引擎检测到碰撞时,就会自动调用OnTriggerEnter函数,这样,只需在OnTriggerEnter()函数中写出相应的代码就可以了。
检测方式:
- 选择多个专用的事件管理器,每种事件管理器只处理特定类型的信息,(例如碰撞、声音或开关状态等),也只有少量的监听者(对事件感兴趣的角色)。
- 选择通用的事件管理器,能够处理各种不同类型的信息。
检测机制:
- 采用独立的代码,以固定的频率检测事件是否发生。如果发生,则向事件管理器发送一个事件。
(相当于轮询游戏世界的状态,然后将查询结果与感兴趣的所有AI角色分享) - 基于“触发器”。触发器是我们希望AI角色能做出反应的任何“刺激源”,是它们触发了AI角色感兴趣的事件,因此,可以直接由它们通知事件管理器发生了某些事件。
常用感知类型的实现
所有触发器的基类——Trigger类
所有感知器的基类——Sensor类
事件管理器
负责管理触发器的集合。 维护一个当前所有触发器的列表,当每个触发器被创建时,都会向这个管理器注册自身,加入到这个列表中。 负责更新和处理所有的触发器,并且当触发器已过期需要被移除时,从列表中删除它们。 维护一个感知器列表,每个感知器被创建时,向这个管理者注册,加入到感知器列表中。
视觉感知
视锥体模拟视觉法:可以用不同的圆锥来模拟不同类型的视觉。一个近距离、大锥角的圆锥可以模拟出视觉中的余光,而远距离的视觉通常用更长、更窄的圆锥体来表示。
LOD(Level-Of-Detail)逻辑:当AI角色距离玩家很远时,可以减少感知检测,降低感知的开销。
听觉感知
- 球形区域模拟。
- 当声音被创建时,为它加上一个强度属性,随着传播距离的增加,声音强度会衰减,而每个AI角色也有自己的听觉阈值,如果声音小于这个阈值,AI角色就听不到这个声音。
声音存在会持续一定时间,然后自行消失。
触觉感知
可以利用Unity3D的物理引擎来处理。通过为一个游戏物体加上碰撞体,并选中Inspector面板中的isTrigger属性,就可以把它标记为“触发器”。触发器不受物理引擎的控制,当触发器和另一个Collider发生碰撞时(其中至少有一个附加了Rigidbody组件),会发出3个触发信息,分别是OnTriggerEnter(当碰撞体Collider进入trigger触发器时调用),OnTriggerExit(当碰撞体Collider停止触发trigger时调用),OnTriggerStay(当碰撞体Collider接触trigger触发器时,这个函数将在每帧被调用)。在这3个函数中编写相应的代码,就可以实现触觉感知了。
Unity3D已经为触觉感知提供了事件管理器,所以在事件感知器中,不再需要编写触觉相关的代码。
记忆感知
实现一个SenseMemory类。 这个类具有一个记忆列表,列表中保存了每个最近感知到的对象、感知类型、最后感知到该对象的时间以及还能在记忆中保留的时间,当有一段时间没感知到这个对象,这个时间超出了记忆时长,就会将这个对象从记忆列表中删除。
其他类型的感知——血包、宝物等物品的感知
有一些游戏对象,在被一个实体触发后,会保持一定时间的非活动状态。 (例如,一些角色可以“捡起”的物件,如血包或武器)当它被捡起后,会在一定时间内处于非活动状态,之后又重新变为活动的,可以再次被捡起。 这种触发器都可以由TriggerRespawning类派生。
AI角色自主决策——有限状态机
决策系统的任务是对从游戏世界中收集到的各种信息进行处理(包括内部信息和外部信息),确定AI角色下一步将要执行的行为。
有两种行为:
- 更改AI角色的外部状态。(例如拨动开关、进人房间、开枪等)
- 引起内部状态的变化。(例如改变AI角色的心情状态、改变总体目标等)
有限状态机的FSM图
有限状态机(Finite State Machine:FSM) 由一组状态(包括一个初始状态)、输入和根据输入及现有状态转换为下一个状态的转换函数组成。
对于游戏AI来说,状态的关键意义是:不同的状态对应不同的行为。
-
有限状态机是AI系统中最简单的,同时也是最为有效和最常用的方法。 对于游戏中的每个对象,都可以在其生命期内区分出一些状态。 (例如,一个骑士可能正在武装自己、巡罗、攻击或在休息。一个农民可能在收集木材、建浩房屋,或在攻击下保护自己) -
当某些条件发生时,状态机从当前状态转换为其他状态。 在不同的状态下,游戏对象会对外部激励做出不同的反应或执行不同的动作。 有限状态机方法让我们可以很容易地把游戏对象的行为划分为小块,这样更容易调试和扩展。 -
用户编写的每个程序都是状态机。 每当写下一个if语句的时候,就创造出了一段至少拥有两个状态的代码——写的代码越多,程序就可能具有越多的状态。 Switch和if语句数量的爆发会让事情很快失去控制,程序会出现奇怪的bug,几乎不可能理解出现这些bug的原因,最后得到不可预知的后果。这样,该项目将很难理解和扩展。 -
有限状态机是AI中最容易的部分,但是也很容易出错。在设计有限状态机的时候,一定要认真地考虑清楚其中的每个状态和转换部分。 一个有限状态机必须要有一个初始状态,还要保存当前状态,另外还有一些事件作为触发状态转换的触发器,这些事件可能来自输入、关卡中的触发点、消息或轮询检测的某些条件。
表示有限状态机的最直接的方法是FSM图。
有限状态机是一个有着有限数目状态的机器,其中的一个状态是当前状态。 有限状态机可以接受输入,这会导致一个基于某些状态转移函数的、从当前状态到输出状态的状态变迁,然后输出状态就成为了新的当前状态。 因此,可以根据FSM图,建立一个状态转移矩阵。
依赖于怪物的当前状态和有限状态机的输人,怪物将会改变状态,执行基于怪物状态的游戏代码,将引起怪物的不同行为。 显然,可以引入更多的状态和输人,增加更多的状态转移,这就是利用有限状态机来创建AI角色行为逻辑的方式。
用Switch语句实现有限状态机
用FSM框架实现通用的有限状态机
FSM框架
每个“当前状态”对应着一个状态类。 这些状态类具有相同的基类——FSMState类。 每个状态类都拥有自己的“字典”,其中存储着当前状态所拥有的“转换——新状态”对,表明在这个状态(即这个类所代表的状态)下,如果发生某个“转换”事件(即上表的“输入”),有限状态机将会转移到何种新状态。 FSM框架中还有一个重要的类——AdvancedFSM类,它负责管理所有这些状态类(如PatrolState、ChaseState等)。
FSMState类——AI状态的基类
FSMState类是所有状态类的基类,它的每个派生类都代表了FSM中的某个状态,即,每个FSM状态都是从它派生出来。
状态类中具有添加转换、删除转换的方法,用于管理记录这些转换。
在FSMState类中,包含一个字典对象,称为map,用来存储“转换-状态”对。 表明在当前状态(即这个类所代表的状态)下,如果发生某个“转换”,FSM将会转移到何种状态。可以通过类中的AddTransition方法和DeleteTransition方法添加或删除“转换―状态”对。
另外,这个类中还包括Reason方法和Act方法。 其中,Reason方法用来确定是否需要转换到其他状态,应该发生哪个转换。Act方法定义了在本状态的角色行为。 在AdvanceFSM中定义了两个枚举,分别表示可能的转换和状态,并且分配了相应的编号。
AdvancedFSM类——管理所有的状态类
这个类FSM类的派生类,负责管理FSMState的派生类,并且随着当前状态和输入,进行状态更新。
需要注意的是,这个类中不能出现Start()、Update()以及FixedUpdate()函数,否则将覆盖基类中的相应函数。
AI角色的复杂决策——行为树
有限状态机难以模块化,编写代码麻烦且易出错。
行为树层次清晰,易于模块化,并且可以利用通用的编辑器简化编程,简洁高效。
行为树很适合用做AI编辑器,它为设计者提供了丰富的流程控制方法。 只要定义好一些条件和动作,策划人员就可以通过简单的拖拽和设置,来实现复杂的游戏AI。
|