從有限狀態機(FSM)到行爲樹(Behavior Tree)

選此次主題,要感謝一位網友的來信,他詢問了一些如何將有限狀態機轉成行爲樹的問題,當時,我回信給了一些建議,但後來我仔細想了一下,以爲可能說得還不夠全面,因此我就想經過這篇文章,來整理出一些比較典型的轉化「模板」,給有這方面疑惑的朋友一些幫助,若是有朋友有一些本身的看法的,能夠在後面留言,咱們一塊兒討論。 node

有限狀態機維護了一張圖,圖的節點是一個個的狀態,節點和節點的連線是狀態間根據必定的規則作的狀態轉換,每個狀態內的邏輯均可以簡要描述爲: 測試

若是知足條件1,則跳轉到狀態1 spa

若是知足條件2,則跳轉到狀態2 設計

code

不然,不作跳轉,維持當前狀態 ci

稍做整理的話,咱們能夠對狀態機的幾種跳轉的狀況一一描述出來,而後看看若是將這些狀況用行爲樹來表示的話,能夠怎麼作。這就是我前面說的「轉化模板」,固然我不能保證我下面列出的是狀態機的全部可能狀況,若是你們在實踐中發現還有其餘的狀況,歡迎留言,我隨時更新。 it

在這以前,咱們能夠先回憶一些關於行爲樹的一些概念(能夠參考1,2) io

控制節點:選擇節點,序列節點,並行節點,等等 table

行爲節點:兩種運行狀態,「運行中」和「完成」 模板

前提條件

模式1:當處在任何狀態中,一旦某條件知足,即跳轉到某個特定的狀態。

好比,在狀態機中的一些錯誤處理,常常會用到上面的這種模式,當狀態在運行過程當中,發生了某些異常,那通常,咱們會把狀態機跳轉到某個異常狀態。這種狀況,咱們能夠用到帶優先級的選擇節點(Priority Selector)方式,以下圖,能夠看到,咱們把狀態c做爲行爲樹裏的行爲節點,跳轉條件(Condition1)做爲這個節點的前提(Precondition)。

再用上面舉到的錯誤處理的例子,在狀態機中,咱們通常會這樣寫:

STATE A::Update()
 2: {
 3:     ...
 4:     if(error)
 5:     {
 6:         return TO_ERROR_STATE();
 7:     }
 8:     ...
 9:     return UNCHANGED_STATE();
 10: }

轉換到行爲樹中,咱們會經過外部的黑板來作通訊(能夠參考這裏),在行爲節點a中,咱們會這樣寫

EXECUTE_STATE A::Execute(BlackBoard& out)
 2: {
 3:     ...
 4:     if(error)
 5:     {
 6:         out.error = error;
 7:         return EXECUTE_STATE_FINISH;
 8:     }
 9:     ...
 10:     return EXECUTE_STATE_RUNNING;
 11: }

而後對於節點c的前提裏,咱們來讀取黑板裏的error值

bool Condition1::IsTrue(const BlackBoard& in) const
 2: {
 3:     return in.error == true;
 4: }

模式2:對於同一個跳轉條件,處在不一樣的狀態會有不一樣的跳轉

好比,咱們有兩個狀態,a和b,他們都對同一個跳轉條件做出響應,但和模式1不一樣的是,a對跳轉到狀態c,而b會跳轉到狀態d,換句話說,這是一種帶有上下文的狀態跳轉方式。對於這種狀況,能夠用到序列節點的相關特性,以下圖

序列節點中,當前一個節點運行完成後,會執行下一個節點,利用此特性,在上圖中能夠看到,咱們在a中,當知足條件Condition1的時候,則返回「完成」,那行爲樹就會自動跳轉到c節點中,參考代碼以下:

EXECUTE_STATE A::Execute(BlackBoard& out)
 2: {
 3:     ...
 4:     if(condition1 == true)
 5:     {
 6:         return EXECUTE_STATE_FINISH;
 7:     }
 8:     ...
 9:     return EXECUTE_STATE_RUNNING;
 10: }

對於這種模式的另外一種轉化,能夠不用序列節點,仍是用到選擇和前提的組合,但咱們在前提中加上一個當前狀態的附加條件,以下圖

在第二層的前提中,咱們能夠這樣寫

bool InACState::IsTrue(const BlackBoard& in)
 2: {
 3:     return in.current_running_node = A::GetID() ||
 4:            in.current_running_node = C::GetID();
 5: }
 6: bool InBDState::IsTrue(const BlackBoard& in)
 7: {
 8:     return in.current_running_node = B::GetID() ||
 9:            in.current_running_node = D::GetID();
 10: }

這樣對於c的前提就是Condition1和InACState的「與」(回想一下前提的相關內容)。因爲咱們保留了上下文的信息,因此經過對於前提的組合,咱們就轉化了這種模式的狀態機。

模式3:根據條件跳轉到多個狀態,包括自跳轉

這是在狀態機裏最多見的模式,因爲是基於條件的跳轉,因此能夠很是方便的用選擇節點和前提的組合來描述,特別值得注意的是,對於自跳轉而言,其實就是維持了當前的狀態,因此,在構建行爲樹的時候,咱們不須要特別考慮自跳轉的轉換。以下圖所描述了,咱們先用序列節點來保證跳轉的上下文(能夠參考模式2中的相關內容),這裏用到的另外一個技巧是,咱們會在狀態a結束的時候,在黑板中記錄其結束的緣由,以供後續的選擇節點來選擇。另外,咱們在第二層選擇節點第一次用到了非優先級的選擇節點,和帶優先級的選擇節點不一樣,它每次都會從上一次所能運行的節點來運行,而不是每次都從頭開始選擇。

固然,和模式2相似的是,咱們也能夠不用序列節點,而是單純的用選擇節點,這樣的話,做爲默認狀態的狀態a就須要處在選擇節點的最後一個,由於僅當全部跳轉條件都不知足的時候,咱們纔會維持在當前的狀態。如上圖的下面那顆行爲樹那樣。請仔細查看,我在前三個節點對於前提的定義,除了自己的跳轉條件外,還加上了一個額外的條件,InAXState,它保證了僅在上一次運行的是A狀態或自身的時候,咱們纔會運行當前的節點,這樣就保證了和本來狀態機描述是一致的。

模式4:循環跳轉

在狀態機中存在這樣一種模式,在狀態a中,根據某條件1,會跳轉到狀態b中,而在狀態b的時候,又會根據某條件2,跳轉到狀態a,產生了這樣一個跳轉的「環」。顯而易見的是,行爲樹是一種樹形結構,而帶環的狀態機是一種圖的結構,因此對於這種狀況,我想了下,以爲須要引入一種新的選擇節點,我稱之爲動態優先級選擇節點(Dynamic Priority Selector),這種選擇節點的工做原理是,永遠把當前運行的節點做爲最低優先級的節點來處理。以下圖

當咱們在節點a的時候,咱們會先判斷b的前提,當b的前提知足的時候,咱們會運行節點b,下一幀再進來的時候,因爲如今運行的是節點b,那它就是最低優先級的,因此,咱們會先判斷節點a的前提,知足的話,就運行節點a,不知足則繼續運行節點b,依次類推。下面是我寫的相關代碼,能夠給你們參考。

void DynamicPrioritySelector::Test(const Blackboard& in) const
 2: {
 3:     bool hasRunningChild = IsValid(m_iCurrentRunningChildIndex);
 4:     int nextRunningChild = -1;
 5:     for(int i = 0; i < m_ChildNodes.Count(); ++i)
 6:     {
 7:         if(hasRunningChild &&
 8:            m_iCurrentRunningChildIndex == i)
 9:         {
 10:             continue;
 11:         }
 12:         else
 13:         {
 14:             if(m_ChildNodes[i]->Test(in))
 15:             {
 16:                 nextRunningChild = i;
 17:                 break;
 18:             }
 19:         }
 20:     }
 21:     if(IsValid(nextRunningChild))
 22:     {
 23:         m_iCurrentRunningChildIndex = nextRunningChild;
 24:     }
 25:     else
 26:     {
 27:         //最後測試當前運行的子節點
 28:         if(hasRunningChild)
 29:         {
 30:             if(!m_ChildNodes[m_iCurrentRunningChildIndex]->Test(in))
 31:             {
 32:                 m_iCurrentRunningChildIndex = -1;
 33:             }
 34:         }
 35:     }
 36:     return IsValid(m_iCurrentRunningChildIndex);
 37: }

總結

從上面4種模式的轉化方式中,咱們好像會有種感受,用行爲樹的表達好像並無狀態機的表述清晰,顯的比較複雜,羅嗦。這主要是由於咱們用行爲樹對狀態機作了直接的轉化,並想要盡力的去維持狀態機的語義的緣故。其實,在AI設計過程當中,通常來講,咱們並非先有狀態機,再去轉化成行爲樹的,當咱們選擇用行爲樹的時候,咱們就要充分的理解控制節點,前提,節點等概念,並試着用行爲樹的邏輯方式去思考和設計。

不過,有時,咱們也許想用行爲樹改造一個已有的狀態機系統,那這時就能夠用我上面提到的這些模式來嘗試着去轉換,固然在實際轉換的過程當中,個人建議是,先理清並列出每個狀態跳轉的條件,查看哪些是帶上下文的跳轉,哪些是不帶上下文的跳轉,哪些是單純的序列跳轉(好比,從狀態A,到狀態B,到狀態C,相似這樣的單線跳轉,常見於流程控制中),哪些跳轉是能夠合併的等等,而後再用行爲樹的控制節點,把這些狀態都串聯起來,當發現有些跳轉用已有的控制節點不能很好的描述的時候,能夠像我上面那樣,添加新的控制節點。

這四種模式,是我如今能想到的,可能不全,若是你們有問題,能夠在後面留言,有指教的也歡迎一塊兒討論。

相關文章
相關標籤/搜索