🦁

GetComponentを使うときはインターフェースを使おう

2020/10/11に公開

Unityの教本を読んでいると、GetComponentを使うケースをよく見ます。GetComponent自体は直接アクセスと速度的には差がほとんど無いので、それ自体は良いのですが、GetComponentする際に型を直接指定している記述を散見します。

サンプルとして、「敵が自機にぶつかったら、敵はダメージを食らう」という処理を作ってみましょう。

public class Player
{
    private void OnCollisionEnter2D(Collision2D collision)
    {
        var enemy = collision.gameObject.GetComponent<Enemy>();
        if (enemy == null) return;
        enemy.Hp -= 10;
    }
}
public class Enemy : MonoBehaviour
{
    public int Hp = 100;
}

これでとりあえず「プレイヤーが敵にぶつかったら、敵のHPを10減らす」という処理ができました。さて、実装を進めていると、ボスを作りたくなってきました。早速ボスのクラスを実装しましょう。ボスはHP以外にシールドを持っているので、シールドも付け加えます。

public class Boss : MonoBehaviour
{
    public int Hp = 100;
    public int Shield = 100;
}

では、Playerスクリプトを、Bossにダメージを与えられるよう修正しましょう。

public class Player
{
    private void OnCollisionEnter2D(Collision2D collision)
    {
        var enemy = collision.gameObject.GetComponent<Enemy>();
        if (enemy != null)
        {
            enemy.Hp -= 10;
        }

        // 追加
        var boss = collision.gameObject.GetComponent<Boss>();
        if(boss != null)
        {
            if(boss.Shield > 0)
            {
                boss.Shield -= 10;
            }
            else
            {
                boss.Hp -= 10;
            }
        }
    }
}

ここまで書けば、問題が見えてくるかと思います。「プレイヤーが敵<T>にぶつかったら、敵<T>にアクションを起こす」という書き方をしていると、クラスが増えるごとにPlayerOnCollisionEnterメソッドはどんどん肥大化していきます。

それを解消するために、インターフェイスを使います。

OnCollisionEnterの共通点を考えてみましょう。基本的に、Playerはぶつかった敵に「ダメージを与える」よう実装されています。現状では、EnemyBossの記述しか無いので拡張性がありません。例えば、木や障害物に対してダメージを与えるためには、都度コードを追記する必要があります。これを抽象化すると、「接触した敵がダメージを与えられるのであれば、ダメージを与える」ということができます。

ですので、このようなインターフェースを定義しましょう。

public interface IDamageable // ダメージが与えられるのであれば
{
    void TakeDamage(int damage); // ダメージを与える
}

「私はダメージを受けられますよ」と宣言するインターフェースになります。これをPlayerは以下ように呼び出します。

public class Player
{
    private void OnCollisionEnter2D(Collision2D collision)
    {
        var damagebale = collision.gameObject.GetComponent<IDamageable>();
        if (damagebale != null)
        {
            damagebale.TakeDamage(10);
        }
    }
}

EnemyBossは、以下のように修正されます。

public class Enemy : MonoBehaviour, IDamageable
{
    private int Hp = 100;

    public void TakeDamage(int damage)
    {
        Hp -= damage;
    }
}
public class Boss : MonoBehaviour, IDamageable
{
    private int Hp = 100;
    private int Shield = 100;

    public void TakeDamage(int damage)
    {
        if(Shield > 0)
        {
            Shield -= damage;
        }
        else
        {
            Hp -= damage;
        }
    }
}

これで、Playerはダメージを与える際、シールドを持ってるかどうかや、個々のオブジェクトが抱える処理の差異を無視することができ、その処理を分離して管理することができるようになりました。

では先ほど、Playerは障害物にダメージを与えられないとしましたが、唐突に「フェンスは壊せたほうがいいな」と思い付いたとします。その場合でも、処理の追記はPlayerに記載する必要は無く、フェンスにIDamageableを背負わせれば実装は単純です。

public class Fence : MonoBehaviour, IDamageable
{
    public void TakeDamage(int damage)
    {
        if(damage > 5)
        {
            Destroy(gameObject);
        }
    }
}

もちろんdamageを無視してもいいですが、damageを使ったとすると「5ダメージ以上の攻撃を食らったら、自分を破壊する」といった具合に実装が可能です。このようにインターフェースを使うことでPlayerクラスが知る必要のない情報を取り出し、クラスごとに分けることができる上、publicな変数をガシガシ削ることもできます。

たった3行のコードで、これほど生産性があがるのだから、やらない手はありません。
インターフェースをもっと活用しましょう!


お役に立てましたらライク、サポートしていただけますと嬉しいです!🙇🙇

Discussion