遊戲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; //執行函數,返還 成功/失敗 };
下面列出一些控制節點的介紹:
按順序執行多個子節點,若成功執行一個子節點,則不繼續執行下一個子節點。
舉例:實現要不攻擊,要不防護,要不逃跑。 用一個選擇節點,按順序添加<攻擊節點>和<防護節點>和<逃跑節點>做爲子節點。
class SelectorNode : public NonLeafNode{ public: virtual bool excute()override{ for(auto child : children){ //若是有一個子節點執行成功,則跳出 if(child->excute() == true){break;} } return true; } };
按順序執行多個子節點,若遇到一個子節點不能執行,則不繼續執行下一個子節點。
舉例:實現先開門再移動到房子裏。 用一個順序節點,按順序添加<開門節點>和<移動節點>做爲子節點。
class SequenceNode : public NonLeafNode{ public: virtual bool excute()override{ for(auto child : children){ //若是有一個子節點執行失敗,則跳出 if(child->excute() == false){break;} } return true; } };
同時執行多個節點。
舉例:一邊說話和一邊走路。 用一個並行節點,添加<說話節點>和<走路節點>做爲子節點。
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只有簡單的3種狀態,因爲人羣的智能體數量較多,若採起行爲樹AI,則會大大影響性能。
簡而言之:行爲樹是適合複雜AI的解決方案。
Unity官方商店插件購買地址:Behavior Designer - Behavior Trees for Everyone - Asset Store
可以讓根節點記錄該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]