AI行為樹的工作原理
最近在項目中應用到了行為樹,在網上看了不少關於行為樹的文章,其中有一篇文章我覺得寫得非常到位,它的原文是英文。在這篇文章裏我會它的大致意思給翻譯出來(註:由於原文有不少啰嗦的地方,所以沒有逐句的翻譯),以它的行為結構為標準,並會在譯文中穿插一些自己的理解來談談我眼中的行為樹。
原文鏈接
一、介紹
盡管網上已經有大量關於行為樹的教程,但是在開發遊戲中的 AI 時還是遇到了不少關於行為樹的問題。很多教程不關註於代碼的實現,而是給出一些非常籠統的節點圖,就像這樣:
這些節點看起來非常的抽象,這對於我了解行為樹的運作機制沒有什麽幫助,因為這樣完全建立不起一個完整行為樹的概念。自然也就更不知道如何在代碼中進行行為樹的實現。
行為樹的概念見名知意,它是一棵具有層級結構的樹,主要用來控制 AI 的決策,樹的末端(註:葉子節點)就是 AI 實際要去做的事情。連接樹枝的是各種類型的節點(註:這些節點將在後面講解),這些節點決定了如何一個 AI 如何從根節點走到某一個葉子節點,並執行相應的命令操作。
二、樹的遍歷
行為樹的一個特點就是它會一層一層的對節點依次進行檢查,而每一層都要花費一個 Tick 的時間,所以一棵樹要花費很多 Tick 的時間才能完成遍歷,這和一般代碼的實現有很大的區別。這是一個很沒有效率的方式,尤其是當樹變得很深的時候。我認為行為樹的實現最好要在一次 Tick 裏面完成整個行為樹的邏輯判斷,也就是說最好能在一次 Tick 時間裏完成整棵樹的遍歷。
三、工作流
行為樹由多種不同類型的節點組成,這些節點都會返回三種狀態中的一種作為節點的運行結果。三種狀態分別是:
- 成功 - Success
- 失敗 - Failure
- 運行中 - Running
前兩個,正如它們的名字一樣,是用來向它們的父節點通知運行的成功或失敗。第三種是指還在運行中,結果還未確定,會在下一個 Tick 的時候再去檢查這個節點的運行結果。這個功能非常重要,它可以讓一個節點持續運行一段時間來維持某些行為。比如一個 Walk 節點會在計算尋路和讓角色保持行走的過程中持續返回 Running 來讓 AI 保持這一狀態。如果尋路因為某些原因失敗,或是除了某些狀況讓行走的行為不得不中止,那麽這個節點會返回 Failure 來告訴它的父節點;如果這個角色走到了指定的目的地,那麽節點返回 Success 來表示這個行走的指令已經成功完成。這些狀態可以用來決定行為樹的走向,確保 AI 可以按照我們預期的方式來以某些順序去執行行為樹裏的行為。
上面是節點的 3 種狀態。說完了節點的 3 種狀態,下面來說一說行為樹中節點的類型。行為樹節點類型分為下面 3 種:
- 組合節點 - Composite
- 修飾節點 - Decorator
- 葉子結點 - Leaf
這些節點類型可以說是行為樹最精華的內容,下面將分別對這 3 種類型的節點做說明。
1、組合節點
我們先來看看行為樹中最常見的組合節點。組合節點通常可以擁有一個或更多的子節點。這些子節點會按照一定的次序或是隨機地執行,並會根據執行的結果向父節點返回 Success 、Failure,或是在未執行完畢時返回 Running 這樣的結果值。
通過使用組合節點,我們就可以構造出復雜的行為樹。其中組合節點又包括下面幾種節點:- 次序節點
- 選擇節點
- 並行節點
(1)次序節點
次序節點會按從左往右的順序依次執行子節點,每個子節點成功之後便輪到下一個執行,直到最後執行完該節點下的全部子節點。如果所有子節點都返回 Success,則向次序節點的父節點返回 Success;其間任何一個子節點返回 Failure,就會立即向次序節點的父節點返回 Failure 的結果。這種性質和與門(AND gate) 是一致的。次序節點有很多的用處,其中最常用的做法就是執行一連串有前後依存關系的行為,其中一個的失敗必然導致後續的動作沒有進行的意義,比如下面這個例子: 這個次序節點讓 AI 角色實現了從走向門、進門、關門等一系列連續動作。整個過程可以描述成這樣:次序節點 ->Walk to Door (Success) ->次序節點(Running) ->Open Door (Success) ->次序節點(Running) ->Walk through Door (Success) ->次序節點(Running) ->Close Door (Success) ->次序節點(Running) -> 向次序節點的父節點返回 Success。如果 AI 因為某些原因未能成功走到門前,比如路被擋住了之類的,那麽試圖開門這些動作都沒有意義了。即當 Walk to Door 這個動作失敗後,次序節點就會向其父節點返回 Failure。註意這裏不是向次序節點返回 Success,可以將次序節點理解成就是一個橋梁的作用,真正想得到這一連串執行結果的是該次序節點的父節點。 次序節點除了非常自然地用於進行一系列前後依存的動作之外,還可以用來做一些其他的事情,比如: 在上面這個例圖中,次序節點的子節點不是一系列動作而是一系列的檢查。這些子節點會檢查 AI 角色是不是餓了,有沒有食物,是不是在安全的地點,只有在它們都返回 Success 時,角色才會吃東西。這樣使用次序節點可以實現類似於代碼中 IF 判斷和與門(AND gate)的效果。這些用於判斷的子節點可以是其他的組合節點或是修飾節點等等來實現更豐富的效果。比如下面這個使用了逆變節點(註:逆變節點是一種裝飾節點,後文闡述)的例子: 盡管功能和前面的例子完全一樣,但是通過逆變節點我們在這裏創建了一個非門(NOT gate),只有在“Enemies Around(敵人在周圍)”這個條件返回 Failure 時,這一步才會返回 Success,從而讓角色繼續進行吃東西的動作。這意味著這些節點的組合可以減少很多不必要的開發量。(2)選擇節點
選擇節點就是次序節點的反面。作為與門(AND gate)的次序節點要求子節點都返回 Success 來讓自己返回 Success,選擇節點則會在任何一個子節點返回 Success 時就返回 Success 並且不再繼續運行後續的子節點。相應的,當所有子節點都 Failure 時,選擇節點才會返回 Failure。選擇節點其實可以被理解為一個或門(OR gate)。 它的主要作用在於它可以用來表示一個行為的多種方式,從最高優先級到最低,任何一個方式的成功都會讓這個動作 Success,比如 Attack(攻擊) 節點下的有 Hack、Cut,Chop三個子節點,每當這個 AI 嘗試攻擊時,都會從這三個中選擇一個來執行,只要任何一個子節點執行成功,該 Attack 節點就返回成功。(3)並行節點
我們每個節點都會有一個運行狀態,來表示當前行為是否結束。對於組合節點來說,它的運行狀態就是其子節點的運行狀態,選擇節點和次序節點比較好處理,因為對於這兩種控制節點來說,每時刻,只會有一個子節點在運行,只要返回在運行的這個子節點的狀態即可。但對於並行節點來說,它同時刻會有多個子節點運行。
2、修飾節點
修飾節點也可以擁有子節點,但是不同於組合節點,它只能擁有一個子節點。取決於修飾節點的類型,它的功能要麽是修改子節點返回的結果、終止子節點,或是重復執行子節點等等。常見的修飾節點有以下幾種:- 逆變節點
- 成功節點
- 重復節點
(1)逆變節點
Inverter(逆變節點)可以將子節點的結果倒轉,比如子節點返回了 Failure,則這個修飾節點會向上返回 Success,就像上面的例子裏所說的一樣。
(2)成功節點
成功節點不管它的子節點向其返回的結果為何,它總是向它的父節點返回 Success 的結果。這個往往用在當你知道一個子節點一定會返回 Failure 的結果,而它的父節點是次序節點,會因為子節點的 Failure 而終止,那麽你可以強行讓這個子節點返回 Success,來避免這一情況的發生。我們並不需要一個專門的失敗節點,因為一個逆變節點加上成功節點就可以達到這一效果。
(3)重復節點
重復節點會在它的子節點返回結果後反復繼續執行它。重復節點常常被用在一棵樹的最頂部來確保樹的持續運行。另外重復節點也可以被設定重復執行的次數。
3、葉子節點
葉節點是最低層的節點,它們不會擁有子節點。葉節點是最強大的節點類型,它們是真正讓你行為樹做具體事情的節點。通過與組合節點和修飾節點的配合,再加上你自己對葉子節點功能的定義,你可以實現非常復雜的、智能的 AI 行為邏輯。拿代碼作為類比的話,組合節點和修飾節點就好比那些改變代碼 Flow( 工作流程 ) 的 If 判斷和 While 循環等等,而葉節點就是那些真正起作用的被調用的方法,去讓角色做具體的事情。 比如一個 Walk 葉子節點可以包含一個具體將要移動到的位置參數,而這個參數可以從其他變量裏獲得,比如角色將要前往的一個地點參數可以被 GetSafeLocation 這個節點所決定,存入一個變量裏,然後 Walk 節點可以使用這個變量來定義它的目的地。在行為樹中, 這樣將一個節點的數據用在其他節點上的行為, 用好了是非常強大的, 行為樹的運行中,這些不同的節點通過數據上下文來共同儲存或使用一些持久數據(persistent data),使得行為樹的功能變得強大。另一種葉節點的類型是調用其他的行為樹並把當前行為樹的數據傳給對方,可見一種是獲得數據另一種是產生數據。AI行為樹的工作原理