行为树最后一个要讲的地方,是关于前提(Precondition),在第一部分里,我略微提到了一下,这次我们来仔细看看,再来看看关于前提的纯虚基类的定义:
1: class BevNodePrecondition
2: {
3: public:
4: virtual bool ExternalCondition(const BevNodeInputParam& input) const = 0;
5: };
每一个前提类,都需要实现这个判断的虚函数。我在《用类来表示逻辑运算–关于行为树前提的一种实现方式》提到,我们可以用类来表示逻辑运算,这样的好处是可以做到模块化,同样的判断条件可以复用,所以在库中,我也实现了这种逻辑的表达方式,定义了基本的逻辑运算类
1: class BevNodePreconditionTRUE{};
2: class BevNodePreconditionFALSE{};
3: class BevNodePreconditionNOT{};
4: class BevNodePreconditionAND{};
5: class BevNodePreconditionOR{};
6: class BevNodePreconditionXOR{};
从这些类的名字应该就可以明显的看出这些类的含义了,和逻辑操作符一样,有些类的构造函数需要两个参数,以此来表示二元的逻辑运算(AND,OR,XOR),有些只需要一个参数,以此来表示一元的逻辑运算(NOT)。前提类被用来附在行为树的节点上(每一个节点都可以附加),默认情况下,节点上是没有前提类的,也就是不存在“外在前提”,而只有“内在前提”,这和附了一个BevNodePreconditionTRUE(永远返回True)的“外在前提”的节点是等价的。
好了,行为树库的内容基本就是这些了。接下去我们来看看例子程序,介绍如何用库来创建行为树,例子的代码在BevTreeTest这个工程中,编译后可直接运行,这个例子分别演示了三个行为树,从简单到复杂,单击鼠标可以在这三个例子间切换。这个程序实现了这样一个功能,“在场景地图上,定时会产生一个目标点,智能体就会根据行为树的定义,用不同的行为模式移动到目标点”。
在这个程序中,我为智能体一共定义了4个行为:
1: class NOD_Idle{}; //空闲,表现是颜色不停变化
2: class NOD_Breathe{}; //呼吸,表现是大小规律性变化
3: class NOD_MoveTo{}; //移动,平移到某目标点
4: class NOD_FaceTo{}; //转向,转向到某方向
再定义了2个“外在前提”:
1: class CON_HasReachedTarget{}; //是否到达目标点
2: class CON_HasFacedToTarget{}; //是否朝向目标点
我就用第一个例子来说,第一例子的行为树图如下:
这是一个很简单的行为树,根节点是一个带优先级的选择节点,所以MoveTo比Idle的优先级高,MoveTo带有一个“外在前提”,“当没有到达目标点”时,会选在MoveTo的行为,反之,则选Idle的行为。
在代码中,可以这样来定义这棵行为树
1: BevNode& ret =
2: BevNodeFactory::oCreatePrioritySelectorNode(NULL, "root");
3: BevNodeFactory::oCreateTeminalNode<NOD_MoveTo>(&ret, "move to")
4: .SetNodePrecondition(new BevNodePreconditionNOT(new CON_HasReachedTarget()));
5: BevNodeFactory::oCreateTeminalNode<NOD_Idle>(&ret, "idle")
6: .SetNodePrecondition(new BevNodePreconditionTRUE());
7: m_BevTreeRoot = &ret;
我在库中定义了一些工厂方法,帮助创建相关的节点。值得注意的是,我在这里演示了用类表示逻辑的用法。我在定义行为树的时候,会用一些格式上的缩进,来表示相应的父子结构,这仅仅是为了视觉上比较明了。当然,以后可以改进行为树的定义接口,更可以用数据文件来定义行为树。
这样定义完毕后,我们就可以用行为树来决策我们的行为了,代码相当简单
1: BevNodeInputParam input(&m_BevTreeInputData);
2: BevNodeOutputParam output(&m_BevTreeOutputdata);
3: if(m_BevTreeRoot->Evaluate(input))
4: {
5: m_BevTreeRoot->Tick(input, output);
6: }
在例子中,我尽量把行为树中要输出的变量写到BevNodeOutputParam结构中(而不是直接修改智能体的信息),这样做的好处是可以让行为树的输入和输出的接口相当清晰,做成黑盒,可以参考我在这里的讨论。
第二个例子演示了并行节点的用法,第三个例子演示了序列节点的用法,就不多说了,大家可以自行看代码。
所有的代码可以通过以下方式获得:
下载地址:
GoogleCode下载点(exe文件夹中已包含可执行文件)
也可用svn通过以下地址来得:
http://tsiu.googlecode.com/svn/branches/blogver/
编译方法:
用VS2005以上打开,选择Debug NoDx或者Release NoDx,编译后,运行BevTreeTest.
相关代码:
TAI_BevTree.h
TAI_BevTree.cpp
关于TsiU
TsiU是我一直在维护的一个自己用的小型的框架,我平时做的一些AI的sample,或者一些工具,都会基于这个框架,TsiU有一些基本的UI控件库,网络模块库,GDI绘图模块,D3D绘图模块等等,可以快速的做成一个小型的示例程序,很方便(具体可参考SampleApps里的例子程序),并且整个架构是用Object的方式来组织,非常容易理解和扩展。整个框架很轻量化,基本就是做了一些底层的基本的功能,这样我在平时做东西的时候,就不需要重新写底层了,把精力都放在高层的实现了。以后分享代码都会基于这个框架,大家也可以通过svn来随时update到我最新的改动。下图就是TsiU里的几个工程介绍,代码不多,大家想看的也可以自己看一下:)
————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————
之前看你的文章做了一版行为树AI,最近发现的一个BUG:带优先级时,高优先级打段低优先级,低优先级会在高优先级执行后才被打断。比如:A:攻击 B:行走,在行走过程中被攻击打断,正常动作应该是:先打断B->再执行A。我在每个节点Update中处理一个回调,如:B执行前先回调打断正在执行的行为节点,就又出现了地狱式回调。
您好 代码可以发我邮箱一份吗,我的邮箱2091036226@qq.com,非常感谢
博主,你文里的连接都挂了,侧边栏的连接也挂了,代码可以发我邮箱一份吗,我的邮箱hzqt210@126.com,非常感谢
楼主,您好,最近一直在研究AI方面的东西,想参考下您的AI库。可以邮件发一下么.wt414@yahoo.com万分感谢!
侧边栏有链接,可以直接下载
博主 你好。有几个问题想问一下您
1)我看别的行为树,条件节点是作为行为树的一部分,而不是放到行为节点的内部。博客中的例子是放到了一起,这样 的考虑是基于什么原因
2)对于行为树的输入的疑问。不知道我理解的对不对。对于外界的事件是当做行为树的输入来处理吗。比如说一个怪物正在执行某个ai行为。因为其他的外在环境的变化倒置改变了他的ai。这样的事情也是通过设置这个输入参数吗。还是事件这种情况,博主的例子中没有考虑进去
1. 作为行为树的一部分的话,会导致有很多的sequence节点,看起来有点累,这个是一个原因,还有一个原因是,我这里的行为树是代码导向的,也就是用代码来构建行为树,并没有考虑很多可视化编辑的方面,如果是可视化编辑,可能作为行为树的一部分会更好一点
2. 输入这里,我只是演示了一种黑盒的方式,但是实际应用的话,不需要这么死板,比如你有一个singleton的数据类,那直接在行为树里访问就可以了,如果你外在环境变化导致一些数据变化,那就直接在行为树中想办法拿到就可以
你好 能把例子程序发我一份吗 911712193@qq.com
博主好 这个地址这边连不上 能否发到 445615105@qq.com 谢谢
侧边栏有链接,可以直接下载
楼主你好
最近我也想用行为树代替我冗余的任务类
本想借鉴你的库
奈何网址实在打不开
如若有空望发至邮箱【719549260@qq.com】
谢谢!Thanks♪(・ω・)ノ
侧边栏有链接,可以直接下载
楼主,您好!
看了你的文章受益匪浅。想下载你写的BT库学习一下,无奈你给的链接我下载不了(可能是我的网络问题),能不能麻烦你把库代码发到我的邮箱:626059472@qq.com。谢谢!
侧边栏有链接,可以直接下载
好文章,内容笔下生辉.禁止此消息:nolinkok@163.com