選此次主題,要感謝一位網友的來信,他詢問了一些如何將有限狀態機轉成行爲樹的問題,當時,我回信給了一些建議,但後來我仔細想了一下,以爲可能說得還不夠全面,因此我就想經過這篇文章,來整理出一些比較典型的轉化「模板」,給有這方面疑惑的朋友一些幫助,若是有朋友有一些本身的看法的,能夠在後面留言,咱們一塊兒討論。 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,相似這樣的單線跳轉,常見於流程控制中),哪些跳轉是能夠合併的等等,而後再用行爲樹的控制節點,把這些狀態都串聯起來,當發現有些跳轉用已有的控制節點不能很好的描述的時候,能夠像我上面那樣,添加新的控制節點。
這四種模式,是我如今能想到的,可能不全,若是你們有問題,能夠在後面留言,有指教的也歡迎一塊兒討論。