インターフェース分割の原則(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);
    }

    // デバッグ用にシーンビューに範囲の球を描画
    void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, radius);
    }
}
using UnityEngine;
using System.Linq;

public class Explosion : MonoBehaviour
{
    [SerializeField] int damage = 5;
    [SerializeField] float radius = 3f;
    // エフェクト再生用プレハブとサウンドクリップ
    [SerializeField] ParticleSystem explosionEffectPrefab;
    [SerializeField] AudioClip explosionSoundClip;

    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);
            }
        }

                // パーティクルエフェクトとサウンドを再生
        if (explosionEffectPrefab != null)
        {
            var fx = Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity);
            fx.Play();
        }
        if (explosionSoundClip != null)
        {
            AudioSource.PlayClipAtPoint(explosionSoundClip, transform.position);
        }
        Destroy(gameObject);
    }

    // デバッグ用にシーンビューに範囲の球を描画
    void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, radius);
    }
}

5. 2D オブジェクト対応例

// Physics2D 用にシグネチャを変更
void Explode2D()
{
    Collider2D[] hits = Physics2D.OverlapCircleAll(transform.position, radius);

    foreach (var col in hits)
    {
        var damageables = col.GetComponents<MonoBehaviour>()
                             .OfType<IDamageable>();

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

6. OnTriggerEnter によるダメージ処理

using UnityEngine;

public class DamageZone : MonoBehaviour
{
    [SerializeField] int damage = 5;

    void OnTriggerEnter(Collider other)
    {
        // Unity 2020.1 以降で利用可能なインターフェース取得
        if (other.TryGetComponent<IDamageable>(out var dmg))
        {
            dmg.TakeDamage(damage);
        }

        // 複数実装を考慮する場合
        // foreach (var d in other.GetComponents<MonoBehaviour>().OfType<IDamageable>())
        //     d.TakeDamage(damage);
    }
}

2D 物理の場合はメソッド名を OnTriggerEnter2D(Collider2D other) に変更するだけで動作します。


7. ISP を活かすメリット

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

8. 拡張例

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

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

8.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)
        {
            // 残りのダメージを他へ伝播するなど
        }
    }
}

9. SOLID 原則適合について

9.1 単一責任原則 (SRP)

  • Explosion クラスが「範囲検出・ダメージ適用」と「エフェクト再生」を担っているため、演出再生のロジックを別クラスに切り出すとより適合します。

9.2 開放閉鎖の原則 (OCP)

  • 新しいダメージ対象を追加しても Explosion を修正せずに対応可能。OCP を満たしています。

9.3 リスコフの置換原則 (LSP)

  • IDamageable を実装する全クラスが共通契約 (TakeDamage(int)) を遵守し、どの実装でも置換して使用して問題ありません。

9.4 インターフェース分割の原則 (ISP)

  • IDamageable は「ダメージを受ける機能」に特化しており、必要な機能だけを分割した設計で ISP に適合しています。

9.5 依存性逆転の原則 (DIP)

  • Explosion が具象クラスではなく IDamageable に依存しているため、抽象への依存を実現。さらに厳密に DI コンテナやファクトリ経由で注入する設計も検討可能です。

以上のとおり、メインの ISP をはじめ、SOLID 原則全体に沿った設計例となっています。

訪問数 9 回, 今日の訪問数 1回