皇家圖書館 - 外國資源翻譯

為遊戲開發者所寫的有限狀態機(Finite state machine (FSM) for game developers)

前言

 
這篇文章是我在剛接觸 FSM 的時候讀到的,一直躺在我的 Chrome 書籤列裡XD
內容是在介紹狀態機在遊戲開發中的應用
觀念沒有很深入,但我覺得非常適合剛接觸這一塊的人閱讀!
 

This article isn’t written by me, all rights retained by the copyright holder.

Author: Marco

Original Post


FSM

如果你想要開發遊戲,你遲早都需要面對狀態機(Finite State Machine)的問題。讓我們從基礎開始:什麼是狀態機?

如果你想要複雜的答案, 這個連結是維基百科的頁面。但我們都喜歡把事情簡單化,所以我對狀態機的解釋是這樣的:

狀態機是一種用來定義複雜行為的結構。

歸功於狀態機,我們可以定義各種複雜的行為,並且把他們分裝成好幾個名為 State 的基本單位。每一個狀態(State)都負責描述一個很單純的動作。而多個狀態之間的交互作用最終就能構成複雜的行為表現。


簡單的狀態機範例

讓我們從一個非常簡單的例子開始:一顆按鈕。

如果我們想要設計一顆典型的按鈕,我們可以將它的行為定義成兩種基本狀態:按(Pressed)、放(Released)。如果我們想在遊戲中實作按鈕的邏輯,一個簡單的布林變數(Boolean)就足夠了:

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

非常的簡單與直覺。現在我們來讓事情更有趣一點!想像你有一位可以做出以下動作的角色:

  • 跳躍(Jump)
  • 移動(Move)
  • 攻擊(Attack)
  • 閒置(Idle)

https://i1.wp.com/gamedevelopertips.com/wp-content/uploads/2016/07/Schermata-2016-07-22-alle-09.01.37.png?resize=607%2C370

如你所見,這是一個由 4 種狀態組成的簡易狀態機,我們同一時間內只能處於一種狀態裡。此外我們還加了新的要素:每個狀態之間的關係。

你可能會想:用 4 個布林變數來代表 4 種狀態不就解決了嗎?理論上來說是可以的,但我強烈不建議這樣做。理由很簡單:

  1. 你最後會創造出一大串可怕的 “If-Else” 語句,因為你每次都要判斷自己現在在哪個狀態、想去的是哪一個狀態,最後再根據這一層關係去決定行為的結果。
  2. 這種方法非常容易出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,再額外規範它的動畫表現就好。

總結來說,我們該怎麼判斷何時適合使用狀態機呢?

  1. 當物件的行為可以被分類成好幾個明確的狀態
  2. 狀態之間的轉換能夠被特定的 trigger/action 所觸發
  3. 區分/定義行為不會耗費太多時間及成本

狀態機的替代方案

如果你想要設計非常複雜的 AI,使用狀態機可能依然會使構造龐大到難以控管。此時,比較好的替代方案是行為樹(Behavior Tree),它能夠更有效地定義複雜的行為,這是在人工智慧領域非常常見的應用。

Example:

https://i0.wp.com/gamedevelopertips.com/wp-content/uploads/2016/07/image01.png?resize=1024%2C336

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s