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