共享节点型的行为树架构(1)

我在博客上已经聊了很多关于行为树的基本概念了,我也实现了一个简单的行为树库供大家参考,在实践中,我一直没有考虑过行为树对于内存占用的问题,因为在我上一个足球的项目里,场上一共就是10个人,内存的使用相较于其他模块而言,实在是微乎其微。但如果对于存在大量NPC的游戏来说,每一个NPC都生成一个完整的行为树实例,那内存的占用量就显得很可观了,特别是对于网络程序的服务器端,或者对于移动设备上的游戏来说,内存的优化,是不得不考虑的问题,所以这就有必要来重新思考一下优化行为树内存占用的策略。

前段时间在aigamedev上看到一篇介绍行为树的视频里,提到了一种共享节点型的行为树的概念,可以改善内存的使用情况,在这里,和大家一起分享和探讨一下。

在原本的行为树架构里,我们提到的一个最终要的概念就是“节点”,不论是“控制节点”,还是“行为节点”,从代码层面上来说,都是继承自“节点”,换句话说,就是节点的一种。行为树就是由这些节点所组成的,在实践中,每一个节点其实包含了两种数据,一个是构成行为树结构的数据(Structure Data),比如子节点指针,父节点指针,通用数据等等,这些数据用来搭建出行为树。还有一种数据是运行时数据(Runtime Data),是运行时会变化的,比如一些计数器,游戏相关数据等等。假设我们有行为树A,它会被附加在100个NPC上,按照原来的架构,我们会复制100份A的实例分别给这100个NPC。如果仔细分析的话,因为他们用的是同一种行为树结构,所以构成行为树结构的数据在这100个A的实例中是一模一样!由此我们就可以想到,其实,所有的这100个A的实例中只需要共享一个份结构数据就可以了,这样就可以节省很多的内存消耗,如下图所示:

SharedBevtreeNode

这样的行为树架构就被称为“共享性节点的行为树”,它把原本的“节点”分成三个部分:

  • 节点(Node):和原来的节点概念有所不同,这个节点仅包含结构性数据,在所有的行为树实例中共享。并且负责创建该节点所能执行的任务。比如一个带选择功能的控制节点,就会产生一个带选择逻辑的任务。
  • 任务(Task):保存运行时特有的数据,并执行逻辑
  • 行为(Behaivor):保存由当前节点做创建的任务,并更新任务的状态

可能用文字叙述的话,大家对上面的概念还不是很好理解,我还是直接给大家看一下相关的代码

 1: class Node
 2: {
 3: public
 4:     ...
 5:     virtual Task* CreateTask() = 0;
 6:     virtual void DestroyTask(Task* pTask) = 0;
 7:     ...
 8: protected:
 9:     Node* m_pParent;
 10: };

可以看到在节点中,我们有两个方法,一个是CreateTask,一个是DestroyTask,前者用来创建一个任务,后者用来销毁一个任务,作为结构性数据的m_pParent也存在Node这个类中。有了这个Node类,我们就可以创建一个带子节点功能的Node类,称之为CompositeNode

 1: typedef std::vector<Node*> Nodes;
 2:
 3: class CompositeNode : public Node
 4: {
 5: public:
 6:     Node* GetChild(int idx);
 7:     void AddChild(Node* node);
 8:     int GetChildCount() const
 9: protected:
 10:     Nodes m_Children;
 11: };

子节点也是结构性数据的一部分,所以也作为成员变量存在CompositeNode的类中,这样有了这个类,我们就可以搭建出一棵树状的结构了,不过这些Node类都是基类,因为我们并没有实现创建任务和销毁任务的方法,这个稍后会提到,下面我们来看一下Task类

 1: class Task
 2: {
 3:     public:
 4:         Task(Node* pNode):m_pNode(pNode){}
 5:         virtual ~Task(){};
 6:         virtual void             OnInit(
 7:             const BevNodeInputParam& inputParam) = 0;
 8:         virtual BevRunningStatus OnUpdate(
 9:             const BevNodeInputParam& inputParam,
 10:             BevNodeOutputParam& outputParam) = 0;
 11:         virtual void             OnTerminate(
 12:             const BevNodeInputParam& inputParam) = 0;
 13:     protected:
 14:         Node* m_pNode;
 15: };

Task类是用来执行具体逻辑的,所以它包含了三个最基本的控制方法,一个是初始化(OnInit),一个是每帧的更新(OnUpdate),最后一个是销毁(OnTerminate),概念上也是很好理解的。

比较以前行为树的实现,和现在的实现就可以发现,以前的行为树中,我们一个Node类,即包含了结构性的数据,比如子节点,父节点等等,也包含了逻辑信息,运行时信息,现在,根据共享节点型行为树的概念,我们把它们一份为二了,将结构性信息都放到了新的Node类中,将逻辑和运行时信息都放到Task类中。那如何把这两个部分结合起来呢,这就要用到前面提到的新的“行为(Behavior)”类了

 1: class Behavior
 2: {
 3:     public:
 4:         ...
 5:         void Install(Node& node)
 6:         {
 7:             Uninstall();
 8:
 9:             m_pNode = &node;
 10:             m_pTask = m_pNode->CreateTask();
 11:         }
 12:         void Uninstall()
 13:         {
 14:             if(!m_pTask) return;
 15:             if(!m_pNode) return;
 16:             m_pNode->DestroyTask(m_pTask);
 17:             m_pTask = NULL;
 18:             m_eStatus = k_BRS_Invalid;
 19:         }
 20:         BevRunningStatus Update(
 21:                     const BevNodeInputParam& inputParam,
 22:                     BevNodeOutputParam& outputParam)
 23:         {
 24:             if(m_eStatus == k_BRS_Invalid)
 25:                 m_pTask->OnInit(inputParam);
 26:             BevRunningStatus ret = m_pTask->OnUpdate(inputParam, outputParam);
 27:             if(ret != k_BRS_Executing)
 28:             {
 29:                 m_pTask->OnTerminate(inputParam);
 30:                 m_eStatus = k_BRS_Invalid;
 31:             }
 32:             else
 33:             {
 34:                 m_eStatus = ret;
 35:             }
 36:             return ret;
 37:         }
 38:     private:
 39:         Node* m_pNode;
 40:         Task* m_pTask;
 41:         BevRunningStatus m_eStatus;
 42:     };

行为类中Install函数可谓是这种行为树架构的核心部分,他在用给定的节点创建了一个相应的任务任务实例,然后在后续的Update函数中更新这个任务的实例。Install函数的重要之处在于,节点是作为参数传入的!!这样我们就可以用同一个节点指针(相当于行为树结构的根节点指针),创建多个的任务实例,而且该节点上绑定的是何种任务,完全是由节点所决定的!这样就保证了,同一个行为树结构产生的一定是同一种行为树的逻辑。是不是很赞?:)

以上就是共享节点型行为树的一个概念,至于选择,序列,并行等节点如何实现,我会在下一篇文章里说明。下面的连接是同一个示例程序(单击鼠标可以添加10个智能体),但是是用两种行为树来实现,看看当加到200个智能体的时候,所占用内存的区别(因为原本的行为树实现方式中,有一些不必要的数据,所以并不是十分精确,但可以说明一定的问题)。具体代码,我会在下一篇文章中给出下载链接。

示例程序下载

参考文档:

1. 《Understanding the Second-Generation of Behavior Trees》- aigamedev.com

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————

(已被阅读11,904次)

3 评论

  1. 创建好的行为树对象不和智能体绑定,doAction的时候,将智能体作为参数传入,执行各自的行为,这样就可以了吧。感觉博主这个好复杂。

    1. 这个是我在aigamedev里看到的一种实现,后来我自己又做了一种实现,c#的Unity版本,把行为树的数据作为context存在参数里了

  2. 在uninstall方法里面,是直接把上一个任务给删除掉, 我想知道共享节点永远是互斥的么? 有没有共存的情况存在? 我觉得删除节点应该是这两种情况: 1.运行完成,节点返回finish,删除掉。2.install这个节点的对象明确调用停止当前节点的方法,删除掉,其余情况应该是放入到一个数据结构保存,并且并行执行所有装载进来的执行节点?

发表评论

邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

Copyright © 2011-2020 AI分享站    登录