游戏AI的决策部分是比较重要的部分,游戏程序的老前辈们留下了两种通过考验的用于AI决策的结构:html
在之前,游戏AI的实现基本都是有限状态机, 随着游戏的进步,游戏AI的复杂性要求愈来愈高,传统的有限状态机实现很难维护愈来愈复杂的AI需求。 现代游戏AI都比较偏向采用行为树做为决策结构。设计模式
有限状态机的通常实现是将每一个状态写成类,再用一个载体(也就是所谓的状态机)管理这些状态的切换。ide
关于状态机设计模式的具体介绍,可参考个人另外一篇博文:https://www.cnblogs.com/KillerAery/p/9680303.html模块化
有限状态机的缺陷:函数
能够看到,行为树由一个个节点组成性能
咱们规定,每一个节点都提供本身的excute函数,返还执行失败/成功结果。 而后根据不一样节点的执行结果,遍历的路径随之改变,而这个过程当中遍历到什么节点就执行excute函数。优化
//节点类(基类) class Node{ //... public: virtual bool excute() = 0; //执行函数,返还 成功/失败 //... };
主流的行为树实现,将节点主要分为四种类型。 下面列举四种节点类型及其对应excute函数的行为:插件
控制节点是用于控制如何执行子节点(控制遍历路径的走向)。线程
因为非叶节点的特性,其须要提供容纳子节点的容器和添加子节点的函数。 因此先写好非叶节点的类:设计
class NonLeafNode : public Node { std::vector<Node*> children; //子节点群 public: void addChild(Node*); //添加子节点 virtual bool excute() = 0; //执行函数,返还 成功/失败 };
下面列出一些控制节点的介绍:
按顺序执行多个子节点,若成功执行一个子节点,则不继续执行下一个子节点。
举例:实现要不攻击,要不防护,要不逃跑。 用一个选择节点,按顺序添加<攻击节点>和<防护节点>和<逃跑节点>做为子节点。
class SelectorNode : public NonLeafNode{ public: virtual bool excute()override{ for(auto child : children){ //若是有一个子节点执行成功,则跳出 if(child->excute() == true){break;} } return true; } };
按顺序执行多个子节点,若遇到一个子节点不能执行,则不继续执行下一个子节点。
举例:实现先开门再移动到房子里。 用一个顺序节点,按顺序添加<开门节点>和<移动节点>做为子节点。
class SequenceNode : public NonLeafNode{ public: virtual bool excute()override{ for(auto child : children){ //若是有一个子节点执行失败,则跳出 if(child->excute() == false){break;} } return true; } };
同时执行多个节点。
举例:一边说话和一边走路。 用一个并行节点,添加<说话节点>和<走路节点>做为子节点。
class ParallelNode : public NonLeafNode{ public: virtual bool excute()override{ //执行全部子节点 for(auto child : children){ child->excute(); } return true; } };
经常使用的控制节点通常是<并行节点><选择节点><并行节点>。固然还有其余更多控制节点种类(不经常使用):
可能到这里,有想到还有个问题:为何控制节点也须要提供(执行成功/执行失败)两种执行结果。 答:这样作就能够作到决策的复合——控制节点不只能够控制行为节点,也能控制控制节点。
执行节点不会老是一路顺风的,有成功也总会有失败的结果。 这就是引入前提条件的做用—— 知足前提条件,才能成功执行行为,返还执行成功结果。不然不能执行行为,返还执行失败结果。
可是每一个节点的前提总会不一样,或有些没有前提(换句话说老是能知足前提)。
一个可行的作法是:让行为节点含有bool函数对象(或函数接口)。这样对于不一样的逻辑条件,就能够写成不一样的bool函数,绑定给相应的行为节点。
std::function<bool()> condition; //前提条件
如今比较成熟的作法是把前提条件抽象分离成新的节点类型,称之为条件节点。 将其做为叶节点混入行为树,提供条件的判断结果,交给控制节点决策。
它至关模块化,更加方便适用。
这里的Sequence节点是上面控制节点的一种:可以让其全部子节点依次运行,若运行到其中一个子节点失败则不继续往下运行。 这样能够实现出不知足条件则失败的效果。
class ConditionNode : public Node { std::function<bool()> condition; //前提条件 public: virtual bool excute()override { return condition(); } };
行为节点是表明智能体行为的叶节点,其执行函数通常位该节点表明的行为。
行为节点的类型是比较多的,毕竟一个智能体的行为是多种多样的,并且都得根据本身的智能体模型定制行为节点类型。 这里列举一些行为:站立,射击,移动,跟随,远离,保持距离....
一些行为是能够瞬间执行完的(例如转身?), 而另一些动做则是执行持续一段时间才能完成的(例如攻击从启动攻击行为到攻击结算要1秒左右的时间)。
所以,这些持续行为节点的excute函数里,应先启动智能体的持续行为,而后挂起该行为树(更通俗地说是暂停行为树),等到持续时间结束才容许退出excute函数并继续遍历该行为树。 为了支持挂起行为树而不影响其余CPU代码执行,咱们每每须要利用协程等待该其行为完成而不产生CPU阻塞,并且开销远低于真正的线程。 此外,通常是一个行为树对应维护一个协程。
不了解协程是什么,能够参考下个人Unity协程笔记:Unity C#笔记 协程 - KillerAery - 博客园
//行为节点类(基类) class BehaviorNode : public Node{ public: virtual bool excute() = 0; //执行节点 };
//举例:移动行为节点 class MoveTo : public BehaviorNode{ public: virtual bool excute()override{ ... //让智能体启动移动行为 ... //协程暂时挂起直到持续时间结束 return true; } };
装饰节点,顾名思义,是用来修饰(辅助)的节点。
例如执行结果取反/并/或,重复执行若干次等辅助修饰节点的做用,都可作成装饰节点。
//取反节点 class InvertNode : public OneChildNonLeafNode{ public: virtual bool excute()override{ return !child->excute(); } };
//重复执行次数节点 class CountNode : public OneChildNonLeafNode{ int count; public: virtual bool excute()override{ while(--count){ if(child->excute() == false)return false; } return true; } };
OneChildNonLeafNode是指最多可拥有一个子节点的非叶节点类,这里就不作具体实现。
到这里,咱们能够看到行为树的本质:
相比较传统的有限状态机:
这里并非说有限状态机一无所用:
在《杀手:赦免》的人群系统里,人群的状态机AI只有简单的3种状态,因为人群的智能体数量较多,若采起行为树AI,则会大大影响性能。
简而言之:行为树是适合复杂AI的解决方案。
Unity官方商店插件购买地址:Behavior Designer - Behavior Trees for Everyone - Asset Store
可以让根节点记录该AI要操控的智能体引用(指针),每次进行决策,传给子节点当前要操控的智能体引用。这样就可使AI行为树容易改变寄主。 (例如1个丧尸死了被释放内存了,寄生它的AI行为树没必要释放并标记为可用。一旦产生新的丧尸,就能够给这个行为树根节点更换新的寄主,标记再改回来)
得益于树状结构,重复执行次数节点(或其余相似的节点),可让它执行完相应的次数后,解开与父节点的链接,释放本身以及本身的子节点。
共享节点型行为树是可供多个智能体共用的一种行为树,是节省内存的一种设计:http://www.aisharing.com/archives/563
LOD优化技术:LOD本来是3D渲染的优化技术。对于远处的物体,渲染面数能够适当减小,对于近处的物体,则须要适当增长细节渲染面数。 一样的能够用于AI上,对于远处的AI,不须要精准每帧执行,能够适当延长到每若干帧执行。
一个武装小队队员的AI行为树示例:
游戏AI 系列文章:https://www.cnblogs.com/KillerAery/category/1229106.html
[Toc]