概要
プレイヤーキャラクターや敵AIは、ゲームの状況に応じて様々な「状態(ステート)」を持ちます。例えば、敵AIは「巡回状態」「プレイヤー追跡状態」「攻撃状態」「待機状態」「逃走状態」などを切り替えながら行動します。これらの状態遷移を、巨大なUpdateメソッドの中にif-elseやswitch文をネストさせて実装しようとすると、コードはすぐさま複雑怪奇になり、新しい状態を追加したり、既存の挙動を修正したりするのが非常 に困難になります。
// 悪い例:巨大なUpdateメソッド
void Update()
{
if (state == "Patrol")
{
// 巡回処理
if (CanSeePlayer())
{
state = "Chase";
}
}
else if (state == "Chase")
{
// 追跡処理
if (IsInAttackRange())
{
state = "Attack";
}
else if (!CanSeePlayer())
{
state = "Patrol";
}
}
else if (state == "Attack")
{
// 攻撃処理
if (!IsInAttackRange())
{
state = "Chase";
}
}
// ... さらに状態が増えていく
}
このような「状態に応じた振る舞いの違い」をエレガントに管理するためのオブジェクト指向設計パターンが、Stateパターンです。Stateパターンの核心は、「状態」そのものをクラスとして表現し、状態ごとの振る舞いをそのクラスのメソッドとして実装することです。そして、状態を持つ本体(コンテキスト)は、現在の状態オブジェクトへの参照を保持し、処理をその状態オブジェクトに「委譲」します。
Stateパターンの構成要素
Stateパターンは、主に3つの要素で構成されます。
- Context (コンテキスト): 状態を持つ本体。この例では敵AIキャラクター。現在の状態オブジェクト (
IState) への参照を保持し、状態遷移の責任を持ちます。 - IState (インターフェース): す べての状態クラスが実装すべき共通のインターフェースを定義します。例えば、
OnEnter()(その状態に入った時の処理)、OnUpdate()(その状態での毎フレームの処理)、OnExit()(その状態から出る時の処理)といったメソッドを定義します。 - Concrete State (具象状態):
IStateインターフェースを実装した具体的な状態クラス。PatrolState,ChaseState,AttackStateなどがこれにあたります。各クラスは、その状態における具体的な振る舞いを実装します。
Unityでの実装例:敵AIのステートマシン
Step 1: IStateインターフェースの定義
まず、すべての状態クラスの設計図となるインターフェースを定義します。
// IState.cs
public interface IState
{
// この状態に入った時に一度だけ呼ばれる
void OnEnter(EnemyAI context);
// この状態である間、毎フレーム呼ばれる
void OnUpdate();
// この状態から出る時に一度だけ呼ばれる
void OnExit();
}
Step 2: 具象状態クラスの実装
次に、具体的な状態クラスをそれぞれ作成します。各状態クラスは、自分自身のロジックと、他の状態へ遷移する条件のチェックを行います。
// PatrolState.cs
using UnityEngine;
public class PatrolState : IState
{
private EnemyAI enemy;
public void OnEnter(EnemyAI context)
{
this.enemy = context;
Debug.Log("巡回状態に移行");
// 巡回アニメーションを開始するなどの処理
}
public void OnUpdate()
{
// 巡回ロジックをここに実装
// プレイヤーを発見したら、追跡状態に遷移する
if (enemy.CanSeePlayer())
{
enemy.ChangeState(new ChaseState());
}
}
public void OnExit()
{
// 巡回アニメーションを停止するなどの処理
}
}
// ChaseState.cs
using UnityEngine;
public class ChaseState : IState
{
private EnemyAI enemy;
public void OnEnter(EnemyAI context)
{
this.enemy = context;
Debug.Log("追跡状態に移行");
}
public void OnUpdate()
{
// 追跡ロジック(プレイヤーを追いかける)をここに実装
// 攻撃範囲内に入ったら、攻撃状態に遷移
if (enemy.IsInAttackRange())
{
enemy.ChangeState(new AttackState());
}
// プレイヤーを見失ったら、巡回状態に戻る
else if (!enemy.CanSeePlayer())
{
enemy.ChangeState(new PatrolState());
}
}
public void OnExit() { }
}
// AttackState, etc... も同様に実装
Step 3: Contextクラスの実装
最後に、状態を管理する本体であるEnemyAIクラスを実装します。このクラスは、現在の状態を保持し、Update処理を現在の状態オブジェクトに委譲します。
// EnemyAI.cs
using UnityEngine;
public class EnemyAI : MonoBehaviour
{
private IState currentState;
void Start()
{
// 初期状態を設定
ChangeState(new PatrolState());
}
void Update()
{
// 現在の状態のUpdate処理を呼び出す
if (currentState != null)
{
currentState.OnUpdate();
}
}
// 状態を切り替えるメソッド
public void ChangeState(IState nextState)
{
// 現在の状態があれば、終了処理を呼び出す
if (currentState != null)
{
currentState.OnExit();
}
// 新しい状態に切り替え、初期化処理を呼び出す
currentState = nextState;
currentState.OnEnter(this);
}
// 状態クラスから使われるヘルパーメソッド群
public bool CanSeePlayer() { /* プレイヤーが見えるかどうかの判定ロジック */ return false; }
public bool IsInAttackRange() { /* 攻撃範囲内かどうかの判定ロジック */ return false; }
}
Stateパターンのメリット
- 関心の分離: 各状態のロジックが、それぞれのクラスに完全に分離されます。
EnemyAIクラスは、状態ごとの詳細な振る舞いを知る必要がなくなります。 - 拡張性: 新しい状態(例:
FleeState- 逃走状態)を追加したい場合、IStateを実装した新しいクラスを作成するだけで済み、既存のコードへの影響を最小限に抑えられます。 - 可読性と保守性:
if-elseのネスト地獄がなくなり、コードが非常にクリーンで読みやすくなります。各状態の振る舞いを修正したい場合も、対応するクラスを修正するだけです。
まとめ
Stateパターンは、複雑な状態遷移を持つオブジェクトの振る舞いを、整理され、拡張可能で、保守しやすい形で実装するための非常に強力な設計パターンです。
- 状態をクラスとして表現し、状態ごとの振る舞いをそのクラス内にカプセル化する。
- Context(本体)は、現在の状態オブジェクトへの参照を持ち、処理をそのオブジェクトに委譲する。
- 状態遷移のロジックは、各状態クラスが自身で管理する。
キャラクターAI、プレイヤーの複雑なアクション(通常時、泳ぎ中、はしご昇り中など)、UIのモーダルウィンドウの管理など、ゲーム開発の様々な場面で応用できます。Updateメソッドがifやswitchで肥大化し始めたら、Stateパターンの導入を検討する良いサインです。