前言
This article isn’t written by me, all rights retained by the copyright holder.
Author: Marco
如果你想要開發遊戲,你遲早都需要面對狀態機(Finite State Machine)的問題。讓我們從基礎開始:什麼是狀態機?
如果你想要複雜的答案, 這個連結是維基百科的頁面。但我們都喜歡把事情簡單化,所以我對狀態機的解釋是這樣的:
狀態機是一種用來定義複雜行為的結構。
歸功於狀態機,我們可以定義各種複雜的行為,並且把他們分裝成好幾個名為 State 的基本單位。每一個狀態(State)都負責描述一個很單純的動作。而多個狀態之間的交互作用最終就能構成複雜的行為表現。
簡單的狀態機範例
讓我們從一個非常簡單的例子開始:一顆按鈕。
如果我們想要設計一顆典型的按鈕,我們可以將它的行為定義成兩種基本狀態:按(Pressed)、放(Released)。如果我們想在遊戲中實作按鈕的邏輯,一個簡單的布林變數(Boolean)就足夠了:

利用布林變數 isButtonPressed,我們區分出了兩種狀態。在這之後,我們可以宣告一個叫做 Button 的 類別(Class),用它來控制每個狀態的行為。它的內容差不多會長這樣:

非常的簡單與直覺。現在我們來讓事情更有趣一點!想像你有一位可以做出以下動作的角色:
- 跳躍(Jump)
- 移動(Move)
- 攻擊(Attack)
- 閒置(Idle)

如你所見,這是一個由 4 種狀態組成的簡易狀態機,我們同一時間內只能處於一種狀態裡。此外我們還加了新的要素:每個狀態之間的關係。
你可能會想:用 4 個布林變數來代表 4 種狀態不就解決了嗎?理論上來說是可以的,但我強烈不建議這樣做。理由很簡單:
- 你最後會創造出一大串可怕的 “If-Else” 語句,因為你每次都要判斷自己現在在哪個狀態、想去的是哪一個狀態,最後再根據這一層關係去決定行為的結果。
- 這種方法非常容易出Bug,因為可能會有多個 Boolean 被意外地設為 true,導致我們同時身處多個狀態。
基本的狀態機實作
我們可以用 Enum 取代一連串的 Bool,把事情變得簡單。例如:

實作這樣的程式碼,我們就解決了 2 個剛剛列出來的問題。
中等難度的狀態機
上面的這個方法並沒有做到「C狀態只能從A狀態切換過去」這樣的設計。例如:只有 idle 狀態可以前往 run 狀態(反之亦然),而且不能從 run 狀態前往 attack 狀態。
我們可以加入另一個變數叫做 nextState。每當我們想要切換狀態,我們必須檢查 currentState 以確保 nextState 是允許前往的地方。然而,這樣的方法依舊會導致一長串的 If-Else 語句,如果我們擁有非常多種狀態,程式碼就會變得難以理解。
該如何解決這個問題呢?
我設計了一個比剛剛提到的更複雜一些的狀態機,你可以在我的 Github 找到相關的程式碼和操作說明。這個狀態機能夠定義狀態之間的轉換(Transition)。例如:

如你所見,在這個新的結構中我們可以使用像是 CanReachNext(stateToReach) 這樣的函式,這個方法能確保只能前往被允許的狀態。如果你試圖前往一個無法直接到達的新狀態,狀態機不會做任何動作。
複雜版的狀態機
既然我們已經做好了可以處理基本規則的狀態機,讓我們來嘗試些更難的東西。接下來要解決的問題是:我們想要在每個狀態身上實作更進階的行為,例如:每當我進入一個狀態,我希望可以追蹤這個狀態的運行階段,然後在我準備要離開這個狀態時執行一些動作。我們可以用下列這幾個函式來描述需要的行為:
- void OnStateEnter(PlayerState state);
- void OnStateExecution(PlayerState state);
- void OnStateExit(PlayerState state);
想像一下:若我們把每個狀態的這 3 個函式都塞在同一個 Class 裡,程式碼最後一定會變得很混亂,所以我們必須找個更易於使用、擴展性更高的方法。
在開始之前,我們必須確保「每一個狀態都會有前面提到的那 3 個函式」。我們可以把每一個狀態都拆成一個獨立的 Class,再用介面(interface)來統一規範它們。

現在我們可以用 Class 來定義狀態:

在角色的 Class 中,我們會宣告一連串的狀態 Class,例如:

每當我們改變狀態,我們就會做這樣的事情:

呼叫狀態執行它該做的事情:

狀態機之間的繼承
又有一個狀況發生了 : (。想像你的角色可以在跳躍、跑步、向下移動的時候攻擊,如果我們選擇幫「邊跑邊攻擊」、「邊跳邊攻擊」…各自設計一個專屬的狀態,那這個狀態機到最後就會充斥著很多行為類似的狀態,造成日後很大的困擾。例如: Attack 狀態和 JumpAttack 狀態的行為幾乎一模一樣,差別只在於播放的動畫不同而已。
為了避免寫太多重複程式碼,我們可以利用繼承(Inheritance)讓行為相似的狀態共享程式碼。
Example:
讓 JumpAttackState 繼承 AttackState,再額外規範它的動畫表現就好。

總結來說,我們該怎麼判斷何時適合使用狀態機呢?
- 當物件的行為可以被分類成好幾個明確的狀態
- 狀態之間的轉換能夠被特定的 trigger/action 所觸發
- 區分/定義行為不會耗費太多時間及成本
狀態機的替代方案
如果你想要設計非常複雜的 AI,使用狀態機可能依然會使構造龐大到難以控管。此時,比較好的替代方案是行為樹(Behavior Tree),它能夠更有效地定義複雜的行為,這是在人工智慧領域非常常見的應用。
Example:
