游戏AI(三)—行为树优化之基于事件的行为树

上一篇我们讲到了关于行为树的内存优化,这一篇我们将讲述行为树的另一种优化方法——基于事件的行为树。

问题

在之前的行为树中,我们每帧都要从根节点开始遍历行为树,而目的仅仅是为了得到最近激活的节点,既然如此,为什么我们不单独维护一个保存这些行为的列表,以方便快速访问呢。我们可以把这个列表叫做调度器,用来保存已经激活的行为,并在必要时更新他们。

解决办法

我们不再每帧都从根节点去遍历行为树,而是维护一个调度器负责保存已激活的节点,当正在执行的行为终止时,由其父节点决定接下来的行为。

监察函数

为了实现基于事件的驱动,我们必须要有一个监察函数,当行为终止时,我们通过执行监察函数通知父节点并让父节点做出相应处理,这里我们通过C++标准库中的std::funcion实现监察函数
using BehaviorObserver = std::function<void(EStatus)>;

行为调度器

调度器负责管理基于事件的行为树的核心代码,负责对所有需要更新的行为进行集中式管理,不允许复合行为自主管理和运行自己的子节点。。。这里我们将调度器整合进了BehvaiorTree类。当然也可以弄个单独的类进行管理。

class BehaviorTree
{
public:
		BehaviorTree(Behavior* InRoot) :Root(InRoot) {}
		void Tick();
		bool Step();
		void Start(Behavior* Bh,BehaviorObserver* Observe);
		void Stop(Behavior* Bh,EStatus Result);
private:
		//已激活行为列表
		std::deque<Behavior*> Behaviors;
		Behavior* Root;
};

void BehaviorTree::Tick()
{
	//将更新结束标记插入任务列表
	Behaviors.push_back(nullptr);
	while (Step())
	{
	}
}

bool BehaviorTree :: Step()
{
	Behavior* Current = Behaviors.front();
	Behaviors.pop_front();
	//如果遇到更新结束标记则停止
	if (Current == nullptr)
		return false;
	//执行行为更新
	Current->Tick();
	//如果该任务被终止则执行监察函数
	if (Current->IsTerminate() && Current->Observer)
	{
		Current->Observer(Current->GetStatus());
	}
	//否则将其插入队列等待下次tick处理
	else
	{
		Behaviors.push_back(Current);
	}
}

void BehaviorTree::Start(Behavior* Bh, BehaviorObserver* Observe)
{
	if (Observe)
	{
		Bh->Observer = *Observe;
	}
	Behaviors.push_front(Bh);
}
void BehaviorTree::Stop(Behavior* Bh, EStatus Result)
{
	assert(Result != EStatus::Running);
	Bh->SetStatus(Result);
	if (Bh->Observer)
	{
		Bh->Observer(Result);
	}
}

我们通过一个双端队列保存已激活行为,在更新时从首端去走哦偶行为,再将需要更新的行为压入队列尾端。当发现任务终止时,执行其监察函数。
而Start()函数负责将行为压入队列首端,Stop()节点则负责设置行为执行状态并显示调用监察函数。

事件驱动的复合节点

大部分动作和条件代码并不受事件驱动方式的影响。而复合节点则是受事件驱动影响最明显的节点。复合节点不再自己更新和管理子节点,而是通过向调度器提出请求以更新子节点。这里我们以Sequence节点为例。
/顺序器:依次执行所有节点直到其中一个失败或者全部成功位置
class Sequence :public Composite
{
public:
virtual std::string Name() override { return "Sequence"; }
static Behavior* Create() { return new Sequence(); }
void OnChildComplete(EStatus Status);
protected:
virtual void OnInitialize() override;
protected:
Behaviors::iterator CurrChild;
BehaviorTree* m_pBehaviorTree;
};

void Sequence::OnInitialize()
{
	CurrChild = Children.begin();
	BehaviorObserver observer = std::bind(&Sequence::OnChildComplete, this, std::placeholders::_1);
	Tree->Start(*CurrChild, &observer);
}


void Sequence::OnChildComplete(EStatus Status)
{
	Behavior* child = *CurrChild;
	//当当前子节点执行失败时,顺序器失败
	if (child->IsFailuer())
	{
		m_pBehaviorTree->Stop(this, EStatus::Failure);
		return;
	}
	
	assert(child->GetStatus() == EStatus::Success);
	//当前子节点执行成功时,判断是否执行到数组尾部
	if (++CurrChild == Children.end())
	{
		Tree->Stop(this, EStatus::Success);
	}
	//调度下一个子节点
	else
	{
		BehaviorObserver observer = std::bind(&Sequence::OnChildComplete, this, std::placeholders::_1);
		Tree->Start(*CurrChild, &observer);
	}
}

因为现在各节点由调度器统一管理,所以Update函数不再需要。我们在OnIntialize()函数中设置需要更新的首个节点,并将OnChildComplete作为其监察函数。在OnchildComplete函数中实现后续子节点的更新。

总结

通过基于事件的方式,我们可以在行为树执行时节省大量的函数调用,对其性能无疑是一次巨大的提升。
github连接

posted @ 2017-12-24 22:36  月夜魔术师  阅读(2817)  评论(0编辑  收藏  举报