Unityにおけるリスコフの置換原則(LSP)適用例

― 敵キャラクターの攻撃処理を安全に拡張する設計 ―


1. 背景と目的

Unityでは継承を多用してさまざまなエンティティ(キャラ、UI、アイテムなど)を表現する場面があります。しかし、誤った継承により動作や契約が壊れると、ゲーム全体の挙動に悪影響を及ぼします。

ここでは「敵キャラクターの攻撃処理」を題材に、LSPを守った実装を行います。


2. 悪い例(LSP違反)

public class Enemy : MonoBehaviour
{
    public virtual void Attack()
    {
        Debug.Log("敵が攻撃!");
    }
}

public class PassiveEnemy : Enemy
{
    public override void Attack()
    {
        // 攻撃しない敵なので、例外で未対応を表現
        throw new System.NotImplementedException("攻撃できません");
    }
}

問題点

  • Enemy は「必ず攻撃できる」という契約を持っている。
  • しかし PassiveEnemy はその契約を破って例外を投げる。
  • List<Enemy> で全体を一括処理するとクラッシュする可能性がある。

3. 良い例(LSPを守る設計)

3.1 共通インターフェースを定義

public interface IAttackable
{
    void Attack();
}

3.2 敵キャラの基本クラス

public abstract class Enemy : MonoBehaviour
{
    public string enemyName;

    public virtual void Move()
    {
        Debug.Log($"{enemyName} が移動した");
    }
}

3.3 攻撃可能な敵

public class Goblin : Enemy, IAttackable
{
    public void Attack()
    {
        Debug.Log($"{enemyName} が剣で攻撃!");
    }
}

3.4 攻撃しない敵

public class Slime : Enemy
{
    // 攻撃しないが、Move はできる
}

4. 使用例(LSPを守る処理)

public class EnemyManager : MonoBehaviour
{
    public List<Enemy> enemies;

    void Start()
    {
        foreach (var enemy in enemies)
        {
            enemy.Move();

            if (enemy is IAttackable attackable)
            {
                attackable.Attack();
            }
            else
            {
                Debug.Log($"{enemy.enemyName} は攻撃できません");
            }
        }
    }
}

実行結果の例

ゴブリンが移動した
ゴブリンが剣で攻撃!
スライムが移動した
スライムは攻撃できません

5. ポイント整理

悪い設計良い設計
継承元クラスが全機能を前提としている振る舞いごとにインターフェースで分離
子クラスが契約を破る(例外を投げる)子クラスが契約を守る
型の置換で実行時エラーが起こるどのクラスでも安全に処理できる

6. まとめ

  • Unityでも「継承だけに頼らず、役割ごとにインターフェースを定義」することで、LSPを守る設計が実現できる。
  • とくに「攻撃できる or できない」などの分岐は、IAttackableのような小さなインターフェースを使って区別することが効果的。
  • 今後の機能追加時も、既存コードを壊さずに拡張できる。

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