ステートパターンを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");
}
}
}
この設計は、ジェネリック型を活用して「ステートパターン」を実装したものです。
Player
や Enemy
が「状態」を持ち、特定の条件に応じて「状態」が変化する仕組みです。
状態遷移の制御は StateMachine
クラスが行い、具体的な状態(例: Idle
状態、Move
状態)は State
クラスから派生した PlayerIdleState
や EnemyMoveState
として実装されます。
T
を使って、プレイヤーや敵などのオーナー (Owner
) を一般化しています。Enter()
, Update()
, Exit()
を持ち、各ステートでオーバーライドされることを前提としています。Player
や Enemy
など、具体的なオーナーをステートに関連付けるための基底クラスです。T
を使用して、オーナーに対して状態遷移を管理するクラスです。_owner
: ステートマシンのオーナー(Player
や Enemy
など)を保持します。_currentState
: 現在のステートを保持します。_states
: ステートを Type
によって管理するための辞書です。AddState(State<T> state)
: ステートを追加します。ChangeState<S>()
: 現在のステートを終了し、新しいステートに遷移します。Update()
: 現在のステートの Update()
メソッドを呼び出して、毎フレーム更新処理を行います。StateMachine<Player>
を持ち、プレイヤーの状態管理を行います。PlayerIdleState
を設定し、Update()
メソッドでステートマシンの更新を呼び出します。ChangeState<S>()
メソッドでステートを変更することができます。Enter()
: Idle状態に入った際に実行されます(ログ出力)。Update()
: Space
キーが押されたら、PlayerMoveState
に遷移します。Exit()
: Idle状態から出る際に実行されます(ログ出力)。Enter()
: Move状態に入った際に実行されます(ログ出力)。Update()
: プレイヤーを前進させ、Space
キーが離されたら PlayerIdleState
に戻ります。Exit()
: Move状態から出る際に実行されます(ログ出力)。StateMachine<Enemy>
を持ち、敵の状態管理を行います。EnemyIdleState
を設定し、Update()
メソッドでステートマシンの更新を呼び出します。ChangeState<S>()
メソッドでステートを変更することができます。EnemyMoveState
に遷移します。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()
メソッドを呼び出します。このように、ジェネリック型を用いたステートパターンにより、キャラクターの状態管理をシンプルかつ効率的に行うことができます。
「Player.cs」「各Stateクラス」どちらにChangeStateメソッドの呼び出しを書くかは、設計の好みや具体的なシナリオに依存しますが、一般的なアプローチは次の通りです。
Playerクラスがキー入力を監視し、状態遷移を直接制御します。 簡潔でシンプルな場合に適しています。
ステートが自分自身で次の状態を決定し、Playerクラスがそれを意識しないようにします。
よりモジュール化されていて、ステートの切り替えロジックが各ステートに閉じ込められます。
より複雑な状態遷移ロジックに適しています。
・作業例
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へのトランジション]
Idle
と Move
の2つのステートを追加します。Idle
ステートに IdleStateBehaviour.cs
をアタッチします。Move
ステートに MoveStateBehaviour.cs
をアタッチします。Idle
ステートから Move
ステートへのトランジションを作成し、トリガー Move
を設定します。Move
ステートから Idle
ステートへのトランジションを作成し、トリガー Idle
を設定します。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
StateMachine
は currentState
というフィールドを持ち、現在アクティブな状態を保持しています。StateMachine
は ChangeState()
メソッドを持ち、異なる状態に切り替える役割を持っています。また、Update()
メソッドを呼び出して現在の状態の更新を行います。State
クラスは基本的な状態のメソッド (Enter()
、Update()
、Exit()
) を定義しており、これを基底クラスとして、具体的な状態 (IdleState
や MoveState
) が派生しています。IdleState
と MoveState
は State
を継承しており、それぞれの状態に応じた挙動を定義しています。Idle
ステートに入った時 (OnStateEnter
)、ログに「Entering Idle State」と出力します。Idle
ステートにいる間、OnStateUpdate
で特定のロジックを実行できます(ここでは何も記述されていません)。Idle
ステートを出る時 (OnStateExit
)、ログに「Exiting Idle State」と出力します。Move
ステートに入った時 (OnStateEnter
)、ログに「Entering Move State」と出力します。Move
ステートにいる間 (OnStateUpdate
)、PlayerAnimatorController
コンポーネントを取得し、プレイヤーを前進させるロジックを実行します。Move
ステートを出る時 (OnStateExit
)、ログに「Exiting Move State」と出力します。Input.GetKeyDown
) と、Animator
に Move
トリガーを設定します。Input.GetKeyUp
) と、Animator
に Idle
トリガーを設定します。これにより、スペースキーを押すとプレイヤーが前進し、離すと待機状態に戻るというアニメーションの切り替えが行われます。