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


上次聊到了共享节点型行为树的基本概念和节点组成,简单的来说,这种行为树就是把构成树的结构性数据,和运行时数据分开了,将结构性数据在多个行为树中共享,这样当存在大量的智能体的时候,内存的使用会减少很多。

在上次的文章里,提到了行为(Behavior),节点(Node),任务(Task)的概念和实现方法,这里再回顾一下这三个概念的内容

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

有了这样的基本节点,我们就可以将其他的控制节点来实现出来,我还是会实现三种基本的控制节点:选择,序列和并行,由于这三种节点都不是叶节点,并且在Node的实现中,可以看到,我们并没有保留用于保存子节点的成员变量,所以,我们首先需要创建一个带子节点的Node类,称之为组合节点(Composite Node)

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

这个类很简单,就提供了一个子节点的成员变量,并包含一些访问接口,下面我们来看看如何来做一个选择节点,我们需要做两个部分,一个是选择节点(Select Node),一个是选择任务(Select Task),选择节点用来生成选择任务,这个是共享节点型行为树的基本概念,后面就不再赘述了。

 1: class SelectorTask : public Task
 2: {
 3: public:
 4:     SelectorTask(Node* pNode)
 5:         : Task(pNode)
 6:         , m_LastBehavior(-1)
 7:     {}
 8:     CompositeNode& GetCompositeNode(){
 9:         return *dynamic_cast<CompositeNode*>(m_pNode);
 10:     }
 11:     virtual void OnInit(const BevNodeInputParam& inputParam)
 12:     {}
 13:     virtual BevRunningStatus OnUpdate(const BevNodeInputParam& inputParam, BevNodeOutputParam& outputParam)
 14:     {
 15:         CompositeNode& comNode = GetCompositeNode();
 16:         if(comNode.GetChildCount() == 0)
 17:             return k_BRS_Failure;
 18:
 19:         if(!m_CurrentBehavior.HasInstalled())
 20:         {
 21:             m_LastBehavior = 0;
 22:             m_CurrentBehavior.Install(*(comNode.GetChild(m_LastBehavior)));
 23:         }
 24:         BevRunningStatus status = m_CurrentBehavior.Update(inputParam, outputParam);
 25:         if(status != k_BRS_Failure)
 26:         {
 27:             return status;
 28:         }
 29:         for(int i = 0; i < comNode.GetChildCount(); ++i)
 30:         {
 31:             if(m_LastBehavior == i)
 32:                 continue;
 33:
 34:             m_CurrentBehavior.Install(*(comNode.GetChild(i)));
 35:             BevRunningStatus status = m_CurrentBehavior.Update(inputParam, outputParam);
 36:             if(status != k_BRS_Failure)
 37:             {
 38:                 m_LastBehavior = i;
 39:                 return status;
 40:             }
 41:         }
 42:         return k_BRS_Failure;;
 43:     }
 44:     virtual void OnTerminate(const BevNodeInputParam& inputParam)
 45:     {
 46:         m_LastBehavior = -1;
 47:         m_CurrentBehavior.Uninstall();
 48:     };
 49:
 50: private:
 51:     int             m_LastBehavior;
 52:     Behavior m_CurrentBehavior;
 53: };
 54: class CompositeNode_Selector : public CompositeNode
 55: {
 56: public:
 57:     virtual Task* CreateTask()
 58:     {
 59:         return new SelectorTask(this);
 60:     }
 61:     virtual void DestroyTask(Task* pTask)
 62:     {
 63:         SelectorTask* pTest = dynamic_cast<SelectorTask*>(pTask);
 64:         D_CHECK(pTest);
 65:         D_SafeDelete(pTest);
 66:     }
 67: };

从上面的代码中,可以看到,这是一个带优先级的选择节点,因为我们每次都是从第一个子节点开始寻找可运行的节点,当找到后,就中断寻找的过程,和上一个版本不同的是,在现在这个版本的实现中,我们并没有引入“前提”的概念,而是通过在Update的返回值来确定当前节点是否可以运行,这样的话,我们需要在行为节点的Update中实现对于“前提”的检查,并返回正确的返回值。

序列和并行的实现方法大同小异,只要了解了这些节点的基本概念,就可以很容易的写出相关的代码。大家可以在文章的最后找到下载链接,我也强烈建议大家自己实现一下,以加深理解。

另外,我们在实现每一个Task类的时候,都需要一个相应的Node类来生成这个Task,而这些Node类的结构基本是一样的,所以我为此专门定义了一个宏来生成这个Node类(不过使用起来,还不是很方便,有改进的余地)

 1: #define DEF_TERMINATE_NODE(name, task) \
 2:     class Node##_##name : public Node {\
 3:     public:\
 4:         virtual Task* CreateTask(){\
 5:             return new task(this);\
 6:         }\
 7:         virtual void DestroyTask(Task* pTask){ \
 8:             task* pTest = dynamic_cast<task*>(pTask);\
 9:             D_CHECK(pTest);\
 10:             D_SafeDelete(pTest);    \
 11:         };\
 12:     };
 13:
 14: #define CREATE_TERMINATE_NODE(name) new Node##_##name()

最后我用新的行为树重写了那些行为节点(参考这里),实现了同样的功能。从内存的使用量来看,共享节点型行为树,大大减少了内存的消耗,比较适合存在大量的智能体的情况。和原来的行为树实现有一点不同的是,并且特别要注意的是,共享节点型的行为树,在运行过程中,会不断的创建和销毁执行完毕的Task(对于不可运行的Task,它也会先创建,然后Update一下,再销毁),所以就会存在大量的小内存的分配和释放,如果没有很好的内存管理机制,这样的分配和释放,会造成大量的内存碎片,在我的程序中,我并没有做内存的部分,但如果要使用这样的行为树模式在实际的开发中的话,必须要在内存的管理上做一些额外的工作,比如使用内存池,小内存表等等。

下载地址:

GoogleCode下载点(exe文件夹中已包含可执行文件)

也可用svn通过以下地址来得:

http://tsiu.googlecode.com/svn/branches/blogver/

编译方法:

用VS2005以上打开,选择Debug NoDx或者Release NoDx,将BTTInit2.cpp以及TAI_BevTree.cpp加入编译(方法:在此文件上右键-属性-在编译中排出,选否),将BTTInit3.cpp移除编译(方法:在此文件上右键-属性-在编译中排出,选是),这样编译后就会得到由原本的行为树版本所作的示例程序,反之,编译后就是用新的行为树版本做的示例程序

相关代码:

TAI_BevTree2.h

关于TsiU

TsiU是我一直在维护的一个自己用的小型的框架,我平时做的一些AI的sample,或者一些工具,都会基于这个框架,TsiU有一些基本的UI控件库,网络模块库,GDI绘图模块,D3D绘图模块等等,可以快速的做成一个小型的示例程序,很方便(具体可参考SampleApps里的例子程序),并且整个架构是用Object的方式来组织,非常容易理解和扩展。整个框架很轻量化,基本就是做了一些底层的基本的功能,这样我在平时做东西的时候,就不需要重新写底层了,把精力都放在高层的实现了。以后分享代码都会基于这个框架,大家也可以通过svn来随时update到我最新的改动。下图就是TsiU里的几个工程介绍,代码不多,大家想看的也可以自己看一下:)

参考文档:

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

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



(已被阅读2,960次)

2 评论

发表评论

电子邮件地址不会被公开。

Copyright © 2011-2017 AI分享站    登录