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

在做一些游戏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

选择节点

//Selector.h
#include "BevComm.h"
namespace BT
{
	class Selector : public BevNode
	{
	public:
		Selector(){}
		virtual ~Selector(){}

		virtual eBevState execute(float fDelta);
	};
}

//Selector.cpp
#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 : 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:
    //控制该节点的初始化,执行和退出
        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;
    }; // endof class LeafNode
}; // endof namespace BT

#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;
        //进入时
	if (m_nLeafStatus == E_LS_ENTER)
	{
		if (m_enterFunc)
		{
			m_enterFunc(this);
		}
		m_nLeafStatus = E_LS_RUNNING;
	}
        //执行该节点
	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;
		}
	}
        //退出该节点
	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来说明下如何使用该框架。

5赞

整个demo啊 楼主 :3:

还有一些东西没写完,下来有时间会弄完会把demo发上来

很不错的代码,思路也很清晰。期待demo

楼主可以把代码放在github上,分享个github的链接。。

恩,用lua写过类似的,比如说控制战斗逻辑,
战斗类型可以有PVP、PVE
每种战斗过程中可以有预览、准备、剧情、开始、暂停、结束、回放等阶段
每个阶段都是个节点,可以自由组合节点间的逻辑

能否共享呀,好想观摩大神代码

我花了点儿时间,把楼主帖子改成markdown格式了。楼主有空检查一下代码是否正确,也可以把源码作为附件上传到这个论坛。

恩,没问题,多谢了王总。我另外找个空闲点的时间把代码整理下上传上来

好的,我会找个空闲时间整理下把代码上传上来

恩,是的。处理一些复杂逻辑会比较方便

不错不错,类似AST