ステートパターンをUnityで使用する方法として以下2つを取り上げます
- ジェネリックを使用する方法
- Animation ControllerでStateMachineBehaviourを使用する方法
ステートパターンは、オブジェクトがその内部状態に応じて異なる動作をするためのデザインパターンです。
ジェネリックを使用したステートパターン
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");
}
}
}
ジェネリックを使用したステートパターンについて
この設計は、ジェネリック型を活用して「ステートパターン」を実装したものです。
Player や Enemy が「状態」を持ち、特定の条件に応じて「状態」が変化する仕組みです。
状態遷移の制御は StateMachine クラスが行い、具体的な状態(例: Idle 状態、Move 状態)は State クラスから派生した PlayerIdleState や EnemyMoveState として実装されます。
クラス図の説明
-
State<T> クラス
- ジェネリック型
Tを使って、プレイヤーや敵などのオーナー (Owner) を一般化しています。 - 抽象メソッド
Enter(),Update(),Exit()を持ち、各ステートでオーバーライドされることを前提としています。 PlayerやEnemyなど、具体的なオーナーをステートに関連付けるための基底クラスです。
- ジェネリック型
-
StateMachine<T> クラス
- ジェネリック型
Tを使用して、オーナーに対して状態遷移を管理するクラスです。 _owner: ステートマシンのオーナー(PlayerやEnemyなど)を保持します。_currentState: 現在のステートを保持します。_states: ステートをTypeによって管理するための辞書 です。AddState(State<T> state): ステートを追加します。ChangeState<S>(): 現在のステートを終了し、新しいステートに遷移します。Update(): 現在のステートのUpdate()メソッドを呼び出して、毎フレーム更新処理を行います。
- ジェネリック型
-
Player クラス
- プレイヤーキャラクターを表すクラスです。
StateMachine<Player>を持ち、プレイヤーの状態管理を行います。 - 初期状態として
PlayerIdleStateを設定し、Update()メソッドでステートマシンの更新を呼び出します。 ChangeState<S>()メソッドでステートを変更することができます。
- プレイヤーキャラクターを表すクラスです。
-
PlayerIdleState クラス
- プレイヤーの「Idle(待機)」状態を表すステートクラスです。
Enter(): Idle状態に入った際に実行されます(ログ出力)。Update():Spaceキーが押されたら、PlayerMoveStateに遷移します。Exit(): Idle状態から出る際に実行されます(ログ出力)。
-
PlayerMoveState クラス
- プレイヤーの「Move(移動)」状態を表すステートクラスです。
Enter(): Move状態に入った際に実行されます(ログ出力)。Update(): プレイヤーを前進させ、Spaceキーが離されたらPlayerIdleStateに戻ります。Exit(): Move状態から出る際に実行されます(ログ出力)。
-
Enemy クラス
- 敵キャラクターを表すクラスで、
StateMachine<Enemy>を持ち、敵の状態管理を行います。 - 初期状態として
EnemyIdleStateを設定し、Update()メソッドでステートマシンの更新を呼び出します。 ChangeState<S>()メソッドでステートを変更することができます。
- 敵キャラクターを表すクラスで、
-
EnemyIdleState クラス
- 敵の「Idle(待機)」状態を表すステートクラスです。
- 一定確率で
EnemyMoveStateに遷移します。
-
EnemyMoveState クラス
- 敵の「Move(移動)」状態を表すステートクラスです。
- 移動しながら、一定確率で
EnemyIdleStateに遷移します。
ジェネリックの利点
- 再利用性:
State<T>クラスやStateMachine<T>クラスはジェネリック型を使っているため、PlayerでもEnemyでも同じロジックを使い回すことができます。これにより、状態管理のロジックがコードの重複を避けつつシンプルに保たれます。 - 柔軟性: プレイヤーや敵以外のキャラクターに対しても簡単に状態遷移のロジックを適用できます。
クラス間の関係
PlayerクラスとEnemyクラスはそれぞれStateMachine<Player>とStateMachine<Enemy>を持っています。StateMachine<Player>はPlayerIdleStateとPlayerMoveStateの2つの状態を管理し、StateMachine<Enemy>はEnemyIdleStateとEnemyMoveStateの状態を管理します。- 各状態(
PlayerIdleState,PlayerMoveState,EnemyIdleState,EnemyMoveState)はState<T>を継承しており、それぞれのオブジェクトの状態遷移を個別に定義しています。
動作の流れ
- 初期状態の設定:
PlayerやEnemyはそれぞれ最初にIdleStateに設定されます。 - 状態遷移: プレイヤーは
Spaceキーを押すと移動状態(MoveState)に遷移し、キーを離すと再びIdleStateに戻ります。敵は一定確率で移動状態に遷移し、また一定確率で待機状態に戻ります。 - 状態管理:
StateMachine<T>クラスが現在の状態を管理し、遷移のタイミングでEnter(),Update(),Exit()メソッドを呼び出します。
このように、ジェネリック型を用いたステートパターンにより、キャラクターの状態管理をシンプルかつ効率的に行うことができます。
・ChangeStateメソ ッドの呼び出しについて
「Player.cs」「各Stateクラス」どちらにChangeStateメソッドの呼び出しを書くかは、設計の好みや具体的なシナリオに依存しますが、一般的なアプローチは次の通りです。
- Player.csに書く場合:
Playerクラスがキー入力を監視し、状態遷移を直接制御します。 簡潔でシンプルな場合に適しています。
- 各Stateクラスに書く場合:
ステートが自分自身で次の状態を決定し、Playerクラスがそれを意識しないようにします。
よりモジュール化されていて、ステートの切り替えロジックが各ステートに閉じ込められます。
より複雑な状態遷移ロジックに適しています。
Animation ControllerでStateMachineBehaviourを使用した例
・作業例