游戲AI的決策部分是比較重要的部分,游戲程序的老前輩們留下了兩種經過考驗的用於AI決策的結構:
- 有限狀態機
- 行為樹
在以前,游戲AI的實現基本都是有限狀態機,
隨着游戲的進步,游戲AI的復雜性要求越來越高,傳統的有限狀態機實現很難維護越來越復雜的AI需求。
現代游戲AI都比較偏向采用行為樹作為決策結構。
有限狀態機
有限狀態機的一般實現是將每個狀態寫成類,再用一個載體(也就是所謂的狀態機)管理這些狀態的切換。
關於狀態機設計模式的具體介紹,可參考我的另一篇博文:https://www.cnblogs.com/KillerAery/p/9680303.html
有限狀態機的缺陷:
- 各個狀態類之間互相依賴很嚴重,耦合度很高。
- 結構不靈活,可擴展性不高,難以腳本化/可視化。
行為樹
可以看到,行為樹由一個個節點組成
- 結構:樹狀結構
- 運行流程:從根節點開始自頂向下往下遍歷,每經過一個節點就執行節點對應的功能。
我們規定,每個節點都提供自己的excute函數,返還執行失敗/成功結果。
然后根據不同節點的執行結果,遍歷的路徑隨之改變,而這個過程中遍歷到什么節點就執行excute函數。
//節點類(基類)
class Node{
//...
public:
virtual bool excute() = 0; //執行函數,返還 成功/失敗
//...
};
主流的行為樹實現,將節點主要分為四種類型。
下面列舉四種節點類型及其對應excute函數的行為:
- 控制節點(非葉節點),行為是控制遍歷路徑的走向。
- 條件節點(葉節點),行為是提供條件的判斷結果。
- 行為節點(葉節點):行為是執行智能體的行為。
- 裝飾節點 :行為是修飾(輔助)其他三類節點。
行為樹 控制節點
控制節點是用於控制如何執行子節點(控制遍歷路徑的走向)。
由於非葉節點的特性,其需要提供容納子節點的容器和添加子節點的函數。
所以先寫好非葉節點的類:
class NonLeafNode : public Node {
std::vector<Node*> children; //子節點群
public:
void addChild(Node*); //添加子節點
virtual bool excute() = 0; //執行函數,返還 成功/失敗
};
下面列出一些控制節點的介紹:
選擇節點(Selector)
按順序執行多個子節點,若成功執行一個子節點,則不繼續執行下一個子節點。
舉例:實現要不攻擊,要不防御,要不逃跑。
用一個選擇節點,按順序添加<攻擊節點>和<防御節點>和<逃跑節點>作為子節點。
class SelectorNode : public NonLeafNode{
public:
virtual bool excute()override{
for(auto child : children){
//如果有一個子節點執行成功,則跳出
if(child->excute() == true){break;}
}
return true;
}
};
順序節點(Sequence)
按順序執行多個子節點,若遇到一個子節點不能執行,則不繼續執行下一個子節點。
舉例:實現先開門再移動到房子里。
用一個順序節點,按順序添加<開門節點>和<移動節點>作為子節點。
class SequenceNode : public NonLeafNode{
public:
virtual bool excute()override{
for(auto child : children){
//如果有一個子節點執行失敗,則跳出
if(child->excute() == false){break;}
}
return true;
}
};
並行節點(Parallel)
同時執行多個節點。
舉例:一邊說話和一邊走路。
用一個並行節點,添加<說話節點>和<走路節點>作為子節點。
class ParallelNode : public NonLeafNode{
public:
virtual bool excute()override{
//執行所有子節點
for(auto child : children){
child->excute();
}
return true;
}
};
常用的控制節點一般是<並行節點><選擇節點><並行節點>。當然還有其他更多控制節點種類(不常用):
- 隨機選擇節點(隨機執行一個子節點)。例如偶爾閑逛,偶爾停下來發呆。
- 隨機順序節點(隨機順序執行若干個子節點)
- 次數限制節點(只允許執行若干次)
- 權值選擇節點(執行權值最高的子節點)
- 等等..
可能到這里,有想到還有個問題:為什么控制節點也需要提供(執行成功/執行失敗)兩種執行結果。
答:這樣做就可以做到決策的復合——控制節點不僅可以控制行為節點,也能控制控制節點。
行為樹 條件節點
前提條件
執行節點不會總是一帆風順的,有成功也總會有失敗的結果。
這就是引入前提條件的作用——
滿足前提條件,才能成功執行行為,返還執行成功結果。否則不能執行行為,返還執行失敗結果。
但是每個節點的前提總會不同,或有些沒有前提(換句話說總是能滿足前提)。
一個可行的做法是:讓行為節點含有bool函數對象(或函數接口)。這樣對於不同的邏輯條件,就可以寫成不同的bool函數,綁定給相應的行為節點。
std::function<bool()> condition; //前提條件
現在比較成熟的做法是把前提條件抽象分離成新的節點類型,稱之為條件節點。
將其作為葉節點混入行為樹,提供條件的判斷結果,交給控制節點決策。
它相當模塊化,更加方便適用。
這里的Sequence節點是上面控制節點的一種:能夠讓其所有子節點依次運行,若運行到其中一個子節點失敗則不繼續往下運行。
這樣可以實現出不滿足條件則失敗的效果。
class ConditionNode : public Node {
std::function<bool()> condition; //前提條件
public:
virtual bool excute()override {
return condition();
}
};
行為樹 行為節點
行為節點是代表智能體行為的葉節點,其執行函數一般位該節點代表的行為。
行為節點的類型是比較多的,畢竟一個智能體的行為是多種多樣的,而且都得根據自己的智能體模型定制行為節點類型。
這里列舉一些行為:站立,射擊,移動,跟隨,遠離,保持距離....
持續行為
一些行為是可以瞬間執行完的(例如轉身?),
而另外一些動作則是執行持續一段時間才能完成的(例如攻擊從啟動攻擊行為到攻擊結算要1秒左右的時間)。
因此,這些持續行為節點的excute函數里,應先啟動智能體的持續行為,然后掛起該行為樹(更通俗地說是暫停行為樹),等到持續時間結束才允許退出excute函數並繼續遍歷該行為樹。
為了支持掛起行為樹而不影響其他CPU代碼執行,我們往往需要利用協程等待該其行為完成而不產生CPU阻塞,而且開銷遠低於真正的線程。
此外,一般是一個行為樹對應維護一個協程。
不了解協程是什么,可以參考下我的Unity協程筆記:Unity C#筆記 協程 - KillerAery - 博客園
行為節點示例實現
//行為節點類(基類)
class BehaviorNode : public Node{
public:
virtual bool excute() = 0; //執行節點
};
//舉例:移動行為節點
class MoveTo : public BehaviorNode{
public:
virtual bool excute()override{
... //讓智能體啟動移動行為
... //協程暫時掛起直到持續時間結束
return true;
}
};
行為樹 裝飾節點
裝飾節點,顧名思義,是用來修飾(輔助)的節點。
例如執行結果取反/並/或,重復執行若干次等輔助修飾節點的作用,均可做成裝飾節點。
//取反節點
class InvertNode : public OneChildNonLeafNode{
public:
virtual bool excute()override{
return !child->excute();
}
};
//重復執行次數節點
class CountNode : public OneChildNonLeafNode{
int count;
public:
virtual bool excute()override{
while(--count){
if(child->excute() == false)return false;
}
return true;
}
};
OneChildNonLeafNode是指最多可擁有一個子節點的非葉節點類,這里就不做具體實現。
總結
到這里,我們可以看到行為樹的本質:
- 把所有行為(走,跑,打,站等等)分離出來作為各種行為節點,
- 然后以不同的控制節點,條件節點,裝飾節點將這些行為復合在一起,組合成一套復雜的AI。
相比較傳統的有限狀態機:
- 易腳本化/可視化的決策邏輯
- 邏輯和實現的低耦合,可復用的節點
- 可以迅速而便捷的組織較復雜的行為決策
這里並不是說有限狀態機一無所用:
- 狀態機可以搭配行為樹:狀態機負責智能體的身體狀態,行為樹則負責智能體的智能決策。這樣在行為樹做決策前,得考慮狀態機的狀態。
- 狀態機適用於簡單的AI:對於區區需兩三個狀態的智能,狀態機解決綽綽有余。
- 狀態機運行效率略高於行為樹:因為狀態機的運行總是在當前狀態開始,而行為樹的運行總在根開始,這樣就額外多了一些要遍歷的節點(也就多了一些運行開銷)。
在《殺手:赦免》的人群系統里,人群的狀態機AI只有簡單的3種狀態,由於人群的智能體數量較多,若采取行為樹AI,則會大大影響性能。
簡而言之:行為樹是適合復雜AI的解決方案。
- 對於Unity用戶,Unity商店現在已經有一個比較完善的行為樹設計(Behavior Designer)插件可供購買使用。
Unity官方商店插件購買地址:Behavior Designer - Behavior Trees for Everyone - Asset Store
- Unreal4用戶則可以免費使用引擎自帶的行為樹
額外
-
可讓根節點記錄該AI要操控的智能體引用(指針),每次進行決策,傳給子節點當前要操控的智能體引用。這樣就可以使AI行為樹容易改變寄主。
(例如1個喪屍死了被釋放內存了,寄生它的AI行為樹不必釋放並標記為可用。一旦產生新的喪屍,就可以給這個行為樹根節點更換新的寄主,標記再改回來) -
得益於樹狀結構,重復執行次數節點(或其他類似的節點),可以讓它執行完相應的次數后,解開與父節點的連接,釋放自己以及自己的子節點。
-
共享節點型行為樹是可供多個智能體共用的一種行為樹,是節省內存的一種設計:http://www.aisharing.com/archives/563
-
LOD優化技術:LOD原本是3D渲染的優化技術。對於遠處的物體,渲染面數可以適當減少,對於近處的物體,則需要適當增加細節渲染面數。
同樣的可以用於AI上,對於遠處的AI,不需要精准每幀執行,可以適當延長到每若干幀執行。
一個武裝小隊隊員的AI行為樹示例:
游戲AI 系列文章:https://www.cnblogs.com/KillerAery/category/1229106.html