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
选择节点
#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来说明下如何使用该框架。




  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值