上次提到,行为树可以让代码更加模块化,也可以提高重用性。这次我们就来看看一个行为树框架是什么样的。

如果你对行为树比较陌生,可以先浏览一下游戏AI - 行为树Part1:简介。

关键词

在展开之前,我们先定义几个关键词(基本都以BT作为前缀...是Behavior Tree之意,别误会了...),会在下面的框架用到。

BTNode:所有节点的base class。定义了一些节点的基本功能,并提供一些可继承的函数。

BTAction:行为节点,继承于BTNode。具体的游戏逻辑应该放在这个节点里面。

BTPrecondition:节点的准入条件,每一个BTNode都会有一个。具体的游戏逻辑判断可以继承于它。

BTPrioritySelector:Priority Selector逻辑节点,继承于BTNode。每次执行,先有序地遍历子节点,然后执行符合准入条件的第一个子结点。可以看作是根据条件来选择一个子结点的选择器。

BTSequence:Sequence逻辑节点,继承于BTNode。每次执行,有序地执行各个子结点,当一个子结点结束后才执行下一个。严格按照节点A、B、C的顺序执行,当最后的行为C结束后,BTSequence结束。

BTParallel:Parallel逻辑节点,继承于BTNode。同时执行各个子结点。每当任一子结点的准入条件失败,它就不会执行。

BTParallelFlexible:Parallel的一个变异,继承于BTNode。同时执行各个子节点。当所有子结点的准入条件都失败,它就不会执行。

BTTree:将所有节点组合起来的地方。

Database:黑板,一个存放共享数据的地方,可以看成是一个Key-Value的字典。为什么需要黑板呢?因为设计良好的行为逻辑,应该是独立的,可以在行为树的任何位置部署的。也就是说行为A和行为B并没有直接的沟通方法。黑板的作用就是作为一个行为树的“数据库”,让各个行为节点都可以储存数据进去,供感兴趣的行为节点利用。(同时,在Unity3d的语境下,Database继承MonoBehavior,可以提供各种Component给节点使用。)

UML类图:

代码资源

我们使用的框架的代码放在了Github:BT Framework。它是用 C# 写的,但概念可以转换到任何语言。

我们的Demo例子是Part1里提到的“贪生怕死的英雄”,Demo的代码可以在这里下载。Demo是用Unity3d写的。

行为树的构建

下面,我们会先从BT行为树框架的使用开始,然后再解释框架的实现。

BT行为树框架与外界的入口在BTTree,下面我们来看看BTTree的子类MoveAttackAI,我们在这里构建了一个行为树:

// MoveAttackAI.cs
// 一个继承于BT Tree的一个类protected override void Init () {// 初始化base classbase.Init();// 创建根节点,根节点_root = new BTPrioritySelector();// ... 创建准入条件,如checkOrcInSight// ... 创建行为/逻辑节点,如findDestination,run// 搭建行为树// Escape 节点BTParallel escape = new BTParallel(BTParallel.ParallelFunction.Or, checkOrcInSight);{escape.AddChild(findDestination);escape.AddChild(run);}_root.AddChild(escape);//... Fight 节点_root.AddChild(fight);//... Idle 节点_root.AddChild(idle);
}

上图就是我们的行为树了!它基本对应了Part1里面的图,不过有所修改。

1. 在上面,我们创建Root节点,创建准入条件,和行为/逻辑节点,然后通过AddChild来搭建行为树。

2. escape节点是一个Parallel逻辑节点,因为每次执行escape的时候我们都需要先找到逃跑的目的地,然后再跑。这时候可能有朋友会问,为什么不用Sequence呢?因为Sequence每次执行,都是按照行为A、行为B、行为C这样的顺序执行的,执行完行为C之后就结束。所以如果用Sequence,我们的目的地在跑到当前目的地之前就不能更新了。

3. findDestination,和run这些行为都是继承于BTAction 。但是为什么我们要将选择目的地和跑这个动作分开呢?是为了更好地分离逻辑——Escape的跑和Fight的跑是一样的,但目的地选择不一样,Escape的目的地是半兽人的相反方向,Fight的目的地是哥布林的位置。

4. 另外,在Part1的评论里面,@余冬冬老师提到

“为什么要有ROOT呢? 直接prority selector不可以么。”

在我们的例子里,Root的确就是一个Priority Selector!不过由于在base class——BTTree里面会对Root特别对待,所以在Part1里就特别提到它。

搭建一个行为树,最核心的就是上面的几行代码了,不难吧 :)

要在Unity3d里面的使用这一个行为树也很简单,在GameObject里面加入MoveAttackAI这个component就好(BTTree继承于MonoBehavior)。

如果在其他引擎当中使用,如Cocos2d-x,BTTree则应该拥有Update函数和自定义的初始化函数。

下面我们看看BT框架的实现。

框架的实现

BTNode和逻辑节点

BTNode提供了节点的最重要的接口:

// BTNode.cs
public abstract class BTNode {//...// 节点的准入条件public BTPrecondition precondition;// 黑板 public Database database;// 冷却功能public float interval = 0;// 当false的时候,节点不会执行public bool activated;// 节点初始化的接口,Database可提供Unity3d中的Component给节点使用public virtual void Activate (Database database) {//...}// 检查节点能否执行,包括是否activated,是否冷却完成,是否通过准入条件,和个性化检查 (DoEvaluate)public bool Evaluate () {bool coolDownOK = CheckTimer();return activated && coolDownOK && (precondition == null || precondition.Check()) && DoEvaluate();}// 给子类提供个性化检查的接口protected virtual bool DoEvaluate () {return true;}// 节点执行的接口,需要返回BTResult.Running,或者BTResult.Endedpublic virtual BTResult Tick () {return BTResult.Ended;}// 节点清零的接口public virtual void Clear () {}//...
}

BTNode提供给子类的接口中最重要的两个是DoEvaluate()和Tick()。

DoEvaludate给子类提供个性化检查的接口(注意和Evaluate的不同),例如Sequence的检查和Priority Selector的检查是不一样的。例如Sequence和Priority Selector里都有节点A,B,C。第一次检查的时候,

Sequence只检查A就可以了,因为A不通过Evaluate,那么这个Sequence就没办法从头开始执行,所以Sequence的DoEvaludate也不通过。

而Priority Selector则先检查A,A不通过就检查B,如此类推,仅当所有的子结点都无法通过Evaluate的时候,才会不通过DoEvaludate。

Tick是节点执行的接口,仅仅当Evaluate通过时,才会执行。子类需要重载Tick,才能达到所想要的逻辑。例如Sequence和Priority Selector,它们的Tick也是不一样的:

Sequence里当active child节点A Tick返回Ended时,Sequence就会将当前的active child设成节点B(如果有B的话),并返回Running。当Sequence最后的子结点N Tick返回Ended时,Sequence也返回Ended。

Priority Selector则是当目前的active child返回Ended的时候,它也返回Ended。Running的时候,它也返回Running。

正是通过重载DoEvaluate和Tick,BT框架实现了Sequence,PrioritySelector,Parallel,ParalleFlexible这几个逻辑节点。如果你有特殊的需求,也可以重载DoEvaluate和Tick来实现!

BTAction

BTAction是负责游戏逻辑的行为节点,也就是行为树里面的“行为”。

// BTAction.cs
public class BTAction : BTNode {private BTActionStatus _status = BTActionStatus.Ready;//...// 第一次进入行为protected virtual void Enter () {//... Debug functionality}// 离开行为protected virtual void Exit () {//... Debug functionality}// 行为的执行,返回BTResultprotected virtual BTResult Execute () {//...}// 重载BTNode的Tick,加入了Enter,Exit,Execute的概念public override BTResult Tick () {BTResult result = BTResult.Ended;if (_status == BTActionStatus.Ready) {Enter();_status = BTActionStatus.Running;}// not using else so that the status changes reflect instantlyif (_status == BTActionStatus.Running) {      result = Execute();if (result != BTResult.Running) {Exit();_status = BTActionStatus.Ready;}}return result;}// 重载清零接口,因为外部没有办法调用Exitpublic override void Clear () {// not cleared yetif (_status != BTActionStatus.Ready) {   Exit();_status = BTActionStatus.Ready;}}//...private enum BTActionStatus {Ready = 1,Running = 2,}
}

BTAction里面最重要的是Tick,它重载了BTNode的Tick,增加了对Enter,Exit,Execute的支持。如果大家对有限状态机比较熟悉,一个状态机里面的状态通常都会支持这三个方法,分别用来初始化,清零,和执行逻辑。在每一次行为节点的一个运行周期(不是生命周期)里,Enter仅在一开始被调用,Exit仅在最后被调用,Execute会在每一次Tick被调用。

例如我们可以这样实现DoRun:

// DoRun.cs// 在某些简单的情况下,没有必要将动画和位移逻辑分开的话,可以这样写;
// 但通常为了更好的逻辑分离,我并不会将它们放在一起。而是分成两个不同的行为。
//   protected override void Enter () {//      database.GetComponent<Animator>().Play("Run");
//   }protected override BTResult Execute () {//...if (CheckArrived()) {return BTResult.Ended;   // 告诉父节点我要结束了}MoveToDestination();return BTResult.Running;   // 告诉父节点我还在运行
}

就是这么简单!

同时,我们可以看到DoRun并没有引用行为节点(也不应该引用),也就是说,它是一个逻辑上独立的行为节点,可以部署到行为树的任何位置。行为节点的逻辑独立,可以让我们写的每一个行为,都可以放到我们自己的逻辑库里面,给以后的项目调用!

Demo的局限和改进方法

如果你有耐心看到这里,你一定已经发现了Demo有一个bug——当半兽人和哥布林在同一方向(相对于英雄)的时候,英雄会先逃跑,然后在某一个点上迅速来回翻转。这是因为AI在Escape和Fight这两个分支上快速切换。

一个改进的方法是分等级的行为树(Hierarchical Behavior Tree)[1]:

有一个做决策的行为树A,和一个按照命令执行的行为树B。A根据游戏世界的情况做出决策,然后将命令放到Database里,然后B根据命令做出动作。由于两个行为树都放在一个Game Object里,所以Database是A、B共享的。通常,决策者A并不会每一帧都做出决策,而是设定一个冷却时间。

// DecisionAI.cs// 设定1.5秒的冷却时间
_root.interval = 1.5f;

改进的Demo代码可以在这里下载。

这样一个分等级的行为树有两个好处:

  1. 让决策逻辑和执行逻辑分离。面对同样的决策,不同Game Object可能有不同的执行方法。
  2. 玩家控制的角色和AI控制的角色可以分享同一个执行逻辑——只需要负责玩家控制的代码将命令存放到Database里面供执行逻辑使用就可以了。

总结

  • 我们从BT框架的使用为学习入口,解释了行为树框架的实现原理;
  • 也了解了怎么去拓展出个性化的逻辑节点和行为节点来满足项目需求;
  • 通过逻辑独立地拓展BTAction,我们能够积累自己的逻辑库!
  • 最后我们提到了分等级的行为树,它可以帮助我们将决策逻辑和执行逻辑分开(并解决了demo里面的一个bug)。

BT框架还可以怎样拓展?我的下一个目标就是将它打造成一个Unity3d的插件,可以通过GUI来搭建行为树,而不用通过代码——当然,行为节点还是得自己用代码写。

题外话

在最近几次参加game jam的时候,我开始全程使用BT framework,做了几个小游戏:

  1. 剑侠,可以参见这篇笔记的编程部分,有讲到用BT framework的感想 :)
  2. 弓箭手,9月低的一个练手小作品 ;p

最后,如果大家觉得文章不错,请帮忙在github里的BT framework点星喔!:D

Reference

[1]“分等级的行为树”这个名字的由来:Behavior Trees for Hierarchical RTS AI

游戏AI - 行为树Part2:框架相关推荐

  1. 游戏ai 行为树_游戏AI –行为树简介

    游戏ai 行为树 游戏AI是一个非常广泛的主题,尽管有很多资料,但我找不到能以较慢且更易理解的速度缓慢介绍这些概念的东西. 本文将尝试解释如何基于行为树的概念来设计一个非常简单但可扩展的AI系统. 什 ...

  2. 游戏AI——行为树理论及实现

    从上古卷轴中形形色色的人物,到NBA2K中挥洒汗水的球员,从使命召唤中诡计多端的敌人,到刺客信条中栩栩如生的人群.游戏AI几乎存在于游戏中的每个角落,默默构建出一个令人神往的庞大游戏世界. 那么这些复 ...

  3. 游戏AI –行为树简介

    游戏AI是一个非常广泛的主题,尽管有很多资料,但我找不到能以较慢,更容易理解的速度缓慢介绍这些概念的东西. 本文将尝试解释如何基于行为树的概念来设计一个非常简单但可扩展的AI系统. 什么是AI? 人工 ...

  4. 谈一谈游戏AI - 行为树

    不要用过去的成绩看未来,而是要用未来的眼睛看现在. 郑重说明:本文适合对游戏开发感兴趣的小白初学者,本人力图将事物用简单的语言表达清楚,但水平有限,能力一般,文章如有错漏之处,还望批评指正. 在本系列 ...

  5. 人工智能_游戏AI –行为树简介

    人工智能 游戏AI是一个非常广泛的主题,尽管有很多材料,但我找不到能以较慢,更易理解的速度缓慢引入这些概念的东西. 本文将尝试解释如何基于行为树的概念来设计一个非常简单但可扩展的AI系统. 什么是AI ...

  6. 游戏AI—行为树研究及实现(转自月夜魔术师 https://segmentfault.com/a/1190000012397660)

    行为树简介 行为树是一种树状的数据结构,树上的每一个节点都是一个行为.每次调用会从根节点开始遍历,通过检查行为的执行状态来执行不同的节点.他的优点是耦合度低扩展性强,每个行为可以与其他行为完全独立.目 ...

  7. AI行为树的基础运作原理

    欢迎捉虫! 之前我研究了一下基于switch case语句的FSM状态机的使用,后来遇到了很多问题. 比如当角色的行为很多时,代码结构相当混乱(你需要考虑每一种状态之间的联系). 所以,当角色的行为愈 ...

  8. 游戏AI:只是AI间的游戏,还是游戏的未来?

    前言背景 1. 雅达利的崛起与沉沦 1974年,一个名叫史蒂夫·乔布斯的年轻人来到了雅达利(Atari)公司位于洛思加图斯的总部,拿着一块他朋友沃兹尼亚克做的电路板,手舞足蹈的比划,试图让对方相信这个 ...

  9. 游戏AI,行为树,Lua框架

    行为树(缩写BT),故名思议是一个树状结构,它是用树的方式来描述一个角色的行为.书本上的一些概念就不进行说明了.(本文仅代表个人理解的一个简易版的行为树框架,适用于轻量级的AI逻辑,处理不当的地方还请 ...

最新文章

  1. 2022-2028年中国领带行业投资分析及前景预测报告
  2. SAP RETAIL物料组的分配规则
  3. 图像质量损失函数SSIM Loss的原理详解和代码具体实现
  4. Linux停止后台运行Django项目
  5. 当AV1视频编解码器来到Webex!
  6. How to put S4 extension field to CRM WebUI search view in the design time
  7. PaperNotes(19)-Learning Lane Graph Representations for Motion Forecasting
  8. Flex4学习笔记(二)--语法相关
  9. 日更100天(53)每天进步一点点
  10. MOEA/D原理及pyton实现
  11. mysql 创建事件_MySQL创建事件(CREATE EVENT)
  12. 2022电大国家开放大学网上形考任务-科学与技术非免费(非答案)
  13. win7访问局域网计算机提示凭据,win7系统访问局域网共享文件时提示输入网络凭据的解决方法?...
  14. [渝粤教育] 中国地质大学 工业通风及除尘 复习题 (2)
  15. hdu 1869 六度分离(bfs)
  16. 第二章:第一节数据清洗及特征处理-自测
  17. 【C/C++】【面经】2022 网易互娱面经( 游戏客户端方向 )(更新:一面;二面;)
  18. golang爬取免费代理IP
  19. 胡阳pyhton作业题--20150801
  20. JL-03-Q9 自动气象站 常见气象9参数 空气温湿度 风速风向 雨量光照 大气压力 土壤温湿度

热门文章

  1. 时尚的不仅仅是它们的服装,还有它们的网站设计
  2. 形容计算机专业的诗句,形容很专业的诗句
  3. win10打印机拒绝访问解决方法
  4. iphone消息推送原理
  5. 球型网络摄像机是什么
  6. 四大加密技术保护移动存储
  7. docker 的下载与基本使用
  8. 命令行计算机ipconfig,ipconfig命令,教您ipconfig命令怎么使用
  9. Swift 通过touchesBegan 方法获取用户点击的view,模拟连续点击效果
  10. 马云对996的表态,会让他跌下神坛吗?