C++ 版本的 行为树的简单实现

如果你想转载这篇文章呢,请严格按照以下格式注明出处和作者
出处:http://www.cnblogs.com/anxin1225/p/4827294.html
作者:Anxin1225、Bianchx、Linker(其实他们都是一个人。。)
 
     行为树是一种简洁明了的整理业务逻辑的有效方法。至于他的好处,不做赘述。
     由于项目的需要,所以实现了一个非常简单的行为树,来应对我们的需求。之所以说简单,是因为我并没有实现很多控制节点,而只是实现了最基础的业务的三个节点而已。至于其他的你觉得有用的控制节点,可以自己修改出来。
     简单说说我实现的节点:基础节点、单条节点、列表节点、选择节点、顺序节点、取反节点。这几个节点分为相对较为基础的节点,和业务节点。基础的节点包括:基础节点、单条节点、列表节点。基础的节点的主要作用是定义,定义最基础的调用方法和关于子节点应该怎么样保存。业务节点包含选择节点、顺序节点和取反节点。他们的继承关系如下:基础节点是最基础的节点;单条节点和列表节点继承自基础节点;选择节点和顺序节点继承自列表节点;取反节点继承自单条节点。
 
     来简单说一下各个节点的作用
     基础节点:
          1、invoke函数,被调用时,返回true或者false
          2、destroy函数,节点被释放时会递归式的释放所有依附于此节点的子节点和曾子节点
          3、设置和获取 Describe 的函数,用于打印AITree时的结构描述
          4、设置和获取 Name 的函数,用于打印AITree时的名称描述和调用时,递归描述的打印
     列表节点:
          1、包含一个有序的子集列表,可以添加和获取子集列表的引用
     单条节点:
          1、包含一个子集节点,可以设置和获取子集节点
     选择节点:
          1、被调用时,如果没有子集节点则会直接返回false
          2、调用时,会依次从前往后执行,任何一个子集节点返回了true,则终止循环,直接返回true
          3、当所有的子集节点都没有返回true时,则会返回false
     顺序节点:
          1、被调用时,如果没有子集节点则会直接返回false
          2、调用时,会依次从前往后执行,任何一个子集节点返回了false,则终止循环,直接返回false
          3、 当所有的子集节点都没有返回false时,则会返回true
     取反节点:
          1、被调用时,如果没有子集节点则直接返回false
          2、存在子集节点时,则会调用子集节点,并且将结果取反并返回
 
实现了这些节点之后就可以实现以下图示的大部分功能(手比较残,又加上身边没有工具,所以用文字的表示吧)
C++ 版本的 行为树的简单实现_第1张图片
先简单解释一下这个图什么意思,第一个是节点的名字,注入的时候写的,可以是中文也可以是英文,这个无所谓,毕竟只有这个地方在用。第二个参数是当前实例的描述,如果是用来帮助你理解这个树的
 
再简单解释一下,这段逻辑是什么意思。这个是一个宠物的逻辑,如果附近有金币呢,他就取捡金币;如果没有金币呢并且很长时间没有捡到金币并且很长时间没有回到主人身边了,那就回到主人身边,否则就随便走走。
 
其实这个逻辑真的挺简单的,如果,是按照普通的方式来写的话。就会在各种状态之间判断条件然后各种跳转执行。这样能实现,不过后期的维护可能更加费劲一些,如果使用配置行为树则相对简单一些,在修改的时候只需要添加新的分支或者减掉原来的分支就可以了。逻辑也相对更加清晰。
 
然后简单说明一下,怎么在我的这个小玩意里边扩展自己的东西。
1、在AITreeNodeType添加一个新的枚举,主要是用来确定Id用的,注入的时候用的(至于什么是注入一会再说)
2、然后继承比较基本的节点,一般情况下继承最基础的三个就好,最常用的就是AINodeBase,那我们就那AINodeBase来举例
3、然后实现virtual bool invoke(int level = 0, bool isLog = false);方法,level代表从根节点开始这是第几层调用,一般用作Log的时候前边有几个空格,isLog代表是否打印Log,你完全可以忽视这两个参数不管,当然你要实现对应的功能最好遵守这两个参数的约定,当然不遵守我也没有意见。
4、在类中添加一个私有的static AINodeRegister<类名> reg;然后在Cpp文件中编写AINodeRegister<类名> AINodeReleaseSkill::reg(NodeId, NodeName);来实现注入,第一个参数是之前你获得的Id,第二个参数是对应的节点名,可以不是类名,不过我推荐你还是用类名,只有查找的时候好找
 
可能放上一段代码更直观一些
//回到主人身边
class AINodeGotoOwnerSide : public AINodeBase
{
private:
    static AINodeRegister<AINodeGotoOwnerSide> reg;

public:
    virtual bool invoke(int level = 0, bool isLog = false);
};
AINodeRegister<AINodeGotoOwnerSide> AINodeGotoOwnerSide::reg(ANT_GOTO_OWNER_SIDE, "AINodeGotoOwnerSide");
bool AINodeGotoOwnerSide::invoke(int level, bool isLog)
{
    return rand() % 100 > 20;
}
 
说完了累的扩展,应该简单说一下什么是注入了,简单点说,就是我写了一个公开的帮助函数,用来接受Id跟一个创建节点的函数指针,然后把它们保存在的字典中,你需要调用的时候,我就从字典里边找找当初注入的函数指针,然后调用它,给你一个实例。至于为什么要写一个静态的AINodeRegister泛型类,是因为静态的初始化实在程序启动的时候会初始化,应用这个特性,我们就可以在初始化的时候把,想要初始化的内容注入到内存中。
 
其实说到这个地方,主要的逻辑已经基本上说的差不多了。还有一些其他的方面,比如说树的组装如何处理,如果是挨个编写他们之间的引用应该也会很麻烦。并且,使用这种结构处理业务逻辑的时候,业务内容就会分的乱七八糟什么地方都有,调试也可能会成为问题。
 
实现Id跟类型之间的关联之后就可以通过描述类型来创建类了,最后的实现如下
 
 AINodeDescribe des[] = {
        AINodeDescribe(1, 0, ANBT_SELECT, "根节点"),
 
        AINodeDescribe(2, 1, ANBT_SEQUENCE, "是否拾取金币的判定节点"),
        AINodeDescribe(5, 2, ANT_RELEASE_SKILL, "附近是否存在金币"),
        AINodeDescribe(6, 2, ANT_PICKING_UP_COINS, "捡取金币节点"),
 
        AINodeDescribe(3, 1, ANBT_SEQUENCE, "是否回到主人身边的判定节点"),
        AINodeDescribe(7, 3, ANT_RELEASE_SKILL, "是不是很长时间没有见到金币了"),
        AINodeDescribe(8, 3, ANT_PICKING_UP_COINS, "是不是很长时间没有回到主人身边了"),
        AINodeDescribe(9, 3, ANT_PICKING_UP_COINS, "回到主人身边的执行节点"),
 
        AINodeDescribe(4, 1, ANT_PICKING_UP_COINS, "没事随便逛逛吧"),
    };
 
    int desCount = sizeof(des) / sizeof(AINodeDescribe);
 
    vector<AINodeDescribe> des_vtr;
    for (int i = 0; i < desCount; ++i)
    {
        des_vtr.push_back(des[i]);
    }
 
    AINodeBase * rootNode = AINodeHelper::sharedHelper()->CreateNodeTree(des_vtr); 
AINodeDescribe初始化的时候接受四个参数:当前Id,父节点Id,当前节点创建的树节点具体类型,当前节点实例的描述。其中父节点如果是0的时候则会被当做根节点返回,这个一点要有一个哦,不然会直接返回NULL,并且申请的所有节点都会造成内存泄露。
 
起始这个地方可以吧参数都写到文件中,然后通过文件来进行初始化,不过,我这个地方只是为了演示用,所以直接写死也没有关系,不过你在用的时候,我推荐你写一个读取文件配置的方法,效果会更好。(因为你可以吧这段的逻辑整理直接做一个编辑器,让策划来进行对应的内容的整理。)
 
对了,这个地方,你可能是按照自己的想法来描写的这个文件,但是实际的执行结果可能跟你的想法并不一样,你可以进行如下处理来进行验证
    cout << "\n状态结构组织图 \n" << endl;
    AINodeHelper::sharedHelper()->printAITree(rootNode);
 
    cout << "\n状态结构组织图 \n" << endl;
 
输出的结果呢,就是最上边那张图了
 
 
剩下的还存在一个问题,那就是调试问题了,我不可能在这么多内容中下断点,那跟下毒没啥区别。所以我们需要有一种方式来打印各个节点的运行结果。这个我的处理如下
    for (int i = 0; i < 10; ++i)
    {
        cout << "调用树开始" << endl;
 
        rootNode->invoke(0, true);
 
        cout << "调用树结束" << endl;
    }
其中invoke的第一个参数的意思为最基础的节点的届位,第二个参数为是否打印Log,如果不想调试的话,两个参数都不要填就可以。
 
 
最后说一下项目的地址
http://git.oschina.net/anxin1225/AiTreeTest
 
可能这个描述还不是很明确,你可以给我留言,我尽量给你解答 

你可能感兴趣的:(C++ 版本的 行为树的简单实现)