Unity 狀態機分享


建立時間: 2023年5月9日 16:10
更新時間: 2023年5月9日 18:29

說明

本篇將分享在 Unity 實現狀態機,並且示範使用狀態機來控制遊戲中的狀態,例如:暫停、遊戲中、勝利、遊戲結束等等。

什麼是狀態機

有限狀態機(英語:finite-state machine,縮寫:FSM)又稱有限狀態自動機(英語:finite-state automaton,縮寫:FSA),簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學計算模型。

截自維基百科 有限狀態機

多種狀態機

如果上網搜尋狀態機,你會看到每個人寫的狀態機都不盡相同,但卻有很多相似之處,這是因為狀態機是一種概念,要如使用程式實現這個概念有很多種方法,因為每個人實現狀態機的目的不同,這也會影響狀態機在設計上的考量,導致每個狀態機會有差異。

關於此狀態機

本篇要介紹的狀態機是參考 State Machines in Unity (how and when to use them) 設計改良的,內文很長,說明的很詳細,若想要更了解狀態機設計概念、目的、以及如何一步一步慢慢修改的話,可以先閱讀這篇。

此狀態機主要是為了處理每個遊戲狀態,例如暫停的時候,玩家不能控制角色,跟大部分的遊戲狀態機一樣,不想寫太多 if-else 述句,因為這樣程式會越來越難維護。

此狀態機算是非常陽春的版本,主要以狀態機控制器、各個狀態實現,除了基本的切換狀態以外,還有新增檢查是否可以切換狀態的功能。

狀態控制器可以有多個,例如:遊戲狀態控制器、玩家狀態控制器等等。

狀態以類別區分,例如:遊戲狀態類、玩家狀態類。

每個類別的狀態可存在多種狀態,例如:遊戲狀態類存在暫停狀態、遊戲中狀態等等。

狀態控制器

StateController.cs

using System;
using UnityEngine;

/// <summary>
/// 狀態控制器,可以存在多種狀態控制器
/// 每個狀態控制器存在多種狀態
/// </summary>
public abstract class StateController : MonoBehaviour
{
    /// <summary>
    /// 當前狀態
    /// </summary>
    private State currentState;

    /// <summary>
    /// 當前狀態類型
    /// </summary>
    public Type CurrentStateType
    {
        get => currentState?.GetType();
    }

    private void Awake()
    {
        DontDestroyOnLoad(gameObject);
    }

    protected virtual void Start()
    {
    }

    protected virtual void Update()
    {
        currentState?.OnStateUpdate();
    }

    /// <summary>
    /// 檢查當前遊戲狀態
    /// </summary>
    /// <typeparam name="StateType">遊戲狀態</typeparam>
    /// <returns></returns>
    public bool IsCurrentState<StateType>() where StateType : State
    {
        return currentState is StateType;
    }

    /// <summary>
    /// 改變狀態
    /// </summary>
    /// <param name="newState">新狀態</param>
    public void ChangeState(State newState)
    {
        if (currentState != null && !currentState.CanTransitionTo(newState))
        {
            Debug.LogError($"Can't change state from {currentState} to {newState}");

            return;
        }
        currentState?.OnStateExit();
        currentState = newState;
        currentState.OnStateEnter(this);
    }
}

注意這裡有使用 DontDestroyOnLoad(gameObject);,這是因為我的遊戲過關會切換場景,但我需要讓狀態控制器始終保持存在,你可以自行決定要不要這麼做,也許你也可以在 StateController 的子類別才這麼做,一切都看你的需求決定。

狀態

State.cs

/// <summary>
/// 抽象根狀態,下一層是定義狀態類型
/// </summary>
public abstract class State
{
    /// <summary>
    /// 用來處理遊戲物件的媒介
    /// </summary>
    protected StateController stateController;

    /// <summary>
    /// 可被覆寫的進入狀態
    /// </summary>
    protected virtual void OnEnter()
    {
    }

    /// <summary>
    /// 可被覆寫的離開狀態
    /// </summary>
    protected virtual void OnExit()
    {
    }

    /// <summary>
    /// 可被覆寫的持續更新狀態
    /// </summary>
    protected virtual void OnUpdate()
    {
    }

    /// <summary>
    /// 檢查是否可以轉換成指定狀態
    /// </summary>
    /// <param name="state">狀態</param>
    /// <returns></returns>
    public abstract bool CanTransitionTo(State state);

    /// <summary>
    /// 狀態控制器呼叫進入狀態
    /// </summary>
    /// <param name="stateController">狀態控制器</param>
    public void OnStateEnter(StateController stateController)
    {
        this.stateController = stateController;
        OnEnter();
    }

    /// <summary>
    /// 狀態控制器呼叫離開狀態
    /// </summary>
    public void OnStateExit()
    {
        OnExit();
    }

    /// <summary>
    /// 狀態控制器呼叫持續更新狀態
    /// </summary>
    public void OnStateUpdate()
    {
        OnUpdate();
    }
}

注意,State 沒有繼承 MonoBehaviour,有關遊戲處理的程式可以依賴 stateController,後面範例會說明。

使用方式

上面是狀態機核心程式,接下來是自定義的程式。

自訂遊戲狀態控制器

GameStateController.cs

/// <summary>
/// 遊戲狀態控制器
/// </summary>
public class GameStateController : StateController
{
    /// <summary>
    /// 處理遊戲邏輯,給狀態呼叫
    /// </summary>
    public void DoSomething()
    {
        // 處理遊戲邏輯
    }
}

自訂狀態類型

GameState.cs

/// <summary>
/// 遊戲狀態,例如:暫停、遊戲進行中等等
/// </summary>
public abstract class GameState : State
{
    protected GameStateController gameStateController;

    protected override void OnEnter()
    {
        base.OnEnter();

        gameStateController = stateController as GameStateController;
    }
}

注意 gameStateController = stateController as GameStateController; 使用轉型讓狀態取得遊戲狀態控制器。

自訂狀態

因為狀態有很多,所以我只舉例做出一種狀態出來。

PausedState.cs

/// <summary>
/// 遊戲暫停狀態
/// </summary>
public class PausedState : GameState
{
    protected override void OnEnter()
    {
        base.OnEnter();

        gameStateController.DoSomething();
    }

    public override bool CanTransitionTo(State state)
    {
        return true;
    }
}

CanTransitionTo 方法 return true 是因為這裡是示範,具體邏輯需自行設計,什麼情況回傳 true,什麼情況回傳 false。

第一次切換狀態示範

這裡示範剛進入遊戲就要暫停的情況。因為我原本遊戲中的遊戲狀態控制器會一直存在,所以在每次過關切換關卡的時候,都得靠如下遊戲初始化器來幫我處理初始化的工作,這是我自己遊戲中的邏輯,主要是要示範剛進入遊戲就暫停,並非狀態機所必要做的工作。

using UnityEngine;

/// <summary>
/// 遊戲初始化器
/// </summary>
public class GameInitializer : MonoBehaviour
{
    GameStateController gameStateController;

    private void Awake()
    {
        gameStateController = FindObjectOfType<GameStateController>();
    }

    // Start is called before the first frame update
    void Start()
    {
        gameStateController.ChangeState(new PausedState());
    }
}

我沒有在 GameStateControllerStart 方法呼叫 ChangeState(new PausedState()); 是因為切換關卡後,GameStateController 不會被摧毀而是一直存在,所以它的 Start 方法不會再次被呼叫,原本使用 SceneManager.sceneLoaded += OnSceneLoaded; 發現會發生無法取得遊戲物件的情況,具體原因不清楚,這是我想很久才想到一個折衷的方法。

觀看次數: 1504
finitegamestatemachineunity遊戲狀態機
按讚追蹤 Enjoy 軟體 Facebook 粉絲專頁
每週分享資訊技術

一杯咖啡的力量,勝過千言萬語的感謝。

支持我一杯咖啡,讓我繼續創作優質內容,與您分享更多知識與樂趣!