— AI分享站

用800行代码做个行为树(Behavior Tree)的库(2)

第一部分

上一次说到了节点的基类,它描述了在行为树上一个节点的基本结构。我们知道,在行为树上有两大类的节点,一种我称之为“控制节点”,像“选择节点”,“并行节点”,“序列节点”都属于此类,这类节点负责行为树逻辑的控制,是和具体的游戏逻辑无关的,属于行为树库的一部分,并且这类节点一般不会作为叶节点。还有一类称为“行为节点”,也就是行为树上挂载的具体行为,是和游戏逻辑相关的,不属于行为树库的一部分,需要自己去继承和实现,这类节点一般都作为叶节点出现。

先来看看“行为节点”的代码,我先从节点的基类继承了一个所有“行为节点”的基类

 1: class BevNodeTerminal : public BevNode
 2: {}

在它的Tick方法中,我做了一个简单的状态机(可以自行看代码),负责处理进入行为(Enter),更新行为(Execute),退出行为(Exit),所有的行为节点应该继承自BevNodeTerminal类,并且重写这些虚函数,在进入和退出行为里,可以做一个初始化和清理的工作:

 1: class BevNodeTerminal : public BevNode
 2: {
 3: protected:
 4:     virtual void                _DoEnter(const BevNodeInputParam& input)                                {}
 5:     virtual BevRunningStatus    _DoExecute(const BevNodeInputParam& input, BevNodeOutputParam& output)  { return k_BRS_Finish;}
 6:     virtual void                _DoExit(const BevNodeInputParam& input, BevRunningStatus _ui_ExitID)    {}
 7: }

值得注意的是,在Tick方法中,它有一个返回值,表示当前节点是否处理完毕,在库中,我定义了一个enum来表示节点的运行状态:

 1: enum BevRunningStatus
 2: {
 3:     k_BRS_Executing                 = 0,
 4:     k_BRS_Finish                    = 1,
 5:     ...
 6: };

当返回k_BRS_Finish的时候,就表示当前节点已经处理完毕了,如果再次进入该节点,就认为是重新进入了。用上面描述的那个状态机的来说的话就是,如果是重新进入,会先调用_DoEnter方法,然后调用_DoExecute方法,如果_DoExecute返回正在运行(k_BRS_Executing),那么以后再进入这个节点就会直接调用_DoExectue,如果返回已经结束(k_BRS_Finish),则会调用_DoExit,以后再进入这个节点就会重新调用_DoEnter方法了。

对于控制节点来说,它的运行状态和子节点的运行状态是息息相关的,比如,选择节点的运行状态,就是它当前选择的这个节点的运行状态,并且,有时控制节点的控制逻辑也和子节点的运行状态有关,比如序列节点,当它前一个子节点运行结束,序列节点就会自动的切换到下一个子节点运行。所以在实现具体的行为类时,我们应该要正确的返回节点的运行状态。在例子程序中,我做的一个“空闲”(idle)的行为节点,就能很好的说明问题:

 1: class NOD_Idle : public BevNodeTerminal
 2: {
 3: public:
 4:     NOD_Idle(BevNode* _o_ParentNode)
 5:         :BevNodeTerminal(_o_ParentNode)
 6:     {}
 7: protected:
 8:     virtual void _DoEnter(const BevNodeInputParam& input)
 9:     {
 10:         m_WaitingTime = 0.5f;
 11:     }
 12:     virtual BevRunningStatus _DoExecute(const BevNodeInputParam& input, BevNodeOutputParam& output)
 13:     {
 14:         const BevInputData& inputData = input.GetRealDataType<BevInputData>();
 15:         BevOutputData& outputData = output.GetRealDataType<BevOutputData>();
 16:
 17:         f32 timeStep = inputData.m_TimeStep;
 18:         m_WaitingTime -= timeStep;
 19:         if(m_WaitingTime < 0)
 20:         {
 21:             outputData.m_BodyColor = D_Color(rand() % 256, rand() % 256, rand() % 256);
 22:             return k_BRS_Finish;
 23:         }
 24:         return k_BRS_Executing;
 25:     }
 26: private:
 27:     float m_WaitingTime;
 28: };

这段代码中的某些内容不明白也没有关系,我们主要关注的是关于节点运行状态的部分。这个Idle行为做了一件这样的事,就是不停的变换自己的颜色,间隔是0.5秒,当时间一到,就会返回运行结束(k_BRS_Finish),并输出当前的颜色,当时间还没到,则返回运行中(k_BRS_Executing),并且维持当前颜色。可以看到,我们用运行状态控制了计时器的重置,选择在_DoEnter方法中重置了计时器,当然,更合理的做法是在时间一到的时候,就重置计时器,并且永远返回运行中,不过这个例子里,我主要就是想用来演示运行状态,和_DoEnter的相关用法。

接下去再来看看控制节点,我一共写了5种控制节点,带优先级的选择节点(BevNodePrioritySelector),不带优先级的选择节点(BevNodeNonePrioritySelector),序列节点(BevNodeSequence),并行节点(BevNodeParallel),循环节点(BevNodeLoop),这些节点的进入条件和选择逻辑都是按照在行为树中改节点的定义来做的,我想用一张表格来说明:

测试(Evaluate) 更新(Tick)
带优先级的选择节点(BevNodePrioritySelector) 从第一个子节点开始依次遍历所有的子节点,调用其Evaluate方法,当发现存在可以运行的子节点时,记录子节点索引,停止遍历,返回True。 调用可以运行的子节点的Tick方法,用它所返回的运行状态作为自身的运行状态返回
不带优先级的选择节点(BevNodeNonePrioritySelector) 先调用上一个运行的子节点(若存在)的Evaluate方法,如果可以运行,则继续运保存该节点的索引,返回True,如果不能运行,则重新选择(同带优先级的选择节点的选择方式) 调用可以运行的子节点的Tick方法,用它所返回的运行状态作为自身的运行状态返回
序列节点(BevNodeSequence) 若是从头开始的,则调用第一个子节点的Evaluate方法,将其返回值作为自身的返回值返回。否则,调用当前运行节点的Evaluate方法,将其返回值作为自身的返回值返回。 调用可以运行的子节点的Tick方法,若返回运行结束,则将下一个子节点作为当前运行节点,若当前已是最后一个子节点,表示该序列已经运行结束,则自身返回运行结束。若子节点返回运行中,则用它所返回的运行状态作为自身的运行状态返回
并行节点(BevNodeParallel) 依次调用所有的子节点的Evaluate方法,若所有的子节点都返回True,则自身也返回True,否则,返回False 调用所有子节点的Tick方法,若并行节点是“或者”的关系,则只要有一个子节点返回运行结束,那自身就返回运行结束。若并行节点是“并且”的关系,则只有所有的子节点返回结束,自身才返回运行结束
循环节点(BevNodeLoop) 预设的循环次数到了就返回False,否则,只调用第一个子节点的Evaluate方法,用它所返回的值作为自身的值返回 只调用第一个节点的Tick方法,若返回运行结束,则看是否需要重复运行,若循环次数没到,则自身返回运行中,若循环次数已到,则返回运行结束

可能看表格内的描述会感觉有点拗口,可以结合代码一起看,会理解的更好。特别要提一点的是,在某些控制节点的Evaluate方法中,我会修改和记录可以运行的节点索引,当调用Tick的时候,就可以用这个索引来找到可以运行的节点了。这种模式和我以前提到的行为树更新模式有点不太一样,不过本质上是相同的。

(待续…)

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

0 comments
Submit comment