❀ 前言 ❀

什麼是狀態

就像 discord 中的狀態
線上綠點、掛機月亮、正在玩遊戲…等等
而遊戲也因不同事件會有不同的狀態
這次以角色為例,例如:閒置、跑步、跳躍…等等。

❀ 正題 ❀

流程圖

各個類的職責

  • IState:製作介面(interface),讓 State 都有固定的版面。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public interface IState
    {
    void Enter();
    void Exit();
    void LogicUpdate();
    void PhysicUpdate();
    }
  • PlayerState:負責 handle 玩家有的各個狀態。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.InputSystem;

    public class PlayerState : ScriptableObject, IState
    {
    protected PlayerStateMachine playerStateMachine;

    protected PlayerMovement playerMovement;

    protected PlayerInput input;
    public void Initialize(PlayerMovement playerMovement, PlayerInput input, PlayerStateMachine playerStateMachine)
    {
    this.playerStateMachine = playerStateMachine;
    this.playerMovement = playerMovement;
    this.input = input;
    }
    public virtual void Enter()
    {

    }

    public virtual void Exit()
    {

    }

    public virtual void LogicUpdate()
    {

    }

    public virtual void PhysicUpdate()
    {

    }
    }
  • StateMachine:使 StateMachine 有固定的版面。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class StateMachine : MonoBehaviour
    {
    IState currentState;

    protected Dictionary<System.Type, IState> stateTable;
    void Update()
    {
    currentState.LogicUpdate();
    }
    private void FixedUpdate()
    {
    currentState.PhysicUpdate();
    }
    protected void SwitchOn(IState newState)
    {
    currentState = newState;
    currentState.Enter();
    }
    public void SwitchState(IState newState)
    {
    currentState.Exit();
    SwitchOn(newState);
    }
    public void SwitchState(System.Type newState)
    {
    SwitchState(stateTable[newState]);
    }
    }
  • PlayerStateMachine:負責執行狀態的行為,與切換狀態的方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.InputSystem;

    public class PlayerStateMachine : StateMachine
    {
    [SerializeField] public PlayerState[] states;

    PlayerMovement playerMovement;

    PlayerInput input;

    private void Awake()
    {
    playerMovement = GetComponent<PlayerMovement>();

    input = GetComponent<PlayerInput>();

    stateTable = new Dictionary<System.Type, IState> (states.Length);

    foreach (PlayerState state in states)
    {
    state.Initialize(playerMovement, input, this);
    stateTable.Add(state.GetType(), state);
    }
    }
    private void Start()
    {
    SwitchOn(stateTable[typeof(PlayerState_Idle)]);
    }
    }

各狀態的邏輯

  1. 進入狀態該做的事
  2. 當前狀態該做的事
  3. 如何切換狀態
  4. 離開狀態該做的事
    以下,我將圍繞著這四點說明
    (本人尚未放置動畫)

Idle State

  1. 進入時播放閒置動畫
  2. 無:)
  3. 當玩家輸入移動,則切換成跑步
  4. 無:)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    using UnityEngine;

    [CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/Idle", fileName = "PlayerState_Idle")]
    public class PlayerState_Idle : PlayerState
    {
    public override void Enter()
    {
    playerMovement.Move(0f);
    }
    public override void LogicUpdate()
    {
    if (input.Move)
    {
    playerStateMachine.SwitchState(typeof(PlayerState_Run));
    }
    }
    }

Run State

  1. 進入時播放跑步動畫
  2. 使玩家移動
  3. 當玩家無輸入移動,則切換成閒置
  4. 無:)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    using UnityEngine;

    [CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/Run", fileName = "PlayerState_Run")]
    public class PlayerState_Run : PlayerState
    {
    [SerializeField] float runSpeed = 5f;

    public override void LogicUpdate()
    {
    if (!input.Move)
    {
    playerStateMachine.SwitchState(typeof(PlayerState_Idle));
    }
    }
    public override void PhysicUpdate()
    {
    playerMovement.Move(runSpeed);
    }
    }

❀ 後話 ❀

switch V.S State Pattern (狀態模式)

明明可以用一連串的if-else(或switch),為何需要那麼麻煩的狀態機?

  1. 更好讀
    如果要用 if-else,那必須將一連串程式碼寫在 Update() 中
    也就是說程式可能長這樣
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class StateMachine : MonoBehaviour
    {
    protected PlayerStateMachine playerStateMachine;

    protected PlayerMovement playerMovement;

    protected PlayerInput input;
    public void Initialize()
    {
    this.playerStateMachine = this;
    this.playerMovement = GetComponent<PlayerMovement>();
    this.input = GetComponent<PlayerInput>();
    }

    private void Awake()
    {
    Initialize();
    }
    void Update()
    {
    if (!input.Move)
    {
    playerMovement.Move(0f);
    }
    }
    private void FixedUpdate()
    {
    if (input.Move)
    {
    playerMovement.Move(runSpeed);
    }
    }
    }
    發現了嗎,無法一次從程式中找到何種狀態該在何處添加。
    當然可以加註解,但當狀態多起來時就會變得雜亂。
    此外,我也有用到 FixedUpdate()
    所以狀態的判斷就需要做兩次(此處剛好沒用上)
  2. 易維護
    我將各個狀態變為 ScriptableObject,
    如果想移除狀態,只需拉出,不會影響到其他程式碼。
    若像上方,則不易新增與移除(移除要找到對應程式後刪除,且要再新增就崩盤)

參考資料

若想實做一遍,請參閱:阿嚴 unity 教程
裡面有更完整的介紹。