遊戲AI之決策結構—行為樹

遊資網發表於2019-09-30
遊戲AI之決策結構—行為樹


目錄

有限狀態機

行為樹

  • 控制節點
  • 條件節點
  • 行為節點
  • 裝飾節點
  • 總結
  • 額外


遊戲AI的決策部分是比較重要的部分,遊戲程式的老前輩們留下了兩種經過考驗的用於AI決策的結構:

  • 有限狀態機
  • 行為樹

在以前,遊戲AI的實現基本都是有限狀態機,

隨著遊戲的進步,遊戲AI的複雜性要求越來越高,傳統的有限狀態機實現很難維護越來越複雜的AI需求。

現代遊戲AI都比較偏向採用行為樹作為決策結構。

有限狀態機

有限狀態機的一般實現是將每個狀態寫成類,再用一個載體(也就是所謂的狀態機)管理這些狀態的切換。

關於狀態機設計模式的具體介紹,可參考我的另一篇博文:https://www.cnblogs.com/KillerAery/p/9680303.html

有限狀態機的缺陷:

  • 各個狀態類之間互相依賴很嚴重,耦合度很高。
  • 結構不靈活,可擴充套件性不高,難以指令碼化/視覺化。


行為樹

遊戲AI之決策結構—行為樹

(一個武裝小隊隊員的AI行為樹示例)


可以看到,行為樹由一個個節點組成

  • 結構:樹狀結構
  • 執行流程:從根節點開始自頂向下往下走,每經過一個節點就執行節點對應的功能。

我們規定,每個節點都提供自己的執行函式,返還執行失敗/成功結果。

然後根據不同節點的執行結果,往下的路徑隨之改變,最終總會走到末尾的葉節點,執行其對應的行為。


  1. //節點類(基類)
  2. class Node{
  3.   //...
  4. public:
  5.   virtual bool excute() = 0;      //執行函式,返還 成功/失敗
  6.   //...
  7. };
複製程式碼

主流的行為樹實現,將節點主要分為四種型別:

  • 控制節點(非葉節點)
  • 條件節點(葉節點)
  • 行為節點(葉節點)
  • 裝飾節點(非葉節點)

控制節點

控制節點是用於控制如何執行子節點。

由於非葉節點的特性,其需要提供容納子節點的容器和新增子節點的函式。

所以先寫好非葉節點的類:


  1. class NonLeafNode : public Node {
  2.     std::vector<Node*> children;    //子節點群
  3. public:
  4.     void addChild(Node*);           //新增子節點
  5.     virtual bool excute() = 0;      //執行函式,返還 成功/失敗
  6. };
複製程式碼

下面列出一些控制節點的介紹:

選擇節點(Selector)

按順序執行多個子節點,若成功執行一個子節點,則不繼續執行下一個子節點。

遊戲AI之決策結構—行為樹


舉例:實現要不攻擊,要不防禦,要不逃跑。

用一個選擇節點,按順序新增<攻擊節點>和<防禦節點>和<逃跑節點>作為子節點。


  1. class SelectorNode : public NonLeafNode{
  2. public:
  3.   virtual bool excute()override{
  4.     for(auto child : children){
  5.         //如果有一個子節點執行成功,則跳出
  6.         if(child->excute() == true){break;}
  7.     }
  8.     return true;
  9.   }
  10. };
複製程式碼


順序節點(Sequence)

按順序執行多個子節點,若遇到一個子節點不能執行,則不繼續執行下一個子節點。

遊戲AI之決策結構—行為樹


舉例:實現先開門再移動到房子裡。

用一個順序節點,按順序新增<開門節點>和<移動節點>作為子節點。

000

並行節點(Parallel)

同時執行多個節點。

遊戲AI之決策結構—行為樹


舉例:一邊說話和一邊走路。

用一個並行節點,新增<說話節點>和<走路節點>作為子節點。


  1. class SequenceNode : public NonLeafNode{
  2. public:
  3.   virtual bool excute()override{
  4.     for(auto child : children){
  5.         //如果有一個子節點執行失敗,則跳出
  6.         if(child->excute() == false){break;}
  7.     }
  8.     return true;
  9.   }
  10. };
複製程式碼

常用的控制節點一般是<並行節點><選擇節點><並行節點>。當然還有其他更多控制節點種類(不常用):

  • 隨機選擇節點(隨機執行一個子節點)。例如偶爾閒逛,偶爾停下來發呆。
  • 隨機順序節點(隨機順序執行若干個子節點)
  • 次數限制節點(只允許執行若干次)
  • 權值選擇節點(執行權值最高的子節點)
  • 等等..

可能到這裡,有想到還有個問題:為什麼控制節點也需要提供(執行成功/執行失敗)兩種執行結果。

答:這樣做就可以做到決策的複合——控制節點不僅可以控制行為節點,也能控制控制節點。

條件節點

前提條件

執行節點不會總是一帆風順的,有成功也總會有失敗的結果。

這就是引入前提條件的作用——滿足前提條件,才能成功執行行為,返還<執行成功>結果。否則不能執行行為,返還<執行失敗>結果。

遊戲AI之決策結構—行為樹


但是每個節點的前提總會不同,或有些沒有前提(換句話說總是能滿足前提)。

一個可行的做法是:讓行為節點含有bool函式物件(或函式介面)。這樣對於不同的邏輯條件,就可以寫成不同的bool函式,繫結給相應的行為節點。


  1.     std::function<bool()> condition;    //前提條件
複製程式碼


但是一種更復雜也更成熟的做法則是把前提條件抽象分離成新的節點型別,稱之為條件節點。

將其作為葉節點混入行為樹,輔助控制節點決策。它相當模組化,更加方便適用。

遊戲AI之決策結構—行為樹


這裡的se節點,能夠讓其所有子節點依次執行,若執行到其中一個子節點失敗則不繼續往下執行。

這樣可以實現出不滿足條件則失敗的效果。

只是由於邏輯條件的種類繁多,其編寫各種條件節點類需要花費一定時間,不過我們可以使用別人現成寫好的庫,

亦或者仍然基於bool函式物件,相當於上文做法的裝飾。


  1. class ConditionNode : public Node {
  2.     std::function<bool()> condition;    //前提條件
  3. public:
  4.         virtual bool excute()override {
  5.         return condition();
  6.     }
  7. };
複製程式碼

行為節點

行為節點是代表行為的葉節點,其執行函式一般位該節點代表的行為。

行為節點的型別是比較多的,畢竟一個智慧體的行為是多種多樣的,而且都得根據自己的智慧體模型定製行為節點型別。

這裡列舉一些行為:站立,射擊,移動,跟隨,遠離,保持距離....

行為狀態

一些行為是可以瞬間執行完的(例如轉身?),

而另外一些動作則是執行持續一段時間才能完成的(例如攻擊從啟動攻擊行為到攻擊結算要1秒左右的時間)

為了不讓每幀重複啟動執行一個持續行為,

我們給所有行為節點引入一個成員變數來標誌,我們稱為<行為狀態>。

行為狀態一般有2種:

  • ready(可執行)
  • running(正在執行)

另外可根據自己實際專案需求來定製狀態(例如加入fail狀態)。

行為節點示例實現


  1. //行為節點類(基類)
  2. class BehaviorNode : public Node{
  3. protected:
  4.     BehaviorState state;                //行為狀態
  5. public:
  6.     virtual bool excute() = 0;          //執行節點
  7. };
複製程式碼
  1. //舉例:移動行為節點
  2. class MoveTo : public BehaviorNode{
  3. public:
  4.     virtual bool excute()override{
  5.         //如果狀態是完成,則啟動行為
  6.         if(state == BehaviorState::ready){
  7.       state = BehaviorState::running; //先賦予正在執行的狀態
  8.       
  9.             ...  //讓智慧體啟動移動行為
  10.         }
  11.         return true;
  12.     }
  13. };
複製程式碼

裝飾節點

裝飾節點,顧名思義,是用來裝飾輔助的節點。

例如執行結果取反/並/或,重複執行若干次等輔助修飾節點的作用,均可做成裝飾節點。

  1. //取反節點
  2. class InvertNode : public OneChildNonLeafNode{
  3. public:
  4.   virtual bool excute()override{
  5.         return !child->excute();
  6.   }
  7. };
複製程式碼
  1. //重複執行次數節點
  2. class CountNode : public OneChildNonLeafNode{
  3.   int count;
  4. public:
  5.   virtual bool excute()override{
  6.       while(--count){
  7.         if(child->excute() == false)return false;
  8.       }
  9.         return true;
  10.   }
  11. };
複製程式碼

OneChildNonLeafNode是指最多可擁有一個子節點的非葉節點類,這裡就不做具體實現。

總結

到這裡,我們可以看到行為樹的本質:

  • 把所有行為(走,跑,打,站等等)分離出來作為各種<行為節點>,
  • 然後以不同的<控制節點><條件節點><裝飾節點>將這些行為複合在一起,組合成一套複雜的AI。

相比較傳統的有限狀態機:

  • 易指令碼化/視覺化的決策邏輯
  • 邏輯和實現的低耦合,可複用的節點
  • 可以迅速而便捷的組織較複雜的行為決策

這裡並不是說有限狀態機一無所用:

  • 狀態機可以搭配行為樹:狀態機負責智慧體的身體狀態,行為樹則負責智慧體的智慧決策。這樣在行為樹做決策前,得考慮狀態機的狀態。
  • 狀態機適用於簡單的AI:對於區區需兩三個狀態的智慧,狀態機解決綽綽有餘。
  • 狀態機執行效率略高於行為樹:因為狀態機的執行總是在當前狀態開始,而行為樹的執行總在根開始,這樣就額外多了一些要遍歷的節點(也就多了一些執行開銷)。

在《殺手:赦免》的人群系統裡,人群的狀態機AI只有簡單的3種狀態,由於人群的智慧體數量較多,若採取行為樹AI,則會大大影響效能。

簡言之:行為樹是適合解決複雜AI的解決方案。

對於Unity使用者,Unity商店現在已經有一個比較完善的行為樹設計(Behavior Designer)外掛可供購買使用。

關於行為樹執行流程,有兩個實現方式,博主只簡單實現過第二個實現方式,這裡對於第一種方式就不做詳解。

  • 一次性流程:Unity行為樹外掛目前的實現方式。一次性流程可能會阻塞在延時行為,但是可以利用協程等待該其行為完成而不產生阻塞。
  • 每幀重複從根節點開始自頂向下執行的流程:這種方式不會產生阻塞,因為若有延時行為在執行中則視為執行成功,繼續往下走。

缺點是每幀都得從根節點重新往下走,效率可能不如一次性流程(一次性走完行為樹,而不會從頭走起)。

額外

  • 可讓根節點記錄該AI要操控的智慧體引用(指標),每次進行決策,傳給子節點當前要操控的智慧體引用。這樣就可以使AI行為樹容易改變寄主。

(例如1個喪屍死了被釋放記憶體了,寄生它的AI行為樹不必釋放並標記為可用。一旦產生新的喪屍,就可以給這個行為樹根節點更換新的寄主,標記再改回來)

  • 得益於樹狀結構,重複執行次數節點(或其他類似的節點),可以讓它執行完相應的次數後,解開與父節點的連線,釋放自己以及自己的子節點。
  • 共享節點型行為樹是可供多個智慧體共用的一種行為樹,是節省記憶體的一種設計:http://www.aisharing.com/archives/563
  • LOD優化技術:LOD原本是3D渲染的優化技術。對於遠處的物體,渲染面數可以適當減少,對於近處的物體,則需要適當增加細節渲染面數。

同樣的可以用於AI上,對於遠處的AI,不需要精準每幀執行,可以適當延長到每若干幀執行。

相關閱讀:
遊戲AI研究(一):感知AI
遊戲AI遊戲研究(二):狀態機與行為樹

作者:KillerAery
部落格地址:https://www.cnblogs.com/KillerAery/p/10007887.html

相關文章