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

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

 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),这些节点的进入条件和选择逻辑都是按照在行为树中改节点的定义来做的,我想用一张表格来说明:

wKioL1aPa1-x-5m1AADTkNlR5ys303.png

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

(待续…)