上次聊到了共享节点型行为树的基本概念和节点组成,简单的来说,这种行为树就是把构成树的结构性数据,和运行时数据分开了,将结构性数据在多个行为树中共享,这样当存在大量的智能体的时候,内存的使用会减少很多。
在上次的文章里,提到了行为(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
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————
有得必有失,频繁创建销毁对象这必定会给CPU带来更多压力
所以需要有内存池的管理