遊戲AI之決策結構—行爲樹(2)

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

  • 有限狀態機
  • 行爲樹

在之前,遊戲AI的實現基本都是有限狀態機, 隨着遊戲的進步,遊戲AI的複雜性要求愈來愈高,傳統的有限狀態機實現很難維護愈來愈複雜的AI需求。 現代遊戲AI都比較偏向採用行爲樹做爲決策結構。設計模式

有限狀態機


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

關於狀態機設計模式的具體介紹,可參考個人另外一篇博文: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

[Toc]

相關文章
相關標籤/搜索