AI 行為樹的工作原理
原文連結:AI Behavior Trees: How They Work
介紹-Introduction
儘管網上已經有大量的關於行為樹的教程了,但是當我在做 Project Zomboid 時還是反覆遇到了關於行為樹的問題。很多教程不是非常關注於具體的程式碼實現,就是給出一些不帶任何實際應用場景、非常籠統的節點圖,像這樣:
因為這些教程在幫助我理解行為樹的核心原則方面沒什麼幫助,我發現我不但不瞭解行為樹的運作機制,更是對一個完整的行為樹該是長什麼樣子毫無概念,也不知道我該如何為我的遊戲中的行為樹放置什麼節點。
我花了大量時間去試驗行為樹的用法(我使用的是 JBT - Java Behavior Trees),所以我沒有花很多時間在實際的程式碼實現上。不過網上這方面的內容很多,覆蓋了各種不同的引擎,所以這對你來說應該不會是問題。
有可能我提到的某些裝飾節點(Decorator Node)型別是專屬於 JBT 而不是更加通用的行為樹概念,不過因為它們對於 Project Zomboid 是不可或缺的一部分,所以即便你使用的行為樹裡不包含它們,你也可以考慮實現類似的功能去幫助你設計你的行為樹。
我並不是這個領域的專家,但是我覺得我在 Project Zomboid 專案中積累下的一些經驗還是很有用的,如果我能在一開始就知道這些東西的話,我可以少走不少彎路。我在下面不會把具體的實現講的特別細緻,而是會抽象地介紹一些我在 PZ 專案中用到的例子。
基礎-Basics
行為樹的名字很好地解釋了它是什麼。不像有限狀態機(Finite State Machine)或其他用於 AI 程式設計的系統,行為樹是一棵用於控制 AI 決策行為的、包含了層級節點的樹結構。樹的最末端——葉子,就是這些 AI 實際上去做事情的命令;連線樹葉的樹枝,就是各種型別的節點,這些節點決定了 AI 如何從樹的頂端根據不同的情況,來沿著不同的路徑來到最終的葉子這一過程。
行為樹可以非常地“深”,層層節點向下延伸。憑藉呼叫實現具體功能的子行為樹,開發者可以建立相互連線的行為樹庫來做出非常讓人信服的 AI 行為。並且,行為樹的開發是高度迭代的,你可以從一個很簡單的行為開始,然後做一些分支來應對不同的情境或是實現不同的目標,讓 AI 的訴求來驅動行為,或是允許 AI 在行為樹沒有覆蓋到的情境下使用備用方案等等。
資料驅動 vs 程式碼驅動-Data Driven vs Code Driven
儘管跟這篇文章關係不大,但我還是想向大家介紹兩種不同的行為樹實現方式。它們二者的主要區別在於,樹是由程式碼之外的東西所定義的——如 XML 或是一些被專屬工具控制的格式,還是由程式碼直接控制的。
JBT 是一種不常見的混合體——它的編輯器提供了直接構建行為樹的工具,同時提供了一個可以將行為樹匯出為 java 程式碼的命令列工具。
但是不論用何種實現方法,這些葉節點(Leaf Node),也就是那些真正幫你的角色實現具體行為和條件判斷的節點,是需要靠你通過程式碼來定義的。用行為樹自帶的語言,或是 Lua 和 Python 這樣的指令碼語言,它們都將通過你安排的行為樹來產生複雜的行為。通過使用行為樹內的節點之間的關聯來驅動角色的行為,比直接用具體的程式碼告訴一個角色去做什麼事情,要來得有意思得多,這也是行為樹最讓人興奮的一點。
樹的遍歷-Tree Traversal
行為樹的一個特點是,它會“一層一層”地去對節點依次進行檢查,而這每一層都需要花費一個 tick 的時間,所以它需要花數個 tick 才能完成從頂部走到底的過程,來完成其邏輯,這和一般用程式碼實現功能是很不同的。
這並不是一個很有效率的方式,尤其是當你的樹變得非常深的時候。我認為行為樹的實現必須具備可以在一個 tick 內完成整個行為樹的判斷邏輯,還好 JBT 符合這一點要求。
工作流-Flow
行為樹由多種不同型別的節點構成,它們都擁有一個共同的核心功能,即它們會返回三種狀態中的一個作為結果。這三種狀態分別是:
- 成功-Success;
- 失敗-Failure;
- 執行中-Running;
前兩個,正如它們的名字,是用來向它們的父節點通知執行的成功或失敗的結果。第三種是指還在執行中,結果還未決定,在下一個 tick 的時候再去檢查這個節點的執行結果。
這個功能非常重要,它可以讓一個節點持續執行一段時間來維持某些行為。比如一個“walk(行走)”的節點會在計算尋路和讓角色保持行走的過程中持續返回“Running”來讓角色保持這一狀態。如果尋路因為某些原因失敗,或是除了某些狀況讓行走的行為不得不中止,那麼這個節點會返回“Failure”來告訴它的父節點;如果這個角色走到了指定的目的地,那麼節點返回“Success”來表示這個行走的指令已經成功完成。
這些狀態可以用來決定行為樹的走向,確保 AI 可以按照我們預期的方式來以某些順序去執行行為樹裡的行為。
一共有三種節點型別,它們分別是:
- 組合節點-Composite;
- 修飾節點-Decorator;
- 葉節點-Leaf;
組合節點 -Composite
組合節點通常可以擁有一個或更多的子節點。這些子節點會按照一定的次序或是隨機地執行,並會根據執行的結果向父節點返回“Success”、“Failure”,或是在未執行完畢時“Running”這樣的結果值。
最常用的組合節點是 Sequence(次序節點),它很簡單地按照固定的次序執行子節點,任何一個子節點返回 Failure,則這個組合節點向它的父節點返回 Failure;當所有子節點都返回 Success 時,這個組合節點返回 Success。
修飾節點-Decorator
修飾節點也可以擁有子節點,但是不同於組合節點,它只能擁有一個子節點。取決於修飾節點的型別,它的功能要麼是修改子節點返回的結果、終止子節點,或是重複執行子節點等等。
一個比較常見的修飾節點的例子是 Inverter(逆變節點),它可以將子節點的結果倒轉,比如子節點返回了 Failure,則這個修飾節點會向上返回 Success,以此類推。
葉節點-Leaf
葉節點是最低層的節點,它們不會擁有子節點。葉節點是最強大的節點型別,它們是真正讓你的樹做具體事情的基礎元素。通過與組合節點和修飾節點的配合,再加上你自己對葉節點功能的定義,你可以實現非常複雜的、智慧的行為邏輯。比如剛才提到過的一個例子,Walk。Walk 這個葉節點會讓一個角色移動到場景裡的指定位置,並根
拿程式碼作為類比的話,組合節點和修飾節點就好比那些改變程式碼 flow 的 if 判斷和 while loop 等等,而葉節點就是那些真正起作用的被呼叫的方法,去讓角色做什麼或是進行某些條件判斷。
引數可以在這些節點中起到作用,比如 Walk 的這個葉節點可以包含一個具體將要移動到的位置的引數。這些引數可以從其他變數裡獲得,比如角色將要前往的一個地點可以被 GetSafeLocation 這個節點所決定,存入一個變數裡,然後 Walk 節點可以使用這個變數來定義它的目的地。行為樹的執行中,這些不同的節點通過資料上下文來共同儲存或使用一些持久資料(persistent data),使得行為樹的功能變得強大。
另一種葉節點的型別是呼叫其他的行為樹並把當前行為樹的資料傳給對方。
這些功能將允許你高度模組化你的樹並把很多節點用在更多的地方。比如 Break into Building(闖入房間)行為需要先有一個 targetBuilding(目標房間)的變數來進行操作,那麼父樹可以先定義 targetBuilding 這個變數,在子樹需要闖入房間時通過一個葉節點來把這個變數傳入。
組合節點-Composite Nodes
接下來我們來看看這些行為樹裡最常見的組合節點。儘管這不是所有的內容,但是它們已經足以共同構建出足夠複雜的行為樹了。
次序節點-Sequences
正如它的名字所說,次序節點會依次(譯者:通常是從左到右)訪問子節點。每個子節點成功之後便輪到下一個,直到最後。如果所有子節點都 Success,則向次序節點返回 Success;其間任何一個子節點返回 Failure,就會立即向次序節點返回 Failure 的結果。(譯者:用“和 and”的方式來理解次序節點就會非常清楚了)
次序節點有很多的用處,其中最顯而易見的用法就是執行一連串有前後依存關係的行為,其中一個的失敗必然導致後續的動作沒有進行的意義,比如這個“進門”行為的例子:
這個次序節點下的子節點共同讓角色實現了從走向門到進門關門的連串動作。過程如下:
次序節點 ->Walk to Door (Success) ->次序節點(Running) ->Open Door (Success) ->次序節點(執行中) ->Walk through Door (Success) ->次序節點(Running) ->Close Door (Success) ->次序節點(Running) -> 向次序節點的父節點返回 Success。
如果角色因為某些原因未能成功走到門前,比如路被擋住了之類的,那麼試圖開門這些動作都沒有意義了。當走向門這個動作失敗的時候,次序節點就會返回 Failure,其父節點就可以根據這個結果來進行後面的事情了。
次序節點除了非常自然地用於進行一系列前後依存的動作之外,還可以用來做一些其他的事情,比如:
在上面這個例子中,次序節點的子節點不是一系列動作而是一系列的檢查。這些子節點會檢查角色是不是餓了,有沒有食物,是不是在安全的地點,只有在它們都返回 Success 時,角色才會吃東西。這樣使用次序節點可以實現類似於程式碼中 if 判斷和“與門(AND gate)”的效果。這些用於判斷的子節點可以是其他的組合節點或是修飾節點等等來實現更豐富的效果。比如下面這個使用了逆變節點的例子:
儘管功能和前面的例子完全一樣,但是通過逆變節點我們在這裡建立了一個非門(NOT gate),只有在“Enemies Around(敵人在周圍)”這個條件返回 Failure 時,這一步才會返回 Success,從而讓角色繼續進行吃東西的動作。這意味著這些節點的組合可以減少很多不必要的開發量。
選擇節點-Selector
選擇節點就是次序節點的反面。作為“與門”的次序節點要求子節點都返回 Success 來讓自己返回 Success,選擇節點則會在任何一個子節點返回 Success 時就返回 Success 並且不再繼續執行後續的子節點。相應的,當所有子節點都 Failure 時,選擇節點才會返回 Failure。選擇節點其實可以被理解為一個“或門”(OR gate)。
它的主要作用在於它可以用來表示一個行為的多種方式,從最高優先順序到最低,任何一個方式的成功都會讓這個動作 Success(譯者:比如“攻擊”節點下的“劈”“砍”和“斬”,每當這個 AI 嘗試攻擊時,都會從這三個中選擇一個來執行)。這個邏輯可以用在很多地方,乃至那些很複雜的 AI 行為。
讓我們回到之前那個進門的例子,看看選擇節點如何給這個行為增加複雜性。
我們只用了幾個節點就製造了一套應對鎖上的門的邏輯。我們來看看這個選擇節點被執行時發生了什麼吧。
首先,它執行了開門節點。它最希望得到的結果是直接開啟門,如果這一步成功了,就沒有後面的事兒了,沒有必要走到其他選擇節點的子節點裡去檢查該做什麼。但是如果門沒法成功開啟,那麼這個開門節點會 Failure,向它的父節點返回 Failure。在這個情況下,選擇節點會繼續嘗試它的第二個子節點,或是優先順序稍低於前一個的節點,來試圖開啟門鎖。
我們在這裡加入了一個執行開啟門鎖動作的次序節點,因為是次序節點,所以它的子節點的行為是有前後依存關係的,必須開鎖成功才會執行後面的開門動作。兩個子節點都 Success 後,這個次序節點的父節點——也就是那個選擇節點也會返回 Success,那麼角色就可以執行後面的穿過門的動作了。
如果前面的嘗試都 Failure(比如角色沒有鑰匙,或是沒有開鎖的技能),它們的失敗會讓選擇節點嘗試第三個開門的方式——把門打爛!
假如角色連這一點也做不到的話,它也令整個選擇節點 Failure,從而導致整個試圖走進門的行為的 Failure。
也許我們可以新增一個新的行為來作為走進門這個行為 Failure 後的備選方案?
我們在樹的頂部增加了一個選擇節點。當角色試圖進入房間時,他會先試著從門進去,當這樣做行不通時,他會嘗試從窗戶進入。這個簡化的示例很好地解釋了這個邏輯,但實際的專案裡的行為樹可要比這個複雜多了。比如說,當這個房間沒有窗戶的時候,整個“進入房間”的節點會失敗來告訴這個角色前往下一個房間?
相比我之前做過的各種 AI 開發的嘗試,行為樹能夠簡化 AI 開發的關鍵因素在於一項任務的失敗不再意味著當前所做事情的完全終止(比如,“尋路失敗。那我該幹什麼?”這樣的情況),而是符合 AI 系統正規化的,行為決策中很自然的一個可預期的結果。
你可以為所有的情況都安排一個“失敗保險”來讓角色總是知道該做什麼。一個例子是 Project zomboid 當中的 EnsureItemInInventory 行為(確認物品在物品欄)(譯者:Zomboid 遊戲裡的物品欄有點像暗黑2裡面的腰帶和異星工廠裡的快捷欄,玩家可以把物品從揹包放到這裡以供便捷地使用)。這個行為用一個選擇節點來決定使用一系列的動作中的某一個,並使用不同引數對相同行為進行遞迴呼叫,來確保某個物品在 NPC 的物品欄裡(譯者:我沒有程式設計背景,故對遞迴相關的理解恐有偏差,如有錯誤望告知)。
首先它會檢查這個物品是不是已經在這個角色的物品欄裡。這是最好的狀況,搞定。EnsureItemInInventory 成功,這個物品已經可供使用。
要是這個物品不再角色的物品欄裡,那麼我們要檢查角色攜帶的一切揹包和袋子來尋找這個物品。如果有,那這個物品會來到角色的物品欄上,返回成功。
如果仍然沒找到,那麼選擇節點的第三個分支會判斷角色當前所處的房間裡有沒有這件物品。如果有,那麼角色會移動到放有這件物品的位置來將其加入其物品欄。
如果繼續失敗,NPC 還會有招數可用。他會檢查他是否有他需要的那個物品的打造配方,並依次對配方里所需要的每一個素材進行 EnsureItemInInventory 行為的遞迴呼叫,這樣我們就可以知道 NPC 是否持有用來打造那個物品的全部素材。接下來,角色就可以打造這個物品。又一次成功。
如果還是失敗了,那麼 EnsureItemInInventory 就失敗了。沒有其他後續方案,NPC 會將這個物品加入到他的需求列表(或可以理解為一個製造任務單)裡,提醒自己接下來要去尋找這個物品。
角色可能會在後面的探索中突然湊齊製作的素材。由於麼 EnsureItemInInventory 的遞迴屬性,NPC 會嘗試尋找和探索那些最基礎的素材,一步一步地最終收集齊打造物品所需要的全部素材。
只是藉助於這些相對簡單的節點和相互的層級關係,我們一下子就擁有了一個看上去很聰明的 AI。每當 NPC 在其他的行為樹裡需要確認他是否擁有某件物品時,我們就可以拿出 EnsureItemInInventory 行為反覆使用。
我相信在隨著遊戲的開發,我可能會讓 NPC 在沒有找到物品之後有其他的後續方案來影響他尋找特別需要的物品時的行為模式,比如在急需工具錘子時會優先前往五金店這樣的地點尋找來提高成功率。又比如,有一天我開發了一些新的遊戲玩法,讓某些物品擁有了臨時的替代品,那麼尋找物品時的優先順序肯定也會受到這一因素的影響。舉個例子,與其穿過重重殭屍的包圍潛入到一家五金店去尋找一把錘子,不如就拿手頭的石頭來敲釘子好了,哪怕它沒有錘子那麼好使。
這些例子所表明的行為樹的可擴充套件性,使得 AI 的開發可以從最簡單的“把事情辦了”開始逐漸迭代,用新的選擇節點新增分支來擴充套件不同情況下 AI 的行為。豐富的後續方案可以降低一個行為徹底失敗情況的出現,從而展現更加合理的 AI 行為。前面提到,NPC 找不到物品時會試圖打造物品,實際上這個功能也是後面才加入的。即便沒有這個行為邏輯,NPC 也會嘗試尋找物品,但這一行為大大提高了 NPC 達成自己目標的能力。
再加上合理地為各種後續方案賦予優先順序和條件,哪怕都是程式設計好的行為,它們也能讓 AI 在行為決策時表現得更加自然和聰明。
隨機選擇節點和隨機次序節點-Random Selectors and Random Sequences
這兩種節點跟它們的非隨機版本運作起來幾乎一樣,除了它們在選擇要執行的子節點時是隨機的。當子節點上的行為沒有明確的優先順序時,使用這種節點可以為 AI 的行為提供更多的的不可預測性。
修飾節點-Decorator Nodes
逆變節點-Inverter
前面提到過這個節點,他會反置或否定子節點的結果。
成功節點-Succeeder
成功節點不管它的子節點向其返回的結果為何,它總是返回 Success 的結果。這個往往用在當你知道一個子節點一定會返回 Failure 的結果,而它的父節點是次序節點,會因此而終止,那麼你可以強行讓這個子節點返回 Success,來避免這一情況的發生。我們並不需要一個專門的失敗節點,因為一個逆變節點加上成功節點就可以達到這一效果。
重複節點-Repeater
重複節點會在它的子節點返回結果後反覆繼續執行它。重複節點常常被用在一棵樹的最頂部來確保樹的持續執行。另外重複節點也可以被設定重複執行的次數。
重複直至失敗節點-Repeat Until Fail
類似重複節點重複執行子節點,但這一節點會在子節點 Failure 的時候返回 Failure。
資料上下文-Data Context
接下來的內容涉及到具體的行為樹的實現,以及所使用的程式語言等問題,所以方法上會因人而異。因此,我會盡量保持內容的抽象和概念化。
當一個行為樹被呼叫時,一個資料上下文也被建立出來。它的用途是儲存被節點所解釋和改變的變數。這些變數隨後可以被節點結合著資料的上下文加以讀寫,從而使整個行為樹保持為一個統一的整體。當你開始深入瞭解這部分內容時,你會發現行為樹的靈活性和適用範圍是多麼出色,你對它的設計的力量變得更為明顯。我將以前面提到過的門和窗的行為為例,來繼續討論這一話題。
定義葉節點-Defining Leaf Nodes
這裡的內容是關於行為樹的具體實現的,所以不同的方法和系統會有所區別。但為了通過葉節點來將遊戲的具體功能加入到行為樹中,大部分的系統都會有下面這兩個功能。
Init(初始) -- 在一個節點第一次被其父節點訪問時被呼叫。比如一個次序節點會在輪到它執行的時候呼叫 Init。在所有的子節點都完成了執行並返回了結果給父節點以前,即走完一次流程之前,Init 不會再次被呼叫。這個功能用來初始化這個節點並開始執行它的動作。以 Walk 這個行為為例,它會獲取一些引數來初始化這個尋路的任務。
Process(執行)-- 在節點執行時的每一 tick 都會被呼叫。如果得到了 Running 的結果則會一直執行下去;而一旦這個功能得到了成功或失敗的結果,執行就會終止,結果也被返回給父節點。在 Walk 這個例子裡,它會返回 Running,直到尋路成功或是失敗。
節點可以有屬性,既可以是被明確指定傳入的引數,也可以是根據資料上下文從控制這個 AI 的實體裡的變數引用而來。我不會涉及具體的實現方式,因為它會因使用的程式語言和行為樹系統而有所不同,但是行為樹的引數和資料儲存的概念是通用的。
比如,我們這樣定義一個 Walk 節點:
Walk(character,destination)
- Success:到達目的地
- Failure: 未能達到目的地
- Running:行進中
在這個例子中 Walk 節點有兩個引數,分別是 character(角色)和 destination(目的地)。儘管很自然地會認為執行這個 AI 行為的物件就是這個節點的所有者,所以不需要專門把這個資訊作為引數傳入,但是最好還是不要這樣預設。很多次我發現,尤其是在作為條件判斷的節點上,我經常需要為了測試其他角色的行為或與他們互動而重新寫程式碼。所以我們最好還是多走一步,哪怕你認為只有這個 AI 會使用這個行為,也還是把角色的資訊也作為引數傳入。
至於 destination,可以手動地填入 XYZ 的座標。但是更有可能的情況,是這些座標資訊作為上下文變數被引用,如從其他的物件中獲取的位置,或是根據 NPC 所在位置計算出的一個最近的安全躲避點等等。
棧-Stacks
當第一次使用行為樹時,很自然地會把節點的適用範圍與角色的行為、判斷條件和情境聯絡起來。帶著這些限制,行為樹的能力得不到最大發揮。
我發現用節點實現棧操作會有很顯著的作用,於是我為遊戲加入了下面這些節點:
- PushToStack(item, stackVar)
- PopFromStack(stack, itemVar)
- IsEmpty(stack)
就是這樣,三個節點而已。它們只需要 init/ process 功能的支援就可以實現一個標準庫的建立和修改的棧操作,只用了幾行程式碼,它們就開啟了一系列的可能性。
比如 PushToStack 建立一個新的棧,並且將傳入的變數名存入,壓入棧。類似的,PopFromStack 將一個前面壓入的變數彈出棧並儲存為 itemVar 變數。當棧已經為空時返回失敗。IsEmpty 就是用來檢查這個棧是否是空的,如果是則返回成功,否則失敗。
使用這些節點,我們就可以用這樣的樹來遍歷整個棧:
PopFromStack,加上一個在棧為空之前都會重複讓其執行的 Until fail repeater 父節點就可以實現我們需要的結果。
接下來是一些其他的我常用的功能性節點:
- SetVariable(varName, object)
- IsNull(object)
現在假設我們新增一個叫做 GetDoorStackFromBuilding 的節點,你可以傳入一個房間物體,然後它會將這個房間的門物體全部取出並建立一個棧來儲存它們並以其作為我們的目標物件。那麼接下來我們可以用它來做什麼事情呢?
呃,看起來變得有些複雜了。不過最終,跟任何語言一樣,當你理解了其中原理你就可以輕鬆讀懂它,而且在失去了可讀性的時候,我們還獲得了靈活性。
它做了什麼呢?簡而言之,這個行為會提取一個房間的所有門並嘗試進入,如果角色成功地進入了任何一個則返回 Success,否則會 Failure。
首先它會獲取這個包含了所有門的棧,通過呼叫 Until Fail repeater 節點,它會反覆執行它的子節點,直到返回 Failure 為止。它的子節點是一個次序節點,會用 PopFromStack 從前面提到的棧裡彈出一個門,並將其儲存在 door 這個變數裡,後面會用到 door 這個變數來告訴角色該去試圖進入哪個門。
如果因為棧空了而彈出失敗,則這個節點會返回 Failure 並用這個結果結束了前面的 Until Fail repeater 節點,繼續最頂上的這個次序節點的執行,來到了這個結果被逆變的 IsNull 節點。它會檢查 usedDoor 這個變數的 IsNull(是不是空的),顯然它一定是空的,即返回 Success,因為我們還從來沒有機會去設定它,所以這個成功被逆變節點返回為 Failure,於是整個行為 Failure。
如果棧確實彈出了門,那麼它會呼叫另一個次序節點(也是帶有逆變節點的),它會嘗試走向那扇門,開啟它然後走進去。
如果因為種種原因 NPC 沒能成功穿過這扇門,那麼這個次序節點會 Failure,而逆變節點會將這個 Failure 轉化為 Success 向上返回,導致它的父節點仍然走不出這個 Until Fail repeater,故而會繼續重複 PopFromStack,改變 door 變數的值,讓 NPC 去嘗試進入另一個門。
如果 NPC 成功穿過了一扇門,那麼它會將這扇門賦值給 usedDoor 變數,並返回 Success。你會注意到我用到了一個 Succeeder 來修飾關門這個動作,這是因為要是前面 NPC 使用了破門的方式來開門的話,這裡關門的動作應該就不會成功了,所以我需要這個修飾節點來確保它的成功結果。最後,整個次序節點的成功會被逆變節點轉化為失敗並讓父節點離開 Until Fail repeater。在這種情況下,行為樹繼續往下走,我們會在 usedDoor 的 IsNull 中 Failure,因為剛剛 usedDoor 被賦了值,通過被逆變後,結果為 Success,現在它的父節點也會返回 Success,得知 NPC 成功地找到了一扇門並走了進去。
如果還是失敗的話,我們可以用 GetWindowStackFromBuilding 來使用一樣的步驟再對所有的窗戶走一遍流程。
翻譯:金盟
來源:INDIENOVA
地址:https://indienova.com/indie-game-development/ai-behavior-trees-how-they-work/
相關文章
- 怪物ai與行為樹設計AI
- 遊戲AI之決策結構—行為樹遊戲AI
- AI模組(有限狀態機、行為樹)-應用在cocos中AI
- nginx+php執行請求的工作原理NginxPHP
- 要知道AI的工作原理,Get這些點就夠了AI
- ?【Spring專題】「原理系列」SpringMVC的執行工作原理(補充修訂)SpringMVC
- Mirror 的工作原理
- LiveData的工作原理LiveData
- OAuth的工作原理OAuth
- Feign的工作原理
- Spark的工作原理Spark
- 一個故事看懂AI神經網路工作原理AI神經網路
- 瀏覽器工作原理(22) - JavaScript是如何影響DOM樹構建的?瀏覽器JavaScript
- JavaScript的工作原理:引擎,執行時和呼叫堆疊JavaScript
- 簡單分析ThreadPoolExecutor回收工作執行緒的原理thread執行緒
- JavaScript的工作原理:解析、抽象語法樹(AST)+ 提升編譯速度5個技巧JavaScript抽象語法樹AST編譯
- Android View 的工作原理AndroidView
- HTTPS代理的工作原理HTTP
- SOCKS代理的工作原理
- OSPF的基本工作原理
- nginx執行請求的工作原理之location匹配詳解Nginx
- 工作流引擎的工作原理與功能
- 深度學習的工作原理深度學習
- PHP 中的 foreach 工作原理PHP
- 基本的爬蟲工作原理爬蟲
- spring-mvc的工作原理SpringMVC
- 介紹GitOps的工作原理Git
- WireGuard 教程:WireGuard 的工作原理
- Android View的工作原理(上)AndroidView
- SSH反向隧道的工作原理
- SAP Spartacus NgExpressEngineDecorator 的工作原理Express
- 【譯】JavaScript的工作原理:引擎,執行時和呼叫堆疊的概述JavaScript
- KubernetesAPIserver工作原理APIServer
- Mybatis工作原理MyBatis
- require工作原理UI
- HTTPS工作原理HTTP
- Nginx工作原理Nginx
- pr工作原理