游戏AI之决策结构—有限状态机/行为树(2)
游戏AI的决策部分是比较重要的部分,游戏程序的老前辈们留下了两种经过考验的用于AI决策的结构:
- 有限状态机
- 行为树
在以前,游戏AI的实现基本都是有限状态机,
随着游戏的进步,游戏AI的复杂性要求越来越高,传统的有限状态机实现很难维护越来越复杂的AI需求。
现代游戏AI都比较偏向采用行为树作为决策结构。
有限状态机
有限状态机的一般实现是将每个状态写成类,再用一个载体(也就是所谓的状态机)管理这些状态的切换。
关于状态机设计模式的具体介绍,可参考我的另一篇博文: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; //执行函数,返还 成功/失败
};
下面列出一些控制节点的介绍:
选择节点(Selector)
按顺序执行多个子节点,若成功执行一个子节点,则不继续执行下一个子节点。

举例:实现要不攻击,要不防御,要不逃跑。
用一个选择节点,按顺序添加<攻击节点>和<防御节点>和<逃跑节点>作为子节点。
class SelectorNode : public NonLeafNode{
public:
virtual bool excute()override{
for(auto child : children){
//如果有一个子节点执行成功,则跳出
if(child->excute() == true){break;}
}
return true;
}
};
顺序节点(Sequence)
按顺序执行多个子节点,若遇到一个子节点不能执行,则不继续执行下一个子节点。

举例:实现先开门再移动到房子里。
用一个顺序节点,按顺序添加<开门节点>和<移动节点>作为子节点。
class SequenceNode : public NonLeafNode{
public:
virtual bool excute()override{
for(auto child : children){
//如果有一个子节点执行失败,则跳出
if(child->excute() == false){break;}
}
return true;
}
};
并行节点(Parallel)
同时执行多个节点。

举例:一边说话和一边走路。
用一个并行节点,添加<说话节点>和<走路节点>作为子节点。
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。
相比较传统的有限状态机:
- 易脚本化/可视化的决策逻辑
- 逻辑和实现的低耦合,可复用的节点
- 可以迅速而便捷的组织较复杂的行为决策
这里并不是说有限状态机一无所用:
- 状态机可以搭配行为树:状态机负责智能体的身体状态,行为树则负责智能体的智能决策。这样在行为树做决策前,得考虑状态机的状态。
- 状态机适用于简单的AI:对于区区需两三个状态的智能,状态机解决绰绰有余。
- 状态机运行效率略高于行为树:因为状态机的运行总是在当前状态开始,而行为树的运行总在根开始,这样就额外多了一些要遍历的节点(也就多了一些运行开销)。
在《杀手:赦免》的人群系统里,人群的状态机AI只有简单的3种状态,由于人群的智能体数量较多,若采取行为树AI,则会大大影响性能。
简而言之:行为树是适合复杂AI的解决方案。
- 对于Unity用户,Unity商店现在已经有一个比较完善的行为树设计(Behavior Designer)插件可供购买使用。
Unity官方商店插件购买地址:Behavior Designer - Behavior Trees for Everyone - Asset Store

- Unreal4用户则可以免费使用引擎自带的行为树

额外
可让根节点记录该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
游戏AI之决策结构—有限状态机/行为树(2)的更多相关文章
- 游戏AI之初步介绍(0)
目录 游戏AI是什么? 游戏AI和理论AI 智能的假象 (更新)游戏AI和机器学习 介绍一些游戏AI 4X游戏AI <求生之路>系列 角色扮演/沙盒游戏中的NPC 游戏AI 需要学些什么? ...
- 趣说游戏AI开发:对状态机的褒扬和批判
0x00 前言 因为临近年关工作繁忙,已经有一段时间没有更新博客了.到了元旦终于有时间来写点东西,既是积累也是分享.如题目所示,本文要来聊一聊在游戏开发中经常会涉及到的话题--游戏AI.设计游戏AI的 ...
- 王亮:游戏AI探索之旅——从alphago到moba游戏
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由云加社区技术沙龙 发表于云+社区专栏 演讲嘉宾:王亮,腾讯AI高级研究员.2013年加入腾讯,从事大数据预测以及游戏AI研发工作.目前 ...
- 使用行为树(Behavior Tree)实现游戏AI
——————————————————————— 谈到游戏AI,很明显智能体拥有的知识条目越多,便显得更智能,但维护庞大数量的知识条目是个噩梦:使用有限状态机(FSM),分层有限状态机(HFSM),决策 ...
- 做游戏长知识------基于行为树与状态机的游戏AI(一)
孙广东 2014.6.30 AI. 我们的第一印象可能是机器人,如今主要说在游戏中的应用. 现代的计算机游戏中已经大量融入了AI元素,平时我们进行游戏时产生的交互都是由AI来完毕的.比方在RPG游戏中 ...
- Unity教程之-基于行为树与状态机的游戏AI
AI.我们的第一印象可能是机器人,现在主要说在游戏中的应用.关于AI的相关文章我们在前面也提到过,详细请戳这现代的计算机游戏中已经大量融入了AI元素,平时我们进行游戏时产生的交互都是由AI来完成的.比 ...
- 实现简易而强大的游戏AI——FSM,有限状态机
http://blog.friskit.me/2012/05/introduction-of-fsm/ 在很久很久以前,受限于计算机性能和图形效果,游戏往往是以玩家为唯一主动对象的,玩家发出动作,游戏 ...
- 如何建立一个完整的游戏AI
http://blog.friskit.me/2012/04/how-to-build-a-perfect-game-ai/ 人工智能(Artificial Intelligence)在游戏中使用已经 ...
- 游戏AI的综合设计
原地址:http://www.cnblogs.com/cocoaleaves/archive/2009/03/23/1419346.html 学校的MSTC要出杂志,第一期做游戏专题,我写了一下AI, ...
随机推荐
- flash builder 4.6与myecilpse 10.7集成
一.在flash builder 4.0以后就没有单独提供插件版的flash builder了,因此必须先安装完整版的flash builder,再进行插件集成. 二.集成过程比较简单但也有几个要注意 ...
- 《T-SQL查询》读书笔记Part 1.逻辑查询处理知多少
一.关于T-SQL T-SQL是ANSI和ISO SQL标准的MS SQL扩展,其正式名称为Transact-SQL,但一般程序员都称其为T-SQL. 二.逻辑查询处理各个阶段 2.1 逻辑查询处理流 ...
- 嵌入Python | 调用Python模块中有参数的函数
开发环境Python版本:3.6.4 (32-bit)编辑器:Visual Studio CodeC++环境:Visual Studio 2013 需求说明前一篇<在C++中嵌入Python|调 ...
- Python_网络攻击之端口
#绝大多数成功的网络攻击都是以端口扫描开始的,在网络安全和黑客领域,端口扫描是经常用到的技术,可以探测指定主机上是否 #开放了指定端口,进一步判断主机是否运行了某些重要的网络服务,最终判断是否存在潜在 ...
- java ArrayList集合
ArrayList集合是程序中最常见的一种集合,它属于引用数据类型(类).在ArrayList内部封装了一个长度可变的数组,当存入的元素超过数组长度时,ArrayList会在内存中分配一个更大的数组来 ...
- Masonry 抗压缩 抗拉伸
约束优先级: 在Autolayout中每个约束都有一个优先级, 优先级的范围是1 ~ 1000.创建一个约束,默认的优先级是最高的1000 Content Hugging Priority: 该优先级 ...
- Maven学习(三)-- 使用Maven构建项目
摘自:http://www.cnblogs.com/xdp-gacl/p/4240930.html maven作为一个高度自动化构建工具,本身提供了构建项目的功能,下面就来体验一下使用maven构建项 ...
- cmd命令行下登陆备份导入导出msql数据
1.进入服务,找到mysql服务,在属性里找到mysql的安装路径 2.登陆 mysql -h 192.168.0.11 -P 3310 -u root -p 如果是访问的本机并且端口是默认的,那么 ...
- RabbitMQ入门:认识并安装RabbitMQ(以Windows系统为例)
最近在学习Spring Cloud,其中消息总线Spring Cloud Bus是必不可少的,但是Spring Cloud Bus目前只支持RabbitMQ和kafka,因此学习RabbitMQ势在必 ...
- 对混合数值,字符,null的字段进行排序
今天有个需求是进行排序. 这一列值是字符串类型的, 但是里面有数值型 比如"1" 和null类型的. 实现效果是需要 数值型的先按照数值的方式先排,然后字符串按照字符传排,最后 ...