游戲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連接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM