
网上比较多的状态机都是结合其实际的应用场景包含一些特定的实现设计在里面我们在搭建自己的状态机的时候可以从一个细小的状态开始搭建先搭建一个节点然后依据同样的设计搭建类似的多个节点2、保持节点的简洁性和去耦性。通常在最初的节点搭建完后是不可能一劳永逸的后续反复的修改都会对这些节点进行特定的修改和某些特定规则的设计。举个列子射击这样一个状态节点A英雄的设计是单发B英雄是连续10发那么如何保证对这两个英雄的同一个射击状态的兼容一种设计方式是将当前的英雄作为一个参数进行传递那么在执行的时候就读取当前传入参数的英雄的具体配置进行相关的射击动作。把射击相关的逻辑封装在该英雄对应的攻击执行器中那么其具体执行的射击单发还是射击10发就可以作为一个循环执行单发循环一次10发循环10次。以前我的设计实现思路是针对单一的英雄特定重载实现其对应的射击节点这样也算一种解决方法只是这样的设计有一个弊端就是随着英雄类型的增多其对应的特定实现会不断的扩大相应的重载实现的版本会增多。如果设计思路和文本清晰那还可以维护如果设计思路不清晰那么就会带来维护的消耗。3、做好节点的剥离避免节点耦合太多。通常状态机是用在比较简易的游戏类型中角色本身的状态类型不会太多那么对应的状态节点应该避免相互之间的耦合和功能交叉状态的切换可以走相同的属性设置来实现交互。如果随着设计的变化状态实现越来越多可以考虑用行为树来实现避免相互之间的耦合太多。二、行为树在RPG游戏中行为树是用的比较多的一种AI机制。就其本质而言行为树是状态机的一种更高封装的实现。我个人的理解行为树就是将特定的行为节点进行封装做成叶子节点这样可以实现任意节点的拼接。想象一下如果我们把每个特定的状态机进一步的封装做成一片片叶子这样我们在搭建树的时候就可以收集特定的叶子来搭建特定的树最后得到特定功能的行为树这就是我对行为树的一个简易理解:D 如果用更为规范的说法那么这些叶子节点就是行为树中的行为节点行为树的行为节点的执行结果可以用枚举的方式列出RunningStatus { INVALID 0, SUCCESS 1, --执行成功 FAILURE 2, --执行失败 RUNNING 3, --执行中 }一棵树的搭建不能只有叶子还需要有枝干这就需要行为树中的一些特定的节点来搭建这棵树这就是行为树中的控制节点Control Node的作用。注意一点行为节点是和游戏实际关联的在行为节点中我们会去具体的定义如何攻击(shoot/attack)寻路(patrol)闲置(idle)等但是在控制节点中其具体运行的逻辑是和实际游戏数据没有关联的其只需要负责基本的控制逻辑即可 。通过控制节点我们可以清晰的知道整个行为树的执行逻辑这就是行为树的一大优势执行逻辑可见。 行为树主要有以下几种控制节点举例说一下4种Composite节点1、Sequence节点顺序执行控制节点其执行的基本逻辑是对其下面的所有叶子节点均顺序执行直到遇到返回为FAILURE的节点。我对这种节点的理解就是串联电路的思维电流顺序走过每个电阻叶子如果遇到第一个无法流过的电阻则返回否则会一直流过去直到所有电阻都流过2、Selector节点选择执行控制节点其执行的基本逻辑是对其下面的所有叶子节点顺序执行直到遇到第一个返回为SUCCESS/RUNNING执行结果的节点。观察上面的Sequence节点和Selector节点的区别仅仅在于选中的节点的返回结果的不同处理。在其他地方好像对选择节点进行了分类可以分为带优先级的选择节点不带优先级的选择节点带权值的选择节点。带优先级是指在选择那个节点更新的时候会根据优先级进行比较来选择。不带优先级的时候一般会设置为每次更新的时候会沿用上一次的更新节点因为一般更新的时候会有一段时间持续处于某个节点的状态目前我用的就是这样的选择节点而对于带权重的选择节点一般用来做多样的随机性比如游戏中的宠物需要表现一个交互动作那么可以做多个交互动作在每次做选择节点的时候根据权重随机一个节点用来表示交互动作。3、Parallel节点并行执行控制节点其执行的基本逻辑是对其下面的所有叶子节点各自执行一次如果返回结果不为RUNNING则分别统计SUCCESS和FAILURE的结果一般结果会采用“与”和“或”的操作。比如当前Parallel节点的返回条件可以分为 全SUCCESS或者任意一个SUCCESS全FAILURE或者任意一个FAILURE。在执行完所有叶子节点后可以对比其执行的SUCCESS和FAILURE节点的个数和其设置的返回结果对比如果满足则返回SUCCESSSUCCESS条件或者FAIULUREFAILURE条件或者返回RUNNING。4、Detector节点检测执行控制节点其执行的基本逻辑是对其下面的所有叶子节点逐个执行如果返回的不为RUNNING则检测如果为SUCCESS则接着执行为FAILURE则执行结束。即遇到第一个返回为FAILURE的节点就结束执行。6种Decorator节点1、Invert节点反转装饰节点其执行的基本逻辑是只有一个叶子节点如果返回为SUCCESS则返回FAILURE如果返回为FAILURE则返回SUCCESS否则返回RUNNING2、Sucees节点Success装饰节点其执行的基本逻辑是只有一个叶子节点执行后直接返回SUCCESS3、SuccessToRunning节点根据名字就可以知道其叶子节点在执行完后如果返回为SUCCESS则修改其结果为RUNNING其他的状态不改变返回最终执行状态给上一层。4、FailureTORunning节点类似于上一个节点其叶子节点在执行完后如果返回为FAILURE则修改其结果为RUNNING其他的状态不改变返回最终执行状态给上一层。5、SuppressSuccess节点suppress的意思是抑制所以这个节点的功能就是不准返回SUCCESS的执行结果如果叶子节点返回为RUNNING则返回RUNNING其他都返回为FAILURE给上一层6、SuppressFailure节点类似于上一个节点该节点只会返回RUNNING或者SUCCESS这两种执行结果除了常见的Compositor节点和Decorator节点还有Condition节点依据前面两种类型节点不难推出后面的Condition节点的功能就是在某些条件下才触发某些特定返回结果的一些节点。简而言之行为树就是在三个大类的控制节点Compositor节点、Decorator节点、Condition节点的搭建下结合各个行为叶子节点拼接出一个基本的AI执行机制举个例子bt_atkRoot Selector Attack Homing /Selector /Root这是一个简单的站在原地攻击的行为树设计首先执行Selector节点然后顺序执行其下面的所有叶子节点首先会执行攻击的叶子节点如果返回为Success则继续执行Homing节点。所以其基本的设计思想就是触发一次攻击检测如果有攻击对象则执行攻击返回SUCCESS同时结束本次tick否则返回FAILUER那么就会执行下一个节点Homing。通过构建一颗基本的行为树我们可以组件一颗更大的树比如我们再构建一颗巡逻的行为树bt_patrolRoot Selector Patrol Homing /Selector /Root通过这两颗基本的行为树我们可以组建一个更大的行为树bt_monsterRoot Selector Sequence Patrol Homing /Sequence Sequence Attack Homing /Sequence /Selector /Root当然我只是一个引申更加具体的设计可以根据具体的设计来实现有时候不只是叶子节点可以复用某些树也可以整体作为一个叶子复用这样可以实现设计的复用。在行为树中一般的设计思路是会构建一个行为树的模版比如上面的bt_atk在每个使用该模版的角色进行初始化行为树的时候是从该模版缓存中取出一份然后初始化相关的信息得到一份实例在进行行为树更新的时候是更新其对应的实例。而且为了通用各个树干和叶子节点都是采用数据封包传递在数据包中封闭当前角色相关的信息或者黑板信息这样在每个节点更新的时候都是从数据封包中获取当前角色相关的信息从而进行对应的逻辑更新。这样就避免了一些设计上的将数据封存在action行为节点上的问题通过封包的传递可以降低耦合提高复用性。对于行为树的优化我提一个优化点吧。大部分的行为树在具体的项目中应用都是结合具体的设计来实现的所以我采用的优化未必适合于其他游戏的优化但是可以采用一个更新频率的设置来降低行为树的更新频率这个是可以通用的。我在测试服务器的代码性能的时候发现当场景中的角色数量比较少的时候大部分的游戏性能都被场景中的怪占用了而怪物的更新中对于AI的更新又是一个很大的占用。我采用一种距离配置的方法进行优化当怪物周边没有玩家的时候降低怪物的更新频率或者就不执行怪物的AI更新当怪物进入玩家的视野的时候采用较高的更新频率当怪物进入玩家的攻击范围的时候采用正常的更新频率。通过不同距离检测设置不同的更新频率可以较好的优化在玩家个数较少时的怪物AI性能。总结游戏中两种常见的状态机和行为树都做了一个简单的讲解当然现在网上比较多相关的资料如果想深入的学习可以搜集相关的资料研究有较多的开源代码也可以参考研究一下。当然我说的都是较为浅显的设计具体的状态机和行为树节点的设计其中具体逻辑的编写是需要结合实际的游戏设计来实现的这就需要程序和策划具体的商量和实现了。好了今天AI的简介就说到这儿下一篇再说说一些优化的总结吧