上一篇我們講到了關於行為樹的內存優化,這一篇我們將講述行為樹的另一種優化方法——基於事件的行為樹。
問題
在之前的行為樹中,我們每幀都要從根節點開始遍歷行為樹,而目的僅僅是為了得到最近激活的節點,既然如此,為什么我們不單獨維護一個保存這些行為的列表,以方便快速訪問呢。我們可以把這個列表叫做調度器,用來保存已經激活的行為,並在必要時更新他們。
解決辦法
我們不再每幀都從根節點去遍歷行為樹,而是維護一個調度器負責保存已激活的節點,當正在執行的行為終止時,由其父節點決定接下來的行為。
監察函數
為了實現基於事件的驅動,我們必須要有一個監察函數,當行為終止時,我們通過執行監察函數通知父節點並讓父節點做出相應處理,這里我們通過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連接
