敵AIに「巡回」「追跡」「攻撃」と状態を足していったら、Updateメソッドがif-elseの迷路になった——状態を持つキャラクターを作ると、誰もが一度はこの「ネスト地獄」に迷い込みます。状態が5つを超えたあたりで、もう自分でも読めません。
この混沌を整理する古典にして最強の道具が Stateパターン(ステートマシン ) です。核心はただ一つ、「状態そのものをクラスにする」こと。この記事では、敵AIを題材に、IStateインターフェースを使ったクリーンなステートマシンの実装を解説します。
この記事でわかること
if-elseの肥大化がなぜ起きるのか、Stateパターンが何を解決するのか- Context / IState / 具象状態クラスという3つの構成要素
OnEnter/OnUpdate/OnExitを持つ敵AIステートマシンの実装- 状態を足しても、既存の状態の動きを書き換えずに済む拡張性の正体
動作確認環境: Unity 6 (6000.x)
ネスト地獄とStateパターンの解決策
プレイヤーキャラクターや敵AIは、ゲームの状況に応じて様々な「状態(ステート)」を持ちます。例えば、敵AIは「巡回状態」「プレイヤー追跡状態」「攻撃状態」「待機状態」「逃走状態」などを切り替えながら行動します。まず、これをif-elseで書いた場合と、Stateパターンで書いた場合の構造を見比べてください。

左の書き方が、悪名高い「巨大Update」です。
// 悪い例:巨大な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";
}
}
// ... さらに状態が増えていく
}
状態が増えるたびにelse ifが伸び、遷移条件が絡み合い、1箇所の修正が別の状態を壊すようになります。
このような「状態に応じた振る舞いの違い」をエレガントに管理するのが Stateパターン です。核心は、「状態」そのものをクラスとして表現し、状態ごとの振る舞いをそのクラスのメソッドとして実装することです。そして、状態を持つ本体(コンテキスト)は、現在の状態オブジェクトへの参照を保持し、処理をその状態オブジェクトに「委譲」します。
Stateパターンの構成要素
Stateパターンは、主に3つの要素で構成されます。

- Context (コンテキスト): 状態を持つ本体。この例では敵AIキャラクター。現在の状態オブジェクト (
IState) への参照を保持し、状態遷移の責任を持ちます。 - IState (インターフェース): すべての状態クラスが実装すべき共通のインターフェースを定義します。例えば、
OnEnter()(その状態に入った時の処理)、OnUpdate()(その状態での毎フレームの処理)、OnExit()(その状態から出る時の処理)といったメソッドを定義します。 - Concrete State (具象状態):
IStateインターフェースを実装した具体的な状態クラス。PatrolState,ChaseState,AttackStateなどがこれにあたります。各クラスは、その状態における具体的な振る舞いを実装します。
補足:
OnEnter→OnUpdate→OnExitという3点セットは、「部屋に入るとき・部屋にいる間・部屋を出るとき」と考えると覚えやすいです。アニメーション開始は入室時、移動処理は在室中、エフェクトの後始末は退室時——と、処理の置き場所が自然に決まります。
Unityでの実装例:敵AIのステートマシン
ここからは、実際に動く敵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; }
}
EnemyAIのUpdateが「現在の状態に丸投げするだけ」の数行になった点に注目してください。状態ごとの複雑さは、それぞれの状態クラスに閉じ込められました。
Stateパターンのメリット
このパターンの最大のご利益は、状態を足しても、既存の状態の動きを書き換えずに済むことです。

- 関心の分離: 各状態のロジックが、それぞれのクラスに完全に分離されます。
EnemyAIクラスは、状態ごとの詳細な振る舞いを知る必要がなくなります。 - 拡張性: 新しい状態(例:
FleeState- 逃走状態)を追加したい場合、IStateを実装した新しいクラスを作り、どの状態から移るか(入口の条件)を書き足すだけで済みます。既存の状態の動きは書き換えずに済むので、影響を最小限に抑えられます。 - 可読性と保守性:
if-else