学習記事一覧 · Unity

IkinokoBattle で学ぶ Unity OOP 設計パターン:第5回

状態パターン(State Pattern)

シリーズ目次: IkinokoBattle で学ぶ Unity OOP 設計パターン
前の記事: 第4回 単一責任の原則とコンポーネント設計
次の記事: 第6回 プロパティとデータ・ビュー分離


はじめに

キャラクターが「通常」「攻撃中」「死亡」という状態を持つとき、素朴に実装すると各処理の中に if / switch が散乱します。

// ❌ 状態フラグが散らばる悪い例
void Update()
{
    if (!isDead && !isAttacking) Move();
    if (!isDead && !isAttacking) ReceiveInput();
    if (!isDead) TakeDamage();
    // isAttacking が終わったら isDead を確認して... と条件が複雑化していく
}

状態が増えるたびに全てのメソッドを修正しなければならず、見落としがバグの温床になります。

IkinokoBattle では MobStatus の中に StateEnum(状態列挙型)と状態遷移メソッド群を封じ込め、外部からは「移動可能か」「攻撃可能か」だけを問い合わせる設計になっています。これが State パターンの核心です。


今回の題材

クラス 状態パターンにおける役割
MobStatus StateEnum を保持し、状態遷移を管理するコンテキスト
PlayerController IsMovable を参照して移動入力を制御する
EnemyMove IsMovable を参照して NavMeshAgent の追跡を制御する
MobAttack IsAttackable を参照して攻撃を制御する

コード全文(状態管理の核心部分)

MobStatus.cs(状態管理コンテキスト)

public abstract class MobStatus : MonoBehaviour
{
    // ─────────────────────────────────────────────────────
    // 状態の定義
    // ─────────────────────────────────────────────────────
    protected enum StateEnum
    {
        Normal, // 通常
        Attack, // 攻撃中
        Die     // 死亡
    }
    // ─────────────────────────────────────────────────────
    // 状態の公開(読み取り専用プロパティ)
    // ─────────────────────────────────────────────────────
    /// <summary>移動可能かどうか</summary>
    public bool IsMovable => StateEnum.Normal == _state;
    /// <summary>攻撃可能かどうか</summary>
    public bool IsAttackable => StateEnum.Normal == _state;
    // ─────────────────────────────────────────────────────
    // 状態フィールド(外部から直接変更させない)
    // ─────────────────────────────────────────────────────
    protected StateEnum _state = StateEnum.Normal;
    // ─────────────────────────────────────────────────────
    // 状態遷移メソッド(ガード付き)
    // ─────────────────────────────────────────────────────
    /// <summary>指定値のダメージを受けます。</summary>
    public void Damage(int damage)
    {
        if (_state == StateEnum.Die) return;       // 死亡済みなら無効
        _life -= damage;
        if (_life > 0) return;                     // 生存中なら何もしない
        _state = StateEnum.Die;                    // 死亡状態へ遷移
        _animator.SetTrigger("Die");
        OnDie();
    }
    /// <summary>可能であれば攻撃中の状態に遷移します。</summary>
    public void GoToAttackStateIfPossible()
    {
        if (!IsAttackable) return;                 // Normal 以外なら無効
        _state = StateEnum.Attack;                 // 攻撃状態へ遷移
        _animator.SetTrigger("Attack");
    }
    /// <summary>可能であれば Normal の状態に遷移します。</summary>
    public void GoToNormalStateIfPossible()
    {
        if (_state == StateEnum.Die) return;       // 死亡済みなら戻せない
        _state = StateEnum.Normal;                 // 通常状態へ遷移
    }
}

状態を参照するクラス群

PlayerController.cs(移動入力のガード)

private void Update()
{
    if (_status.IsMovable) // Normal 状態のときだけ移動入力を受け付ける
    {
        var moveValue = _move.ReadValue<Vector2>();
        _moveVelocity.x = moveValue.x * moveSpeed;
        _moveVelocity.z = moveValue.y * moveSpeed;
        _transform.LookAt(...);
    }
    else
    {
        _moveVelocity.x = 0;
        _moveVelocity.z = 0;
    }
}

EnemyMove.cs(追跡のガード)

public void OnDetectObject(Collider collider)
{
    if (!_status.IsMovable)      // Normal 以外なら追跡を止める
    {
        _agent.isStopped = true;
        return;
    }
    // Normal 状態のときだけプレイヤーを追跡する
    if (collider.CompareTag("Player")) { ... }
}

MobAttack.cs(攻撃のガード)

public void AttackIfPossible()
{
    if (!_status.IsAttackable) return; // Normal 状態のときだけ攻撃できる
    _status.GoToAttackStateIfPossible();
}

ポイント解説

① State パターンとは

GoF(Gang of Four)が定義する State パターンは「オブジェクトの内部状態が変化したとき、振る舞いを切り替える」パターンです。状態を外側から if で分岐するのではなく、状態そのものをオブジェクトの内部に持たせ、状態遷移を一か所に集約します。

IkinokoBattle の実装は GoF の古典的なクラス分割(各状態を別クラスにする)ではなく、enum と遷移メソッドで状態機械(State Machine)を表現するシンプルな変形版です。Unity の規模感にちょうどよく、プロジェクト内でよく見られる現実的なアプローチです。

【GoF の古典的 State パターン】
Context ──► IState(interface)
               ├─ NormalState
               ├─ AttackState
               └─ DieState
(各状態をクラスに分割、状態ごとにファイルが増える)
【IkinokoBattle の State パターン(enum 版)】
MobStatus(Context)
  ├─ StateEnum { Normal, Attack, Die }
  ├─ IsMovable   プロパティ(状態を読む)
  ├─ IsAttackable プロパティ(状態を読む)
  ├─ Damage()          → Die へ遷移
  ├─ GoToAttackState() → Attack へ遷移
  └─ GoToNormalState() → Normal へ遷移
1 クラスで完結、小〜中規模プロジェクトに適する)

② enum — 状態を型安全に列挙する

protected enum StateEnum
{
    Normal,
    Attack,
    Die
}

enum を使うことで、状態を intbool のフラグで管理する場合と比べて次の利点があります。

比較 bool フラグ複数 enum
型安全 △(不正な組み合わせが起きる) ○(定義した状態のみ有効)
読みやすさ △(isDead && !isAttacking と冗長) ○(_state == StateEnum.Die と明確)
状態の追加 △(フラグを増やすと組み合わせ爆発) ○(enum に値を 1 行追加するだけ)
switch 対応 ○(switch (_state) で網羅性チェック可能)

protected にすることで、派生クラス(PlayerStatusEnemyStatus)は _state を参照・変更できますが、外部クラスからは直接触れません。


③ 状態遷移を「ガード付きメソッド」で一元管理する

MobStatus の状態遷移メソッドは全て**ガード節(早期 return)**で始まります。

// Damage() のガード
if (_state == StateEnum.Die) return; // 死亡済みには効かない
// GoToAttackStateIfPossible() のガード
if (!IsAttackable) return;          // Normal でないと攻撃に移れない
// GoToNormalStateIfPossible() のガード
if (_state == StateEnum.Die) return; // 死亡からは Normal に戻れない

この設計の利点は、「何をしたら何に遷移できるか」というルールが MobStatus の中だけに書かれていることです。呼び出し側(MobAttack など)は「できれば攻撃して」と依頼するだけで、実際に遷移するかどうかの判断は MobStatus が行います。


④ 状態遷移図で全体を把握する

         ┌────────────────────┐
         │                    │
         ▼                    │
    ┌──────────┐   GoToAttackStateIfPossible()   ┌──────────┐
    │  Normal  │ ─────────────────────────────►  │  Attack  │
    └──────────┘                                 └──────────┘
         │                                            │
Damage() & life <= 0OnAttackFinished()
         │                                            │  → GoToNormalStateIfPossible()
         ▼                                            │
    ┌──────────┐                                      │
    │   Die    │ ◄────────────────────────────────────┘
    └──────────┘

         │(Die からは Normal・Attack へ戻れない)

      (終端)
遷移元 遷移先 トリガー ガード条件
Normal Attack GoToAttackStateIfPossible() IsAttackable(= Normal)のときのみ
Attack Normal GoToNormalStateIfPossible() Die でないときのみ
Normal / Attack Die Damage() life <= 0 Die でないときのみ
Die どこへも遷移できない(終端)

⑤ プロパティで「状態の意味」を抽象化する

外部クラスは _state の中身を直接チェックしません。代わりに意味のある名前のプロパティを使います。

// ❌ 状態の実装を外部に漏らす
if (_status._state == MobStatus.StateEnum.Normal) { ... }
// ✅ IkinokoBattle の実際の書き方
if (_status.IsMovable) { ... }
if (_status.IsAttackable) { ... }

IsMovableIsAttackable は現在どちらも StateEnum.Normal == _state と同じ実装ですが、将来の仕様変更に強いという利点があります。たとえば「スタン状態のときは攻撃できないが移動はできる」という仕様が加わったとき、修正するのは MobStatus のプロパティ 2 行だけで済み、PlayerControllerMobAttack も変更不要です。

// 仕様変更の例:スタン状態(Stun)を追加した場合
protected enum StateEnum
{
    Normal,
    Attack,
    Stun,  // ← 追加
    Die
}
public bool IsMovable   => _state == StateEnum.Normal || _state == StateEnum.Stun; // スタンでも動ける
public bool IsAttackable => _state == StateEnum.Normal;                             // スタン中は攻撃不可
// ↑ここだけ変更すれば、PlayerController も EnemyMove も MobAttack も変更不要

⑥ Unity Animator の StateMachine との対比

Unity の Animator には StateMachine(アニメーション状態機械)が内蔵されています。SetTrigger("Attack")SetFloat("MoveSpeed") で状態を切り替えるのはまさに State パターンの利用です。

比較 MobStatus(C# State) Animator(Unity StateMachine)
管理対象 ゲームロジックの状態 アニメーションクリップの切り替え
遷移のトリガー メソッド呼び出し Trigger / Bool / Float パラメータ
状態の取得 IsMovable などのプロパティ animator.GetCurrentAnimatorStateInfo()

IkinokoBattle では C# の状態(_state)と Animator の状態が連動しています。_state = StateEnum.Attack と同時に _animator.SetTrigger("Attack") を呼ぶことで、ロジックとビジュアルの状態が常に一致するようにしています。


全体の状態遷移を時系列で追う

[ゲーム開始]
  MobStatus._state = Normal
  IsMovable = true, IsAttackable = true
[プレイヤーが攻撃ボタンを押す]
  PlayerController.Update()
    └─ _attack.WasPressedThisFrame() = true
         └─ MobAttack.AttackIfPossible()
              └─ IsAttackable = trueGoToAttackStateIfPossible()
                   └─ _state = Attack
                   └─ Animator.SetTrigger("Attack")
                        └─ [アニメーションイベント] OnAttackStart()
                             └─ attackCollider.enabled = true
                        └─ [アニメーションイベント] OnAttackFinished()
                             └─ attackCollider.enabled = false
                             └─ (クールダウン後) GoToNormalStateIfPossible()
                                  └─ _state = Normal
[敵の攻撃でダメージを受ける]
  MobAttack.OnHitAttack()
    └─ targetMob.Damage(1)
         └─ _state != Die → ダメージ処理
         └─ _life <= 0 → _state = Die
              └─ Animator.SetTrigger("Die")
              └─ OnDie() → シーン遷移 or Destroy
[Die 以降]
  Damage() → return(無効)
  GoToAttackStateIfPossible() → return(無効)
  IsMovable = false → 移動停止
  IsAttackable = false → 攻撃不可

まとめ

キーワード 意味と役割
State パターン 内部状態を一か所に集約し、状態に応じた振る舞いをカプセル化するパターン
protected enum StateEnum 状態を型安全に列挙。protected で子クラスのみアクセス可
ガード節(早期 return) 遷移メソッドの先頭で「不正な遷移」を弾き、ルールを一元管理
IsMovable / IsAttackable 内部状態を抽象化したプロパティ。仕様変更時の修正範囲を最小化
Animator との連動 _state 変化と SetTrigger() を同時に呼び、ロジックとアニメを同期
終端状態(Die) どこへも遷移できない。ガード節でそれを保証する

MobStatus の State パターンは「状態が増えても if の嵐にならない」「どの状態遷移が許可されているかがひと目でわかる」という大きなメリットをもたらしています。プレイヤー・敵ともに同じ MobStatus の仕組みを使うことで、HP 管理と状態遷移のルールが統一されており、新しい Mob タイプを追加するときも OnDie() だけ override すれば済む設計になっています。


シリーズ目次: IkinokoBattle で学ぶ Unity OOP 設計パターン
次の記事: 第6回 プロパティとデータ・ビュー分離


最終更新:2026年4月