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

  • 35
    いいね
  • 0
    コメント

Unityの教本を読んでいると、GetComponentを使うケースをよく見る。
最近試してみて知ったが、GetComponent自体は直接アクセスと速度的には大差ないので、それはいいのだけれど、なぜか型を直接指定していることが多い。
例のごとく中級者向けの記事なので、初心者の皆さんはコードの効率とかよりゲームを完成することを目指すとよいでしょう。

サンプルとして、敵にぶつかったらダメージを食らう、という処理を作ってみよう。

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

いろいろと問題があるが、これでとりあえず「プレイヤーが敵にぶつかったら、敵のHPを10減らす」という処理ができた。

さて、実装を進めていると、ボスを作りたくなった。早速ボスのクラスを実装する。
ボスはHP以外にシールドを持っているので、シールドも付け加える。

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

では、PlayerをBossにダメージを与えられるよう修正しよう。

Player.cs
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;
            }
        }
    }
}

ここまで書けば、問題がわかるだろうか。「プレイヤーが敵にぶつかったら、それぞれにアクションを起こす」という書き方をしていると、クラスが増えるごとにPlayerのOnCollisionEnterメソッドがどんどん肥大化していくのだ。

ではそれをどう解消するか、インターフェースを使うのである。
OnCollisionEnterの共通点はなんだろうか。基本的に、Playerはぶつかった敵に「ダメージを与える」ことができる。ただ、EnemyとBossだけを切り抜いているのだから、例えば木などの障害物にはダメージが与えられないと考えらえる。なので「ダメージを与えることのできる敵に、ダメージを与える」ということもできる。
そこで、このようなインターフェースを定義する。

IDamageable.cs
public interface IDamageable
{
    void TakeDamage(int damage); // ダメージを受ける
}

「私はダメージを受けられますよ」と宣言するインターフェースだ。
これをPlayerはこのように呼ぶことになる。

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

EnemyとBossは、以下のように修正される。

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

    public void TakeDamage(int damage)
    {
        Hp -= damage;
    }
}
Boss.cs
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を背負わせればいいのだ。

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

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

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