学習記事一覧 · Unity

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

テンプレートメソッドパターンと抽象クラス

シリーズ目次: IkinokoBattle で学ぶ Unity OOP 設計パターン
前の記事: 第1回 シングルトンパターン
次の記事: 第3回 委譲パターンと UnityEvent


はじめに

「プレイヤーが倒れたらゲームオーバー画面へ」「敵が倒れたら GameObject を削除」——どちらも**「倒れたときに何かする」という処理の骨格は同じ**ですが、具体的な内容は違います。

この「骨格は共通、詳細だけ差し替える」という設計を Template Method パターン(テンプレートメソッドパターン)と呼びます。IkinokoBattle では MobStatusPlayerStatus / EnemyStatus の継承ツリーがこのパターンを体現しています。


今回の題材:MobStatus とその派生クラス

クラス 種別 役割
MobStatus abstract class(MonoBehaviour 継承) HP 管理・ダメージ処理・状態遷移の骨格
PlayerStatus MobStatus 派生 死亡時にゲームオーバーシーンへ遷移
EnemyStatus MobStatus 派生 死亡時に 3 秒後に GameObject を Destroy

コード全文

MobStatus.cs(抽象基底クラス)

using UnityEngine;
/// <summary>
/// Mob(移動するオブジェクト、MovingObjectの略)の状態管理スクリプト
/// </summary>
public abstract class MobStatus : MonoBehaviour
{
    /// <summary>
    /// 状態の定義
    /// </summary>
    protected enum StateEnum
    {
        Normal, // 通常
        Attack, // 攻撃中
        Die     // 死亡
    }
    /// <summary>移動可能かどうか</summary>
    public bool IsMovable => StateEnum.Normal == _state;
    /// <summary>攻撃可能かどうか</summary>
    public bool IsAttackable => StateEnum.Normal == _state;
    /// <summary>ライフ最大値を返します</summary>
    public float LifeMax => lifeMax;
    /// <summary>ライフの値を返します</summary>
    public float Life => _life;
    [SerializeField] private float lifeMax = 10;   // ライフ最大値
    protected Animator _animator;
    protected StateEnum _state = StateEnum.Normal; // Mob状態
    private float _life;                           // 現在のライフ値
    protected virtual void Start()
    {
        _life = lifeMax;
        _animator = GetComponentInChildren<Animator>();
        // ライフゲージの表示開始
        LifeGaugeContainer.Instance.Add(this);
    }
    /// <summary>
    /// キャラクターが倒れたときの処理を記述します。
    /// </summary>
    protected virtual void OnDie()
    {
        // ライフゲージの表示終了
        LifeGaugeContainer.Instance.Remove(this);
    }
    /// <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;
        _state = StateEnum.Attack;
        _animator.SetTrigger("Attack");
    }
    /// <summary>可能であれば Normal の状態に遷移します。</summary>
    public void GoToNormalStateIfPossible()
    {
        if (_state == StateEnum.Die) return;
        _state = StateEnum.Normal;
    }
}

PlayerStatus.cs(プレイヤー用の派生クラス)

using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;
public class PlayerStatus : MobStatus
{
    protected override void OnDie()
    {
        base.OnDie(); // ← 親クラスの共通処理(ライフゲージ削除)を先に実行
        StartCoroutine(GoToGameOverCoroutine());
    }
    private IEnumerator GoToGameOverCoroutine()
    {
        // 3秒待ってからゲームオーバーシーンへ遷移
        yield return new WaitForSeconds(3);
        SceneManager.LoadScene("GameOverScene");
    }
}

EnemyStatus.cs(敵用の派生クラス)

using System.Collections;
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public class EnemyStatus : MobStatus
{
    private NavMeshAgent _agent;
    protected override void Start()
    {
        base.Start(); // ← 親クラスの共通処理(ライフ初期化・ゲージ登録)を先に実行
        _agent = GetComponent<NavMeshAgent>();
    }
    private void Update()
    {
        // NavMeshAgent の velocity で移動速度を取得してアニメーションに反映
        _animator.SetFloat("MoveSpeed", _agent.velocity.magnitude);
    }
    protected override void OnDie()
    {
        base.OnDie(); // ← 親クラスの共通処理(ライフゲージ削除)を先に実行
        StartCoroutine(DestroyCoroutine());
    }
    private IEnumerator DestroyCoroutine()
    {
        yield return new WaitForSeconds(3);
        Destroy(gameObject);
    }
}

ポイント解説

① abstract class — 「抽象クラス」とは何か

public abstract class MobStatus : MonoBehaviour

クラスに abstract を付けると 「そのままではインスタンス化できないクラス」 になります。new MobStatus() を書いてもコンパイルエラーです。「必ず派生クラスを作って使ってください」という宣言です。

Unity では GameObject.AddComponent<MobStatus>() もできなくなり、直接アタッチを禁止することができます。

MobStatus         ← abstract(直接 AddComponent 不可)
  ├─ PlayerStatus ← 具体クラス(AddComponent 可)
  └─ EnemyStatus  ← 具体クラス(AddComponent 可)

② virtual と override — デフォルト実装を持ちつつ上書きできる

MobStatus では StartOnDievirtual で定義しています。

// 親クラス(MobStatus)
protected virtual void Start()
{
    _life = lifeMax;
    _animator = GetComponentInChildren<Animator>();
    LifeGaugeContainer.Instance.Add(this);
}
protected virtual void OnDie()
{
    LifeGaugeContainer.Instance.Remove(this);
}

派生クラスは override で上書きします。

// 派生クラス(EnemyStatus)
protected override void Start()
{
    base.Start(); // 親の処理を実行したうえで
    _agent = GetComponent<NavMeshAgent>(); // 敵独自の処理を追加
}

virtualabstract の違いは以下のとおりです。

修飾子 親クラスの実装 派生クラスの実装
virtual あり(デフォルト動作) 任意で override できる
abstract なし(本体を書けない) 必ず override しなければならない

MobStatus では OnDievirtual にしています。「ライフゲージを消すだけでいい」という派生クラスがあっても override しなくて済むようにするためです。


③ Template Method パターンの骨格:Damage() を読む

今回のテンプレートメソッドDamage() です。

public void Damage(int damage)
{
    if (_state == StateEnum.Die) return;  // ガード:死亡済みなら無視
    _life -= damage;
    if (_life > 0) return;               // ガード:まだ生きていれば何もしない
    _state = StateEnum.Die;
    _animator.SetTrigger("Die");
    OnDie(); // ← 「死亡後の処理」をサブクラスに委ねる
}

Damage()public で外部から呼ばれますが、死亡判定・アニメーション指示という共通の骨格MobStatus が完全に管理しています。「死亡後に何をするか」という差分だけ OnDie() に切り出し、サブクラスに記述させています。

処理の流れを図で整理するとこうなります。

外部コード


Damage(int damage)  ← テンプレートメソッド(骨格)

  ├─ HP 減算
  ├─ 死亡判定
  ├─ _state = Die
  ├─ Animator.SetTrigger("Die")

  └─ OnDie()  ← ここだけサブクラスに任せる

       ├─ PlayerStatus.OnDie() → ゲームオーバーシーンへ
       └─ EnemyStatus.OnDie()3秒後に Destroy

④ base 呼び出し — 親の処理を引き継ぐ

派生クラスの OnDie は最初に base.OnDie() を呼んでいます。

protected override void OnDie()
{
    base.OnDie(); // ← ライフゲージの削除(共通処理)を必ず実行
    StartCoroutine(DestroyCoroutine()); // ← 敵固有の処理
}

base.OnDie() を呼ばないと、ライフゲージが残ったままになります。「共通処理は必ず実行してほしい」なら base を先頭で呼ぶのが定石です。逆に「共通処理の後に追加する」なら末尾で呼ぶこともあります。


⑤ protected — 子クラスだけに公開する

_animator_stateprotected です。

protected Animator _animator;
protected StateEnum _state = StateEnum.Normal;

private にすると派生クラスから見えなくなります。EnemyStatus.Update() では _animator.SetFloat(...) を呼んでいるので、protected が必要です。public にすると外部からも変更できてしまい、状態管理が崩れます。**「子クラスには見せるが、外の世界には隠す」**のが protected の役割です。

アクセス修飾子 同クラス 派生クラス 外部
private × ×
protected ×
public

全体の処理の流れを整理する

[Start]
MobStatus.Start()
  └─ _life = lifeMax
  └─ _animator = GetComponentInChildren<Animator>()
  └─ LifeGaugeContainer.Instance.Add(this)
     ↑
EnemyStatus.Start() は base.Start() の後で
  └─ _agent = GetComponent<NavMeshAgent>()
[ダメージを受けたとき]
Damage(damage) ← 外部から呼ばれる
  └─ _life -= damage
  └─ _state = Die
  └─ _animator.SetTrigger("Die")
  └─ OnDie()
        ├─ PlayerStatus: LifeGauge削除 → 3秒後 GameOverScene
        └─ EnemyStatus : LifeGauge削除 → 3秒後 Destroy(gameObject)

カスタマイズ:新しいキャラクターを追加するには

テンプレートメソッドパターンの恩恵は「新しい派生クラスを作るだけでいい」点です。ボスキャラを追加する例を考えてみましょう。

public class BossStatus : MobStatus
{
    [SerializeField] private GameObject _explosionPrefab;
    protected override void OnDie()
    {
        base.OnDie(); // ライフゲージ削除(共通処理)
        // ボス固有:爆発エフェクトを生成してからゆっくり消える
        Instantiate(_explosionPrefab, transform.position, Quaternion.identity);
        StartCoroutine(FadeAndDestroyCoroutine());
    }
    private IEnumerator FadeAndDestroyCoroutine()
    {
        yield return new WaitForSeconds(5);
        Destroy(gameObject);
    }
}

Damage() の骨格(HP 管理・死亡判定・アニメーション)は 一行も書き直さずOnDie() だけを実装すれば動きます。これがテンプレートメソッドパターンの最大のメリットです。


まとめ

キーワード 意味と役割
abstract class インスタンス化できない基底クラス。継承して使う設計を強制する
virtual メソッド デフォルト実装を持ち、派生クラスで任意に override できる
abstract メソッド 実装を持たず、派生クラスで必ず override しなければならない
override 親クラスの virtual/abstract メソッドを上書きする
base.メソッド() 親クラスの実装を引き継いで実行する
protected 同クラスと派生クラスのみアクセス可能
テンプレートメソッド 処理の骨格を親クラスに定義し、差分だけ派生クラスに書かせるパターン

IkinokoBattle では Damage() が骨格メソッドとして機能し、OnDie() に差分の処理を集約しています。「共通処理を親クラスに、差分だけ子クラスに」——この考え方は Unity の大規模開発でも繰り返し登場する重要な設計パターンです。


シリーズ目次: IkinokoBattle で学ぶ Unity OOP 設計パターン
次の記事: 第3回 委譲パターンと UnityEvent


記事に関するご意見・誤りの指摘は GitHub Issue または Twitter(@著者名)までお願いします。


最終更新:2026年4月