インターフェース分割の原則(ISP)適用例:爆発エフェクトによるダメージシステム

1. 目的と背景

  • 問題:範囲攻撃処理で特定の型(Enemy/DestructibleBox)を個別に列挙すると、拡張性や保守性が低下する
  • 解決策:IDamageableインターフェースを活用し、どんなオブジェクトでも一括処理可能にする

2. インターフェース定義

/// <summary>
/// ダメージを受けられるオブジェクトが実装するインターフェース
/// </summary>
public interface IDamageable
{
    /// <summary>
    /// ダメージを与える
    /// </summary>
    void TakeDamage(int amount);
}

3. オブジェクト実装例

3.1. 敵キャラクター(Enemy.cs)

public class Enemy : MonoBehaviour, IDamageable
{
    public int hp = 10;

    public void TakeDamage(int amount)
    {
        hp -= amount;
        if (hp <= 0) Destroy(gameObject);
    }
}

3.2. 破壊可能な箱(DestructibleBox.cs)

public class DestructibleBox : MonoBehaviour, IDamageable
{
    public int hp = 3;

    public void TakeDamage(int amount)
    {
        hp -= amount;
        if (hp <= 0) Destroy(gameObject);
    }
}

4. Explosionスクリプト

using UnityEngine;
using System.Linq;

public class Explosion : MonoBehaviour
{
    [SerializeField] int damage = 5;
    [SerializeField] float radius = 3f;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
            Explode();
    }

    void Explode()
    {
        // 指定半径内のコライダーを取得
        Collider[] hits = Physics.OverlapSphere(transform.position, radius);

        foreach (var col in hits)
        {
            // MonoBehaviour から IDamageable を実装したものを抽出
            var damageables = col.GetComponents<MonoBehaviour>()
                                 .OfType<IDamageable>();

            foreach (var d in damageables)
            {
                d.TakeDamage(damage);
            }
        }

        // エフェクト再生など(省略)
        Destroy(gameObject);
    }
}

2Dオブジェクトで対応したい場合

最初にIDamegeableを型として取得できる場合についてみましょう

col.GetComponent<IDamageable>()の動作

col.GetComponent<IDamageable>() は、col の GameObject にアタッチされたコンポーネントの中から、IDamageable インターフェースを実装している最初の1つだけを取得します。

IDamageable target = col.GetComponent<IDamageable>();
if (target != null)
{
    target.TakeDamage(10);
}

特徴

  • 該当するコンポーネントがあれば取得され、なければ null が返されます。
  • 対象が1つであれば簡潔で効率的な書き方です。

col.GetComponents<MonoBehaviour>().OfType<IDamageable>()の動作

こちらは、アタッチされているすべての MonoBehaviour コンポーネントの中から、IDamageable を実装しているものを抽出します。

var damageables = col.GetComponents<MonoBehaviour>()
                     .OfType<IDamageable>();

foreach (var d in damageables)
{
    d.TakeDamage(10);
}

特徴

  • 1つではなく複数の対象に一括で処理が可能。
  • 複数の IDamageable 実装が存在する可能性がある場合に向いています。
  • GetComponents<IDamageable>() のようなインターフェース型の直接指定は Unity ではサポートされていないため、このような手法が必要です。

比較まとめ

方法対象数使用用途備考
GetComponent<IDamageable>()最初の1つのみ単体対象への攻撃シンプルで高速
GetComponents<MonoBehaviour>().OfType<IDamageable>()複数取得可複数対象への攻撃インターフェース抽出が必要な場合

まとめ

  • 単体にしか IDamageable が存在しない、または1体だけを攻撃対象とする場合は GetComponent<IDamageable>() で十分です。
  • 複数の IDamageable を同時に対象とする場合は GetComponents<MonoBehaviour>().OfType<IDamageable>()を使います。

使用目的に応じて、どちらの形式が適しているかを判断するとよいでしょう。

以下のように OnDrawGizmos(あるいは選択時のみ表示したい場合は OnDrawGizmosSelected)を使うと、Scene ビュー上にデバッグ用の球を描画できます。

using UnityEngine;

[ExecuteInEditMode]  // エディタ上でも描画したい場合に付ける
public class OverlapSphereDebugger : MonoBehaviour
{
    public float radius = 3f;
    public Color gizmoColor = new Color(1f, 0f, 0f, 0.5f);

    // シーン全体に常時表示したいとき
    void OnDrawGizmos()
    {
        Gizmos.color = gizmoColor;
        Gizmos.DrawWireSphere(transform.position, radius);
    }

    // オブジェクト選択時のみ表示したいとき
    void OnDrawGizmosSelected()
    {
        Gizmos.color = gizmoColor;
        Gizmos.DrawWireSphere(transform.position, radius);
    }
}

ポイント

  • [ExecuteInEditMode] をクラスに付けると、Play モードでなくてもエディタの Scene ビュー上で常に描画されます。
  • Gizmos.color で色と透明度(アルファ)を設定できます。
  • DrawWireSphere はワイヤーフレーム、DrawSphere にすると塗りつぶしの球を描画します。
  • OnDrawGizmosSelected を使うと、Hierarchy でそのオブジェクトを選択しているときだけ表示され、Scene がごちゃごちゃしません。

このスクリプトを OverlapSphere を使っているオブジェクトにアタッチすれば、Scene ビューで範囲を直感的に確認しながらデバッグできます。


5. ISPを活かすメリット

  • 汎用性:IDamageableを実装すれば、どんなオブジェクトにも一括ダメージ適用が可能
  • 拡張性:新しいダメージ対象(シールド、プレイヤーなど)を追加しても、インターフェース実装だけで対応
  • 低結合:特定型への依存や型チェック(GetComponent<Enemy>())が不要

6. 拡張例

6.1. プレイヤーへのダメージ

public class Player : MonoBehaviour, IDamageable
{
    public int hp = 20;
    public void TakeDamage(int amount)
    {
        hp -= amount;
        if (hp <= 0) Debug.Log("Player defeated");
    }
}

6.2. バリアオブジェクト

public class Shield : MonoBehaviour, IDamageable
{
    public int absorb = 5;
    public void TakeDamage(int amount)
    {
        int remaining = amount - absorb;
        absorb = Mathf.Max(0, absorb - amount);
        if (remaining > 0)
        {
            // 残りのダメージを他へ伝播するなど
        }
    }
}
訪問数 23 回, 今日の訪問数 1回