IkinokoBattle で学ぶ Unity OOP 設計パターン:第2回
テンプレートメソッドパターンと抽象クラス
シリーズ目次: IkinokoBattle で学ぶ Unity OOP 設計パターン
前の記事: 第1回 シングルトンパターン
次の記事: 第3回 委譲パターンと UnityEvent
はじめに
「プレイヤーが倒れたらゲームオーバー画面へ」「敵が倒れたら GameObject を削除」——どちらも**「倒れたときに何かする」という処理の骨格は同じ**ですが、具体的な内容は違います。
この「骨格は共通、詳細だけ差し替える」という設計を Template Method パターン(テンプレートメソッドパターン)と呼びます。IkinokoBattle では MobStatus → PlayerStatus / 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 では Start と OnDie を virtual で定義しています。
// 親クラス(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>(); // 敵独自の処理を追加
}virtual と abstract の違いは以下のとおりです。
| 修飾子 | 親クラスの実装 | 派生クラスの実装 |
|---|---|---|
virtual |
あり(デフォルト動作) | 任意で override できる |
abstract |
なし(本体を書けない) | 必ず override しなければならない |
MobStatus では OnDie を virtual にしています。「ライフゲージを消すだけでいい」という派生クラスがあっても 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 と _state は protected です。
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月