400行代码实现行为树(基于cocos2dx框架下)

来源:互联网 发布:mysql开启binlog日志 编辑:程序博客网 时间:2024/06/05 12:04

在做一些游戏AI时,比如游戏里面的角色、npc、怪物等一些预设的AI逻辑,最简单的时候用if...else...,但是当游戏逻辑有点复杂时就显得有点力不从心,单单看这一大堆的if...else都恶心到吐。目前比较流行的ai模型有状态机和行为树(Behavior tree).

状态机的实现我这里就不多加讨论了

当游戏中的角色,npc,怪物等的决策不太复杂时状态机很有效,然而随着决策的复杂,状态机的缺点也慢慢的体现出来了

罗列状态机比较突出的几个缺点:

1、每一个状态的逻辑会随着新的状态的增加而越来越复杂。

2、状态机状态的复用性很差,一旦一些因素变化导致环境发生变化,你只能新增一个状态,并给这个新状态添加连接及其跳转逻辑。

3、没办法并行处理多个状态。

行为树

1、高度模块化状态,去掉状态中的逻辑跳转,使得状态编程一个"行为"。

2、行为和行为之间的跳转是通过父节点的类型来决定的。并且可以通过并行节点来并行处理多个状态。

3、通过增加控制节点的类型,可以达到复用行为的目的。


关于行为树网上有不少相关文章,大部分都是理论方面的东西,对于行为树的实现,不少朋友不知从何下手。最近相对空闲之余,写了一个简单的行为树库。

如果没有这方面基础的同学请先网上找下这方面的资料,先了解下行为树的一些基本的知识点。

行为树的一些基本的控制节点。我们先实现几个最基本的控制节点。可以根据项目的需要再加一些其他控制节点。

1、选择节点

从头到尾按顺序选择执行条件为真的节点

2、带记忆的选择节点

从上一次执行的子节点开始,按顺序选择执行条件为true的节点

3、序列节点

从头到尾按顺序执行每个子节点,遇到false为止

4、带记忆的序列节点

从上一次执行的子节点开始,按顺序执行每个子节点,遇到false为止


实现部分:

定义一个节点的基类:

#ifndef __BevNode_H__#define __BevNode_H__#include <vector>//#include "BevComm.h"using namespace std;namespace BT{enum eBevState{E_BevState_Success,//成功E_BevState_Fail,//失败E_BevState_Running,//该节点正在运行};class BevNode{public:BevNode(): m_pParent(nullptr){}~BevNode(){for (auto pNode : m_VecChildren){delete pNode;pNode = nullptr;}m_VecChildren.clear();}void addChild(BevNode* pBevNode);void setParent(BevNode* pParent){ m_pParent = pParent; }BevNode* getParent(){ return m_pParent; }virtual eBevState execute(float fDelta){return E_BevState_Fail;}protected:BevNode* m_pParent;vector<BevNode*> m_VecChildren;};}#endif
选择节点
#include "BevComm.h"</span>
namespace BT{class Selector : public BevNode{public:Selector(){}virtual ~Selector(){}virtual eBevState execute(float fDelta);};}
#include "Selector.h"using namespace BT;eBevState Selector::execute(float fDelta){eBevState result = eBevState::E_BevState_Fail;for (auto pNode : m_VecChildren){eBevState status = pNode->execute(fDelta);if (status != eBevState::E_BevState_Fail){result = status;break;}}return result;}

带记忆的选择节点


#include "memorySelector.h"using namespace BT;eBevState memorySelector::execute(float fDelta){for (int i = m_nLastNode; i < m_VecChildren.size(); ++i){BevNode* pNode = m_VecChildren[i];eBevState status = pNode->execute(fDelta);if (status != eBevState::E_BevState_Fail){if (status == eBevState::E_BevState_Running){m_nLastNode = i;return status;}}}return eBevState::E_BevState_Fail;}

序列节点
#include "SequenceNode.h"using namespace BT;eBevState SequenceNode::execute(float fDelta){for (auto pNode : m_VecChildren){eBevState status = pNode->execute(fDelta);if (status != eBevState::E_BevState_Success){return status;}}return eBevState::E_BevState_Success;}

带记忆的序列节点
#include "memorySequence.h"using namespace BT;eBevState memorySequence::execute(float fDelta){for (int i = m_nLastIndex; i < m_VecChildren.size(); ++i){BevNode* pNode = m_VecChildren[i];eBevState status = pNode->execute(fDelta);if (status != eBevState::E_BevState_Success){if (status == eBevState::E_BevState_Running){m_nLastIndex = i;return status;}}}m_nLastIndex = 0;return eBevState::E_BevState_Fail;}


叶子节点(LeafNode)

叶子节点也就是真正跟我们逻辑相关的节点了。

首先叶子节点需要   进入时的逻辑(即该节点的初始化逻辑),运行逻辑,退出该节点时的逻辑。因为叶子节点直接跟业务逻辑挂钩,一开始实现时我是把处理具体逻辑的类继承于叶子节点。这样做的弊端是随着业务逻辑的复杂,基本上每个业务逻辑都要写一个业务逻辑的节点。而且这些业务逻辑节点不好共用,跟具体逻辑的耦合性太高了。后来想了想,干脆所有具体业务逻辑的节点都使用叶子节点,那么不一样的业务逻辑怎么处理呢?每个业务逻辑类不一样的无非就是进入时的逻辑(即该节点的初始化逻辑),运行逻辑,退出该节点时的逻辑,那么好办了,我们可以通过函数指针的形式,把不一样的逻辑传到叶子节点里面,这样所有的业务逻辑都可以使用叶子节点类LeafNode了。

下来开始上代码

#ifndef __LeafNode_H__#define __LeafNode_H__#include <functional>#include "BevNode.h"namespace BT{
//外部强制中断enum eInterruptState{E_IS_NONE,E_IS_FAIL,E_IS_SUCCESS,};class LeafNode;
//刚进入时的初始化操作,外部可能需要跟该节点交互,所以把该节点的指针传出去,下面两个函数同理
typedef std::function<void(LeafNode*)> enterFunc;

//退出时执行的逻辑

typedef std::function<void(LeafNode*)> exitFunc

typedef std::function<eBevState(LeafNode*, float)> executeFunc;;//运行逻辑

class LeafNode : public BevNode

{

public:

LeafNode();

virtual ~LeafNode();

virtual eBevState execute(float fDelta);

void interruptState(eInterruptState nInterruptState);

void setEnterFunc(const enterFunc& enterFun);

void setExecuteFunc(const executeFunc& executeFun);

void setExitFunc(const exitFunc& exitFun);

private:

<span style="font-family: Arial, Helvetica, sans-serif;"><span style="white-space:pre"></span>//控制该节点的初始化,执行和退出</span>
<span style="white-space:pre"></span>enum{E_LS_ENTER,E_LS_RUNNING,E_LS_EXIT,};
private:int m_nInterrupt;int m_nLeafStatus;enterFunc m_enterFunc;executeFunc m_executeFunc;exitFunc m_exitFunc;<span style="white-space:pre"></span>};}#endif
//LeafNode.cpp

#include "LeafNode.h"using namespace BT;LeafNode::LeafNode(): m_nInterrupt(E_IS_NONE), m_nLeafStatus(E_LS_ENTER), m_enterFunc(nullptr), m_executeFunc(nullptr), m_exitFunc(nullptr){}LeafNode::~LeafNode(){}eBevState LeafNode::execute(float fDelta){eBevState status = E_BevState_Success;<span style="white-space:pre"></span>//进入时if (m_nLeafStatus == E_LS_ENTER){if (m_enterFunc){m_enterFunc(this);}m_nLeafStatus = E_LS_RUNNING;}
<span style="white-space:pre"></span>//执行该节点if (m_nLeafStatus == E_LS_RUNNING){if (E_IS_NONE == m_nInterrupt){if (m_executeFunc){status = m_executeFunc(this, fDelta);if (status != eBevState::E_BevState_Running){m_nLeafStatus = E_LS_EXIT;}}else{//m_nLeafStatus = E_LS_EXIT;status = E_BevState_Running;}}else{//被打断if (E_IS_FAIL == m_nInterrupt){status = E_BevState_Fail; }else if (E_IS_SUCCESS == m_nInterrupt){status = E_BevState_Success;}m_nLeafStatus = E_LS_EXIT;}}
<span style="white-space:pre"></span>//退出该节点
if (m_nLeafStatus == E_LS_EXIT){if (m_exitFunc){m_exitFunc(this);}m_nLeafStatus = E_LS_ENTER;m_nInterrupt = E_IS_NONE;}return status;}void BT::LeafNode::setEnterFunc(const enterFunc& enterFun){m_enterFunc = enterFun;}void BT::LeafNode::setExecuteFunc(const executeFunc& executeFun){m_executeFunc = executeFun;}void BT::LeafNode::setExitFunc(const enterFunc& exitFun){m_exitFunc = exitFun;}void BT::LeafNode::interruptState(eInterruptState nInterruptState){m_nInterrupt = nInterruptState;}
条件节点:跟具体逻辑节点一样,可以使用LeafNode节点来把条件逻辑传进来执行。

到此一个简单的行为树框架已经完成,当然目前的控制节点太少了,需要补充更丰富的控制节点来满足我们的逻辑需要。


后续有时间我会写一些demo来说明下如何使用该框架。




1 0