【Unity】継承とポリモーフィズムのコードで作るステートマシン

ステートマシン(状態管理)には、様々なアプローチがあります
今回は、C#のオブジェクト指向を使った方法をみていきましょう

実行結果

衝突させ、その後離れるたびに状態が次のように変化していきます
名前がアニメーションのようになっていますが、今回はテストなので直接の意味はなしていませんのでそのように考えてください

状態の遷移

Idle(アイドル)→ Walk(歩く)→ Run(走る)→ またIdleに戻る

衝突前

PlayerIdleState
PlayerIdleEnter

1回目の衝突

IdleTriggerEnter
IdleTriggerStay(衝突の間中)

離れた時

WalkTriggerExit
PlayerIdleExit
PlayerWalkEnter

2回目の衝突

WalkTriggerEnter
WalkTriggerStay(衝突の間中)

同様な状態の変化を繰り返します

特徴

ステート(状態)ごとにクラスを用意しています
クラスに記述するコードがその状態の時に実行されることになります

特定状態ごとに実行されるイベントについて

UpdateStateこの状態の間中、実行されます(UnityのUpdateに相当します)
EnterState状態に入った時に実行されます
ExitState状態を出た時に実行されます
GetNextState状態を変化させたい時に戻り値として状態を返します(変化させないときは今の状態を返します
OnTriggerEnterトリガー発生時に1度だけ実行されます
OnTriggerStayトリガー発生中にずっと実行されます
OnTriggerExitトリガーを出た時に1度だけ実行されます

シーン構成

2つのゲームオブジェクト(CupselとCube)を作成します
シーン画面でマウスでCupselをつかんでCubeにぶつけるテストをするため、次のように構成します

PlantUMLクラス図

スクリプト

このコードは、Unityゲームエンジンを使用してプレイヤーキャラクターの状態を管理するためのクラスとシステムを実装しています。以下にコードの要点を説明します。

BaseState<T> クラス

ジェネリック型 T を使用して抽象クラスを定義しています。このクラスは、プレイヤーの状態を表すための基本的な機能を提供します。T は Enum 型である必要があります。

PlayerStateMachine クラス

プレイヤーキャラクターの状態遷移を管理するためのクラスです。PlayerState という列挙型を定義し、各状態に対応する具象状態クラスのインスタンスを作成します。また、状態の切り替えと更新を行います。

PlayerIdleStatePlayerWalkStatePlayerRunState

それぞれプレイヤーのアイドル、ウォーク、ランの状態を表す具象状態クラスです。各クラスは基底クラス BaseState<PlayerStateMachine.PlayerState> を継承し、必要な状態固有の振る舞いを実装します。

各状態クラスは、状態に入るとき (EnterState)、状態から出るとき (ExitState)、状態を更新するとき (UpdateState)、次の状態を取得するとき (GetNextState)、トリガーコライダーとの相互作用があるとき (OnTriggerEnter、OnTriggerStay、OnTriggerExit) に特定の処理を行います。これらのメソッド内でデバッグログが出力されています。

PlayerStateMachine クラスの Awake メソッドでは、各状態をインスタンス化し、初期状態を設定します。また、Update メソッド内で特定のキー入力に応じて状態を更新しますが、現在のコードでは実際の状態遷移ロジックはコメントアウトされています。

このコードは、プレイヤーキャラクターの状態遷移を管理し、各状態に対する振る舞いを定義するためのフレームワークを提供しています。各状態クラスをカスタマイズすることで、プレイヤーキャラクターの振る舞いを制御できます。

using UnityEngine;
using System;

public abstract class BaseState<T> where T : Enum
{
    public BaseState(T key)
    {
        StateKey = key;
    }

    public T StateKey { get; internal set; }

    public abstract void EnterState();
    public abstract void ExitState();
    public abstract void UpdateState();
    public abstract T GetNextState();
    public abstract void OnTriggerEnter(Collider other);
    public abstract void OnTriggerStay(Collider other);
    public abstract void OnTriggerExit(Collider other);
}

アイドルの時に実行したいことを記述します
OnTrigger関連のコードも記述することができます

using UnityEngine;

public class PlayerIdleState : BaseState<PlayerStateMachine.PlayerState>
{
    bool IsTrigger = false;

    public PlayerIdleState(PlayerStateMachine.PlayerState key) : base(key)
    {
    }

    public override void EnterState()
    {
        Debug.Log("PlayerIdleEnter");
    }

    public override void ExitState()
    {
        Debug.Log("PlayerIdleExit");
        IsTrigger = false;
    }

    public override PlayerStateMachine.PlayerState GetNextState()
    {
        // Debug.Log("IdleGetNextState");

        if (IsTrigger)
        {
            return PlayerStateMachine.PlayerState.Walk;
        }
        return PlayerStateMachine.PlayerState.Idle;
    }

    public override void OnTriggerEnter(Collider other)
    {
        Debug.Log("IdleTriggerEnter");
    }

    public override void OnTriggerExit(Collider other)
    {
        Debug.Log("IdleTriggerExit");

        IsTrigger = true;
    }

    public override void OnTriggerStay(Collider other)
    {
        Debug.Log("IdleTriggerStay");
    }

    public override void UpdateState()
    {
        // Debug.Log("UpdateState");
    }
}
using UnityEngine;

public class PlayerWalkState : BaseState<PlayerStateMachine.PlayerState>
{

    bool IsTrigger = false;

    public PlayerWalkState(PlayerStateMachine.PlayerState key) : base(key)
    {
    }

    public override void EnterState()
    {
        Debug.Log("PlayerWalkEnter");
    }

    public override void ExitState()
    {
        Debug.Log("PlayerWalkExit");
        IsTrigger = false;
    }

    public override PlayerStateMachine.PlayerState GetNextState()
    {
        // Debug.Log("WalkGetNextState");
        if (IsTrigger)
        {
            return PlayerStateMachine.PlayerState.Run;
        }
        return PlayerStateMachine.PlayerState.Walk;
    }

    public override void OnTriggerEnter(Collider other)
    {
        Debug.Log("WalkTriggerEnter");
    }

    public override void OnTriggerExit(Collider other)
    {
        Debug.Log("WalkTriggerExit");

        IsTrigger = true;
    }

    public override void OnTriggerStay(Collider other)
    {
        Debug.Log("WalkTriggerStay");
    }

    public override void UpdateState()
    {
        // Debug.Log("WalkUpdateState");
    }
}
using UnityEngine;

public class PlayerRunState : BaseState<PlayerStateMachine.PlayerState>
{
    bool IsTrigger = false;

    public PlayerRunState(PlayerStateMachine.PlayerState key) : base(key)
    {
    }

    public override void EnterState()
    {
        Debug.Log("PlayerRunEnter");
    }

    public override void ExitState()
    {
        Debug.Log("PlayerRunExit");
        IsTrigger = false;
    }

    public override PlayerStateMachine.PlayerState GetNextState()
    {
        // Debug.Log("RunGetNextState");
        if (IsTrigger)
        {
            return PlayerStateMachine.PlayerState.Idle;
        }
        return PlayerStateMachine.PlayerState.Run;
    }

    public override void OnTriggerEnter(Collider other)
    {
        Debug.Log("RunTriggerEnter");
    }

    public override void OnTriggerExit(Collider other)
    {
        Debug.Log("RunTriggerExit");

        IsTrigger = true;
    }

    public override void OnTriggerStay(Collider other)
    {
        Debug.Log("RunTriggerStay");
    }

    public override void UpdateState()
    {
        // Debug.Log("RunUpdateState");
    }
}
using System.Collections.Generic;
using UnityEngine;
using System;

public class StateManager<T> : MonoBehaviour where T : Enum
{
    protected Dictionary<T, BaseState<T>> States = new Dictionary<T, BaseState<T>>();
    protected BaseState<T> CurrentState;

    protected bool IsTransitioningState = false;

    void Start()
    {
        CurrentState.EnterState();
    }

    public void Update()
    {
        T nextStateKey = CurrentState.GetNextState();

        if (!IsTransitioningState && nextStateKey.Equals(CurrentState.StateKey))
        {
            CurrentState.UpdateState();
        }
        else if (!IsTransitioningState)
        {
            TransitionToState(nextStateKey);
        }
    }

    private void TransitionToState(T stateKey)
    {
        // Debug.Log("TransitionToState");
        IsTransitioningState = true;
        CurrentState.ExitState();
        CurrentState = States[stateKey];
        CurrentState.EnterState();
        IsTransitioningState = false;
    }

    void OnTriggerEnter(Collider other)
    {
        CurrentState.OnTriggerEnter(other);
    }

    void OnTriggerStay(Collider other)
    {
        CurrentState.OnTriggerStay(other);
    }

    void OnTriggerExit(Collider other)
    {
        CurrentState.OnTriggerExit(other);
    }
}
using UnityEngine;

public class PlayerStateMachine : StateManager<PlayerStateMachine.PlayerState>
{
    public enum PlayerState
    {
        Idle,
        Walk,
        Run,
    }

    void Awake()
    {
        States.Add(PlayerState.Idle, new PlayerIdleState(PlayerState.Idle));
        States.Add(PlayerState.Walk, new PlayerWalkState(PlayerState.Walk));
        States.Add(PlayerState.Run, new PlayerRunState(PlayerState.Run));
        Debug.Log(States[PlayerState.Idle].ToString());
        CurrentState = States[PlayerState.Idle];
    }
}

参考

次の動画を参考にしています

PlantUMLクラス図

@startuml

abstract class BaseState<T>
{
    + StateKey: T
    {abstract} + EnterState(): void
    {abstract} + ExitState(): void
    {abstract} + UpdateState(): void
    {abstract} + GetNextState(): T
    {abstract} + OnTriggerEnter(other: Collider): void
    {abstract} + OnTriggerStay(other: Collider): void
    {abstract} + OnTriggerExit(other: Collider): void
}

class StateManager<T>
{
    - States: Dictionary<T, BaseState<T>>
    - CurrentState: BaseState<T>
    - IsTransitioningState: bool

    + Start(): void
    + Update(): void
    + TransitionToState(stateKey: T): void
    + OnTriggerEnter(other: Collider): void
    + OnTriggerStay(other: Collider): void
    + OnTriggerExit(other: Collider): void
}

class PlayerStateMachine
{
    + enum PlayerState
    + Awake(): void
}

class PlayerIdleState
{
    - IsTrigger: bool

    + EnterState(): void
    + ExitState(): void
    + UpdateState(): void
    + GetNextState(): PlayerStateMachine.PlayerState
    + OnTriggerEnter(other: Collider): void
    + OnTriggerStay(other: Collider): void
    + OnTriggerExit(other: Collider): void
}

class PlayerWalkState
{
    - IsTrigger: bool

    + EnterState(): void
    + ExitState(): void
    + UpdateState(): void
    + GetNextState(): PlayerStateMachine.PlayerState
    + OnTriggerEnter(other: Collider): void
    + OnTriggerStay(other: Collider): void
    + OnTriggerExit(other: Collider): void
}

class PlayerRunState
{
    - IsTrigger: bool

    + EnterState(): void
    + ExitState(): void
    + UpdateState(): void
    + GetNextState(): PlayerStateMachine.PlayerState
    + OnTriggerEnter(other: Collider): void
    + OnTriggerStay(other: Collider): void
    + OnTriggerExit(other: Collider): void
}

StateManager <|-- PlayerStateMachine
BaseState <|-- PlayerIdleState
BaseState <|-- PlayerWalkState
BaseState <|-- PlayerRunState

PlayerStateMachine "1" -- "1..*" PlayerIdleState: Contains
PlayerStateMachine "1" -- "1..*" PlayerWalkState: Contains
PlayerStateMachine "1" -- "1..*" PlayerRunState: Contains

@enduml

Project: StateMachinePoly

C#,Unity

Posted by hidepon