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


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


免責聲明!

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



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