インターフェース分割の原則(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回
ディスカッション
コメント一覧
まだ、コメントがありません