Unity_Lesson

ステートパターンをUnityで使用する方法として以下2つを取り上げます

ステートパターンは、オブジェクトがその内部状態に応じて異なる動作をするためのデザインパターンです。


ジェネリックを使用したステートパターン


State.cs


namespace StatePattern_Generic
{
    public abstract class State<T>
    {
        protected T Owner;

        public State(T owner)
        {
            Owner = owner;
        }

        public abstract void Enter();
        public abstract void Update();
        public abstract void Exit();
    }
}



StateMachine.cs


using System.Collections.Generic;

namespace StatePattern_Generic
{
    public class StateMachine<T>
    {
        private T _owner;
        private State<T> _currentState;
        private Dictionary<System.Type, State<T>> _states = new Dictionary<System.Type, State<T>>();

        public StateMachine(T owner)
        {
            _owner = owner;
        }

        public void AddState(State<T> state)
        {
            var type = state.GetType();
            if (!_states.ContainsKey(type))
            {
                _states[type] = state;
            }
        }

        public void ChangeState<S>() where S : State<T>
        {
            if (_currentState != null)
            {
                _currentState.Exit();
            }

            var type = typeof(S);
            if (_states.ContainsKey(type))
            {
                _currentState = _states[type];
                _currentState.Enter();
            }
        }

        public void Update()
        {
            if (_currentState != null)
            {
                _currentState.Update();
            }
        }
    }
}




Player.cs


using UnityEngine;

namespace StatePattern_Generic
{
    public class Player : MonoBehaviour
    {
        private StateMachine<Player> _stateMachine;

        void Start()
        {
            _stateMachine = new StateMachine<Player>(this);
            _stateMachine.AddState(new PlayerIdleState(this));
            _stateMachine.AddState(new PlayerMoveState(this));

            _stateMachine.ChangeState<PlayerIdleState>();
        }

        void Update()
        {
            _stateMachine.Update();
        }

        public void ChangeState<S>() where S : State<Player>
        {
            _stateMachine.ChangeState<S>();
        }
    }
}



PlayerIdleState.cs

using UnityEngine;

namespace StatePattern_Generic
{
    public class PlayerIdleState : State<Player>
    {
        public PlayerIdleState(Player owner) : base(owner) { }

        public override void Enter()
        {
            Debug.Log("Player Entering Idle State");
        }

        public override void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Owner.GetComponent<Player>().ChangeState<PlayerMoveState>();
            }
        }

        public override void Exit()
        {
            Debug.Log("Player Exiting Idle State");
        }
    }
}



PlayerMoveState.cs

using UnityEngine;

namespace StatePattern_Generic
{
    public class PlayerMoveState : State<Player>
    {
        public PlayerMoveState(Player owner) : base(owner) { }

        public override void Enter()
        {
            Debug.Log("Player Entering Move State");
        }

        public override void Update()
        {
            Owner.transform.Translate(Vector3.forward * Time.deltaTime);

            if (Input.GetKeyUp(KeyCode.Space))
            {
                Owner.GetComponent<Player>().ChangeState<PlayerIdleState>();
            }
        }

        public override void Exit()
        {
            Debug.Log("Player Exiting Move State");
        }
    }
}



Enemy.cs



using UnityEngine;

namespace StatePattern_Generic
{
    public class Enemy : MonoBehaviour
    {
        private StateMachine<Enemy> _stateMachine;

        void Start()
        {
            _stateMachine = new StateMachine<Enemy>(this);
            _stateMachine.AddState(new EnemyIdleState(this));
            _stateMachine.AddState(new EnemyMoveState(this));

            _stateMachine.ChangeState<EnemyIdleState>();
        }

        void Update()
        {
            _stateMachine.Update();
        }

        public void ChangeState<S>() where S : State<Enemy>
        {
            _stateMachine.ChangeState<S>();
        }
    }
}



EnemyIdleState.cs


namespace StatePattern_Generic
{
    public class EnemyIdleState : State<Enemy>
    {
        public EnemyIdleState(Enemy owner) : base(owner) { }

        public override void Enter()
        {
            Debug.Log("Enemy Entering Idle State");
        }

        public override void Update()
        {
            // 敵のIdleロジック
            if (Random.Range(0, 100) < 5)
            {
                Owner.GetComponent<Enemy>().ChangeState<EnemyMoveState>();
            }
        }

        public override void Exit()
        {
            Debug.Log("Enemy Exiting Idle State");
        }
    }
}




EnemyMoveState.cs


namespace StatePattern_Generic
{
    public class EnemyMoveState : State<Enemy>
    {
        public EnemyMoveState(Enemy owner) : base(owner) { }

        public override void Enter()
        {
            Debug.Log("Enemy Entering Move State");
        }

        public override void Update()
        {
            Owner.transform.Translate(Vector3.forward * Time.deltaTime);

            // 偶然の確率でIdle状態に戻る
            if (Random.Range(0, 100) < 5)
            {
                Owner.GetComponent<Enemy>().ChangeState<EnemyIdleState>();
            }
        }

        public override void Exit()
        {
            Debug.Log("Enemy Exiting Move State");
        }
    }
}



classDiagram class State~T~ { - T Owner + Enter()* + Update()* + Exit()* } class StateMachine~T~ { - T _owner - State~T~ _currentState - Dictionary<Type, State~T~> _states + AddState(State~T~ state) + ChangeState~S~()* + Update()* } class Player { + ChangeState~S~()* + Start() + Update() } class PlayerIdleState { + Enter()* + Update()* + Exit()* } class PlayerMoveState { + Enter()* + Update()* + Exit()* } class Enemy { + ChangeState~S~()* + Start() + Update() } class EnemyIdleState { + Enter()* + Update()* + Exit()* } class EnemyMoveState { + Enter()* + Update()* + Exit()* } State~T~ <|-- PlayerIdleState State~T~ <|-- PlayerMoveState State~T~ <|-- EnemyIdleState State~T~ <|-- EnemyMoveState Player --> StateMachine~Player~ Enemy --> StateMachine~Enemy~ StateMachine~Player~ --> PlayerIdleState StateMachine~Player~ --> PlayerMoveState StateMachine~Enemy~ --> EnemyIdleState StateMachine~Enemy~ --> EnemyMoveState



ジェネリックを使用したステートパターンについて

この設計は、ジェネリック型を活用して「ステートパターン」を実装したものです。
PlayerEnemy が「状態」を持ち、特定の条件に応じて「状態」が変化する仕組みです。
状態遷移の制御は StateMachine クラスが行い、具体的な状態(例: Idle 状態、Move 状態)は State クラスから派生した PlayerIdleStateEnemyMoveState として実装されます。

クラス図の説明

  1. **State クラス**
    • ジェネリック型 T を使って、プレイヤーや敵などのオーナー (Owner) を一般化しています。
    • 抽象メソッド Enter(), Update(), Exit() を持ち、各ステートでオーバーライドされることを前提としています。
    • PlayerEnemy など、具体的なオーナーをステートに関連付けるための基底クラスです。
  2. **StateMachine クラス**
    • ジェネリック型 T を使用して、オーナーに対して状態遷移を管理するクラスです。
    • _owner: ステートマシンのオーナー(PlayerEnemy など)を保持します。
    • _currentState: 現在のステートを保持します。
    • _states: ステートを Type によって管理するための辞書です。
    • AddState(State<T> state): ステートを追加します。
    • ChangeState<S>(): 現在のステートを終了し、新しいステートに遷移します。
    • Update(): 現在のステートの Update() メソッドを呼び出して、毎フレーム更新処理を行います。
  3. Player クラス
    • プレイヤーキャラクターを表すクラスです。StateMachine<Player> を持ち、プレイヤーの状態管理を行います。
    • 初期状態として PlayerIdleState を設定し、Update() メソッドでステートマシンの更新を呼び出します。
    • ChangeState<S>() メソッドでステートを変更することができます。
  4. PlayerIdleState クラス
    • プレイヤーの「Idle(待機)」状態を表すステートクラスです。
    • Enter(): Idle状態に入った際に実行されます(ログ出力)。
    • Update(): Space キーが押されたら、PlayerMoveState に遷移します。
    • Exit(): Idle状態から出る際に実行されます(ログ出力)。
  5. PlayerMoveState クラス
    • プレイヤーの「Move(移動)」状態を表すステートクラスです。
    • Enter(): Move状態に入った際に実行されます(ログ出力)。
    • Update(): プレイヤーを前進させ、Space キーが離されたら PlayerIdleState に戻ります。
    • Exit(): Move状態から出る際に実行されます(ログ出力)。
  6. Enemy クラス
    • 敵キャラクターを表すクラスで、StateMachine<Enemy> を持ち、敵の状態管理を行います。
    • 初期状態として EnemyIdleState を設定し、Update() メソッドでステートマシンの更新を呼び出します。
    • ChangeState<S>() メソッドでステートを変更することができます。
  7. EnemyIdleState クラス
    • 敵の「Idle(待機)」状態を表すステートクラスです。
    • 一定確率で EnemyMoveState に遷移します。
  8. EnemyMoveState クラス
    • 敵の「Move(移動)」状態を表すステートクラスです。
    • 移動しながら、一定確率で EnemyIdleState に遷移します。


ジェネリックの利点


クラス間の関係


動作の流れ

  1. 初期状態の設定: PlayerEnemy はそれぞれ最初に IdleState に設定されます。
  2. 状態遷移: プレイヤーは Space キーを押すと移動状態(MoveState)に遷移し、キーを離すと再び IdleState に戻ります。敵は一定確率で移動状態に遷移し、また一定確率で待機状態に戻ります。
  3. 状態管理: StateMachine<T> クラスが現在の状態を管理し、遷移のタイミングで Enter(), Update(), Exit() メソッドを呼び出します。

このように、ジェネリック型を用いたステートパターンにより、キャラクターの状態管理をシンプルかつ効率的に行うことができます。



・ChangeStateメソッドの呼び出しについて

「Player.cs」「各Stateクラス」どちらにChangeStateメソッドの呼び出しを書くかは、設計の好みや具体的なシナリオに依存しますが、一般的なアプローチは次の通りです。

Playerクラスがキー入力を監視し、状態遷移を直接制御します。 簡潔でシンプルな場合に適しています。

ステートが自分自身で次の状態を決定し、Playerクラスがそれを意識しないようにします。
よりモジュール化されていて、ステートの切り替えロジックが各ステートに閉じ込められます。
より複雑な状態遷移ロジックに適しています。





Animation ControllerでStateMachineBehaviourを使用した例

・作業例

flowchart LR
    A[Animator Controllerを作成] --> B[Idleステートを追加]
    A --> C[Moveステートを追加]
    B --> D[IdleStateBehaviourをアタッチ]
    C --> E[MoveStateBehaviourをアタッチ]
    D --> F[トリガー Moveを設定]
    E --> G[トリガー Idleを設定]
    F --> H[IdleからMoveへのトランジション]
    G --> I[MoveからIdleへのトランジション]


1. UnityエディタでAnimator Controllerの作成

2. StateMachineBehaviourのアタッチ

3. トランジションの設定


IdleStateBehaviour.cs


using UnityEngine;

namespace StatePattern_AnimationController
{
    public class IdleStateBehaviour : StateMachineBehaviour
    {
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            Debug.Log("Entering Idle State");
        }

        override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            // Idle state logic
        }

        override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            Debug.Log("Exiting Idle State");
        }
    }
}



MoveStateBehaviour.cs


using UnityEngine;

namespace StatePattern_AnimationController
{
    public class MoveStateBehaviour : StateMachineBehaviour
    {
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            Debug.Log("Entering Move State");
        }

        override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            // Move state logic
            var player = animator.GetComponent<PlayerAnimatorController>();
            if (player != null)
            {
                player.transform.Translate(Vector3.forward * Time.deltaTime);
            }
        }

        override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            Debug.Log("Exiting Move State");
        }
    }
}



PlayerAnimatorController.cs


using UnityEngine;

namespace StatePattern_AnimationController
{
    public class PlayerAnimatorController : MonoBehaviour
    {
        private Animator _animator;

        void Start()
        {
            _animator = GetComponent<Animator>();
        }

        void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                _animator.SetTrigger("Move");
            }

            if (Input.GetKeyUp(KeyCode.Space))
            {
                _animator.SetTrigger("Idle");
            }
        }
    }
}



・状態遷移に関する基本的な関係性

classDiagram
    class StateMachine {
        +currentState : State
        +ChangeState() : void
        +Update() : void
    }

    class State {
        +Enter() : void
        +Update() : void
        +Exit() : void
    }

    class IdleState {
        +Enter() : void
        +Update() : void
        +Exit() : void
    }

    class MoveState {
        +Enter() : void
        +Update() : void
        +Exit() : void
    }

    StateMachine --> State : "currentState"
    State <|-- IdleState
    State <|-- MoveState

説明


各スクリプトの役割

1. IdleStateBehaviour.cs

2. MoveStateBehaviour.cs

3. PlayerAnimatorController.cs

これにより、スペースキーを押すとプレイヤーが前進し、離すと待機状態に戻るというアニメーションの切り替えが行われます。