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 を使うことで、状態を int や bool のフラグで管理する場合と比べて次の利点があります。
| 比較 | bool フラグ複数 | enum |
|---|---|---|
| 型安全 | △(不正な組み合わせが起きる) | ○(定義した状態のみ有効) |
| 読みやすさ | △(isDead && !isAttacking と冗長) |
○(_state == StateEnum.Die と明確) |
| 状態の追加 | △(フラグを増やすと組み合わせ爆発) | ○(enum に値を 1 行追加するだけ) |
| switch 対応 | △ | ○(switch (_state) で網羅性チェック可能) |
protected にすることで、派生クラス(PlayerStatus・EnemyStatus)は _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 <= 0 │ OnAttackFinished()
│ │ → 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) { ... }IsMovable と IsAttackable は現在どちらも StateEnum.Normal == _state と同じ実装ですが、将来の仕様変更に強いという利点があります。たとえば「スタン状態のときは攻撃できないが移動はできる」という仕様が加わったとき、修正するのは MobStatus のプロパティ 2 行だけで済み、PlayerController も MobAttack も変更不要です。
// 仕様変更の例:スタン状態(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 = true → GoToAttackStateIfPossible()
└─ _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月