AI行为树
用行为树来控制AI行为
项目上线的时候有很多竞品压力,关于AI方面并没有做太多的规则,为了快速上线服务器只是实现了AI的移动和攻击两个行为,其移动规则只是随机寻点移动,攻击行为也只是攻击最近的敌人。随着项目的发展,策划对AI的智能程度提出了要求,所以我们对AI的功能进行了加强。 我在之前的RPG项目中也做过AI,是基于状态机的,简单的RPG野外怪物使用状态机完全可以满足需求,但是目前的项目对AI行为的要求比较细致,所以使用了行为树。记录一下行为树的基本原理和使用情况
行为树的基本组成
最简单的AI使用if else
语句就可以实现,但是随着AI行为的复杂度越来越高,要提供给策划配置等等需求的出现,必须使用一种更为方便清晰的方法来实现AI,行为树就是一个不错的选择。要实现一个行为树,首先要把怪物的各种行为模块化,比如巡逻行为,追击行为,普通攻击行为,技能攻击行为,各种条件判断行为(自身血量是否不足30%,队友人数是否少于1个…)等等,之后利用行为树的原理将这些模块进行组合,以达到行为的多样性,这种多样性的组合是基于行为数量指数级增加的,所以提供给策划配置的灵活性非常好。一棵行为树表示一组AI逻辑,要执行这组AI逻辑,需要从根节点开始遍历执行整棵树,遍历执行的过程中,父节点根据其自身类别选择需要执行的子节点并执行,子节点执行完后将执行结果返回给父节点, 一棵普通的行为树主要由四种节点组成,我把他们分为两类,枝干节点和叶子节点。枝干节点肯定不是行为树的末端,它们主要的功能是对自己枝干下的节点进行“管理”,如何执行自己枝干下的节点是枝干节点需要做的事情。叶子节点一定是行为树的末端,它们通常是一个动作,如攻击,移动等等。
–顺序节点(Sequence):枝干节点,顺序执行子节点,只要碰到一个子节点返回false,则返回false;否则返回true。
–选择节点(Selector):枝干节点,顺序执行子节点,只要碰到一个子节点返回true,则返回true;否则返回false。
–条件节点(Condition):叶子节点,执行条件判断,返回判断结果。
–动作节点(Action):叶子节点,执行设定的动作,一般返回true。
下面是一棵简单的普通行为树,这棵行为树的根节点是一个选择节点(Selector),根节点下有两个顺序节点的枝干,每个顺序节点下面是叶子节点的条件和动作。把这课行为树用语言描述一下就是,怪物开始行动,执行根节点,首先选择移动还是攻击,顺序执行移动(11)和攻击(12),当执行移动(11)的条件中战场内无敌人(111)为true,那么继续执行下面的条件自身范围内是否有友方(112),如果返回true那么就执行定点移动行为(113),执行113结束后整棵行为树执行结束,从根节点开始再次执行。如果111或者112返回false,那么执行攻击(12),如果视野范围内有敌人,那么就攻击距离最近的敌人(122),如果没有敌人,那么整棵行为树执行结束,从根节点再次开始执行。
项目中的代码实现:
//////////////////////////////////////////////////////////////////////////
///
/// @file Behaviour.h
///
/// @date 2016-11-22 20:02:52
///
/// @brief 行为树
///
//////////////////////////////////////////////////////////////////////////
#ifndef __behaviour__Behaviour__
#define __behaviour__Behaviour__
#include <string>
#include <vector>
#include <_BaseGameObject.h>
namespace BehaviorTree
{
//每个行为的状态
enum Status
{
BH_INVALID, //无效
BH_SUCCESS, //成功
BH_FAILURE, //失败
BH_RUNNING, //执行中
};
//行为基类
class Behavior
{
public:
virtual Status update(uint32& delay){return BH_SUCCESS;};
virtual void onInitialize(){}
virtual void onTerminate(Status) {}
virtual void addBehavior(Behavior* node){}
virtual void init(){};
Behavior():
m_eStatus (BH_INVALID)
,action_id_(0)
{};
virtual ~Behavior()
{};
Status tick(uint32& delay);
Status getState()
{
return m_eStatus;
}
bool setState(uint32 _eStatus)
{
m_eStatus = (BehaviorTree::Status)_eStatus;
return true;
}
private:
Status m_eStatus;
public:
uint32 action_id_;
uint32 std_id_;
typedef std::vector <Behavior*> Behaviors;
Behaviors m_Children;
};
//枝类
class Composite : public Behavior
{
public:
virtual void onTerminate(Status) {}
virtual void addBehavior(Behavior* node)
{
m_Children.push_back(node);
}
};
//顺序节点
class Sequence : public Composite
{
public:
virtual void onInitialize();
virtual Status update(uint32& delay);
Behaviors::iterator m_CurrentChild;
};
//选择节点
class Selector : public Composite
{
public:
virtual void onInitialize();
virtual Status update(uint32& delay);
virtual void onTerminate();
Behaviors::iterator m_CurrentChild;
};
//并行节点
class Parallel : public Composite
{
public:
virtual void onInitialize();
virtual Status update(uint32& delay);
Behaviors::iterator m_CurrentChild;
};
//并行节点(全部节点执行完成结束)
class ParallelEx : public Composite
{
public:
virtual void onInitialize();
virtual Status update(uint32& delay);
Behaviors::iterator m_CurrentChild;
};
}
#endif //__behaviour__Behaviour__
//////////////////////////////////////////////////////////////////////////
///
/// @file Behaviour.cpp
///
///
/// @brief 行为树
///
//////////////////////////////////////////////////////////////////////////
#include "Behaviour.h"
namespace BehaviorTree
{
Status Behavior::tick(uint32& delay)
{
if (m_eStatus == BH_INVALID)
{
onInitialize();
}
m_eStatus = update(delay);
if (m_eStatus != BH_RUNNING)
{
onTerminate(m_eStatus);
}
return m_eStatus;
}
void Sequence::onInitialize()
{
m_CurrentChild = m_Children.begin();
}
Status Sequence::update(uint32& delay)
{
while (true)
{
Status s = (*m_CurrentChild)->tick(delay);
if (s != BH_SUCCESS) {
return s;
}
// 最后一个节点,执行完了
if(++m_CurrentChild == m_Children.end())
{
m_CurrentChild = m_Children.begin();
return BH_SUCCESS;
}
}
return BH_INVALID;
}
void Selector::onInitialize()
{
m_CurrentChild = m_Children.begin();
}
Status Selector::update(uint32& delay)
{
while (true)
{
Status s = (*m_CurrentChild)->tick(delay);
if (s != BH_FAILURE) {
return s;
}
if(++m_CurrentChild == m_Children.end())
{
m_CurrentChild = m_Children.begin();
return BH_FAILURE;
}
}
return BH_INVALID;
}
void Selector::onTerminate()
{
setState(BH_INVALID);
}
void Parallel::onInitialize()
{
m_CurrentChild = m_Children.begin();
}
Status Parallel::update( uint32& delay )
{
return BH_INVALID;
}
void ParallelEx::onInitialize()
{
m_CurrentChild = m_Children.begin();
}
Status ParallelEx::update( uint32& delay )
{
while (true)
{
bool b_break = true;
uint32 delay_prev=delay;
for (auto ite = m_Children.begin();ite!=m_Children.end();++ite)
{
Status s = (*ite)->tick(delay);
if (delay<delay_prev)
{
delay_prev = delay;
}
if (BH_RUNNING==s)
{
b_break = false;
}
}
delay = delay_prev;
//if ( delay<100 )
//{
// delay = 100;
//}
if (true == b_break)
{
return BH_SUCCESS;
}
else
{
return BH_RUNNING;
}
}
return BH_INVALID;
}
}
上面的代码是行为树的基类,四种节点中的叶子节点(条件节点,动作节点)可以直接继承Behavior类,顺序节点是Sequence类,选择节点是Selector,我的代码里还加入了一种扩展的节点,并行节点Parallel类,这种枝干节点会同时执行枝干下的节点,不论这些节点返回什么,加入这种节点主要是为了实现同时移动同时攻击这种动作。我们项目中继承自Behavior类的部分代码(一个动作节点,一个条件节点):
//定点循环移动行为
class CActionMoveLoop : public BehaviorTree::Behavior
{
public:
CActionMoveLoop(IHZArenaPlayer* player,uint32 action_id);
~CActionMoveLoop();
public:
virtual BehaviorTree::Status update(uint32& delay);
virtual void onInitialize();
virtual void onTerminate(BehaviorTree::Status){setState(BehaviorTree::BH_INVALID);};
private:
World3DPosition GetoutOnePoint(bool& result);
int32 point_index_;
int32 order_type_;//0顺时针,1逆时针
std::vector<WorldPosition> path_point_; ///< 路径
void init();
IPlayer* player_;
CFightAIPlugin* ai_plugin_;
World3DPosition target_point_;
};
//检测条件,属性检测
class CConditionProp : public BehaviorTree::Behavior
{
public:
CConditionProp(IHZArenaPlayer* player,uint32 action_id);
~CConditionProp(){};
public:
virtual BehaviorTree::Status update(uint32& delay);
virtual void onInitialize();
virtual void onTerminate(BehaviorTree::Status){};
void init();
private:
uint32 compare_type_;
uint32 prop_value_;
uint32 prop_percent_;
IPlayer* player_;
CFightAIPlugin* ai_plugin_;
};
策划通过绘制行为树逻辑图来梳理逻辑,然后通过逻辑图输出逻辑配置到xml中,生成一棵行为树的代码:
BehaviorTree::Behavior* CFightAIPlugin::create_tree_root( const StdTreeRoot_data::IStdRoot* std_root )
{
switch (std_root->get_RootType())
{
//顺序
case 1:
{
BehaviorTree::Sequence* rootseq = new BehaviorTree::Sequence();
for (uint32 i = 0; i<std_root->StdChild_size(); i++)
{
auto std_child = std_root->get_StdChild(i);
if (std_child->get_NodeType()==1)
{
auto std_node = get_StdTreeRoot_manager()->find_StdRoot_byRootID(std_child->get_ChildID());
if (NULL == std_node)
{
continue;
}
rootseq->m_Children.push_back(create_tree_root(std_node));
}
else
{
auto std_node = get_StdTreeAction_manager()->find_StdAction_byActionID(std_child->get_ChildID());
if (NULL == std_node)
{
continue;
}
rootseq->m_Children.push_back(create_tree_leaf(std_node));
}
}
return rootseq;
}
//选择
case 2:
{
BehaviorTree::Selector* rootsel = new BehaviorTree::Selector();
for (uint32 i = 0; i<std_root->StdChild_size(); i++)
{
auto std_child = std_root->get_StdChild(i);
if (std_child->get_NodeType()==1)
{
auto std_node = get_StdTreeRoot_manager()->find_StdRoot_byRootID(std_child->get_ChildID());
if (NULL == std_node)
{
continue;
}
rootsel->m_Children.push_back(create_tree_root(std_node));
}
else
{
auto std_node = get_StdTreeAction_manager()->find_StdAction_byActionID(std_child->get_ChildID());
if (NULL == std_node)
{
continue;
}
rootsel->m_Children.push_back(create_tree_leaf(std_node));
}
}
return rootsel;
}
//并行
case 3:
{
BehaviorTree::ParallelEx* rootpara = new BehaviorTree::ParallelEx();
for (uint32 i = 0; i<std_root->StdChild_size(); i++)
{
auto std_child = std_root->get_StdChild(i);
if (std_child->get_NodeType()==1)
{
auto std_node = get_StdTreeRoot_manager()->find_StdRoot_byRootID(std_child->get_ChildID());
if (NULL == std_node)
{
continue;
}
rootpara->m_Children.push_back(create_tree_root(std_node));
}
else
{
auto std_node = get_StdTreeAction_manager()->find_StdAction_byActionID(std_child->get_ChildID());
if (NULL == std_node)
{
continue;
}
rootpara->m_Children.push_back(create_tree_leaf(std_node));
}
}
return rootpara;
}
//动作或者条件
case 4:
{
RLOG(MINFO)<<"root can not be action!!!";
break;
}
default:
break;
}
RLOG(MINFO)<<"root wrong:"<<std_root->get_RootID();
return NULL;
}
BehaviorTree::Behavior* CFightAIPlugin::create_tree_leaf( const StdTreeAction_data::IStdAction* std_action )
{
switch (std_action->get_ActionType())
{
//移动行为
case 1:
{
return create_move_action(std_action);
break;
}
//条件检测
case 2:
{
return create_check_action(std_action);
break;
}
//攻击行为
case 3:
{
return create_attack_action(std_action);
break;
}
//其他行为
case 4:
{
return create_other_action(std_action);
break;
}
default:
{
break;
}
}
RLOG(MINFO)<<"No This Big Type:"<<std_action->get_ActionSubType()<<" id:"<<std_action->get_ActionID();
return NULL;
}
BehaviorTree::Behavior* CFightAIPlugin::create_move_action( const StdTreeAction_data::IStdAction* std_action )
{
switch (std_action->get_ActionSubType())
{
// 多坐标点顺序移动:到过的坐标点会被移除
case 1:
{
CActionMovePath *move_path = new CActionMovePath(m_pPlayerObj,std_action->get_ActionID());
return move_path;
}
// 多坐标点循环移动:反复循环
case 2:
{
CActionMoveLoop *move_loop = new CActionMoveLoop(m_pPlayerObj,std_action->get_ActionID());
return move_loop;
}
// 坐标点半径巡逻
case 3:
{
CActionMovePatrol *move_patrol = new CActionMovePatrol(m_pPlayerObj,std_action->get_ActionID());
return move_patrol;
}
// 指定id移动
case 4:
{
CActionMoveToAI *move_ai = new CActionMoveToAI(m_pPlayerObj,std_action->get_ActionID());
return move_ai;
}
// 最近敌人移动
case 5:
{
CActionMoveToCloseEnemy *move_close_enemy = new CActionMoveToCloseEnemy(m_pPlayerObj,std_action->get_ActionID());
return move_close_enemy;
}
// 占旗点巡逻
case 6:
{
CActionMoveFlag *move_patrol_flag = new CActionMoveFlag(m_pPlayerObj,std_action->get_ActionID());
return move_patrol_flag;
}
default:
{
break;
}
}
RLOG(MINFO)<<"No This Move Action Type:"<<std_action->get_ActionSubType()<<" id:"<<std_action->get_ActionID();
return NULL;
}
这些代码片段包含了行为树的创建,通过配置表来生成一棵复杂的行为树。然后我们还需要在一个循环中执行这棵树的逻辑:
uint32 CFightAIPlugin::AIUpdateBT()
{
if (behavior_tree_==NULL)
{
return 0;
}
if (BehaviorTree::BH_RUNNING != behavior_status_)
{
behavior_now_ = behavior_tree_;
behavior_now_->onInitialize();
RLOG(MINFO)<<"----------loop---------";//执行结束,从树根重新开始执行
}
uint32 delay = 1000;
behavior_status_ = behavior_now_->update(delay);//update当前的节点
RLOG(MINFO)<<"ai delay:"<<delay;
return delay;
}
AIUpdateBT()
函数会在定时器下每间隔一段时间(100~1000ms,根据自己的需求调整每一次update的时间)update一次,来驱动整棵树的执行。
掌握了行为树的基本原理就可以实现各种各样复杂的AI逻辑并且条理清晰,便于管理,不会因为逻辑的复杂而使代码混乱,下面的图片展示了一个比上面例子更复杂的逻辑,你完全可以根据自己项目的需要来配置一个更大的行为树,它会严格按照你的配置来执行。