C#
Unity
インターフェース

【Unity】C# インターフェースを使う実例 (備忘録)

概要

C#の機能にインターフェースというものがあります。
詳しい説明は先人の方の説明を読まれるのがいいと思いますが…簡単に説明すると「インターフェースはクラスが実装すべき機能を定める機能」です。
よくわからないので、実例を書いてみます。

作りたい物と仕様

作りたいものはアクションゲームで、プレイヤーは敵と戦闘します。
ここではプレイヤーが敵にダメージを与える処理を作りたいです。

仕様

Player は敵に対してダメージを与える事ができる。
敵は Enemy と Boss の二種類が存在する。(敵は追加になる可能性がある)
Enemy は HitPoint が0以下になると倒せる。(復活しない)
Boss は HitPoint を0にすると RestorableCount(復活可能な回数) が1減る。
Boss は RestorableCount が0の時に HitPoint を0にすると倒せる。

Player と Boss と Enemy の3のクラスに分かれます。
もしも、Playerクラスに「ダメージを与える処理」を実装し、BossとEnemyにHitPointを実装、BossだけにRestorableCountを実装し、それぞれ管理する…といった場合、非常に煩雑になります。
ここに「(バリアを解除するまで)ダメージを与えられない」といった類の敵が追加された場合、 Playerクラスのダメージを与える処理は「相手のクラスを判定して処理を分ける」といった事をしないといけません。

課題

敵クラスの判定に文字列やswitch文を使う場合、型安全でなくなりバグのリスクが増える。
(文字列の打ち間違いなどは、コンパイルエラーにならない)
敵を追加する場合、新規に追加した敵とPlayerの両方を保守する必要がある。煩雑である。
おそらくPlayerが神クラスになるポテンシャルを秘めている。

原因

ダメージを与える処理の実行 現在のhp ダメージ処理の管理 がクラスをまたがって分散している点。
(2つのクラスで「ダメージに関する処理」を扱っている)

インターフェース実用

では、インターフェースを使って実際に実装してゆきます。

IDamagable (インターフェース部分)

IDamagable.cs
public interface IDamagable
{
    void AddDamage(float damage);
}

「私はダメージを与えれますよ!」というルールを決めたインターフェースです。
中身はコレだけで「IDamagable」を継承したクラスは「floatを引数にしたAddDamageメソッドを持っている」という意味になります。
つまり、IDamagableを持ってるかどうかの判定を行えば、固有のAddDamageメソッドを呼び出せる事が分かります。

PlayerCharacter (インターフェースを呼ぶ側)

PlayerCharacter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerCharacter : MonoBehaviour
{
    //ダメージを与える敵をアタッチする
    public GameObject _attakTarget;
    public float _attackPoint = 10.0f;

    void Update () {
        if (Input.GetKeyDown(KeyCode.A))
        {
            // _attakTarget にセットされたオブジェクトから、IDamagable を呼ぶ
            var damagetarget = _attakTarget.GetComponent<IDamagable>();

            //IDamagable は AddDamage の処理が必須
            if (damagetarget != null)
            {
                _attakTarget.GetComponent<IDamagable>().AddDamage(_attackPoint);
            }
        }
    }
}

_attakTarget にアタッチされたゲームオブジェクトが IDamagable のインターフェースを持ってる場合、ダメージを与える事ができます。
このインターフェースの利点は、敵が追加された場合でもPlayerCharacterクラスを変更する事なく、IDamagableを持ったクラス(敵)にはダメージが与えられるという点です。
(本来は接触したときにダメージを与える~といった風にすべきですが、簡略化しました)

Enemy (インターフェースが実装されてる側)

Enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour, IDamagable
{
    public float _HitPoint = 100.0f;

    public void AddDamage(float damage)
    {
        _HitPoint -= damage;
        Debug.Log("add: " + damage + "hp: " + _HitPoint);

        if (_HitPoint <= 0)
        {
            Debug.Log("Enemyを倒した");
        }
    }
}

MonoBehaviourをカンマで区切って、IDamagableを継承させます。
Enemyの場合、AddDamageはそのまま処理されます。

BarrieEnemy (インターフェースが実装されてる側)

BarrieEnemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BarrieEnemy : MonoBehaviour, IDamagable
{
    public float _HitPoint = 100.0f;

    public bool _BarrieIsEnable = true;

    public void AddDamage(float damage)
    {
        if (_BarrieIsEnable)
        {
            Debug.Log("バリアを張っている ダメージを与えられない");
            return;
        }

        _HitPoint -= damage;
        Debug.Log("add: " + damage + "hp: " + _HitPoint);

        if (_HitPoint <= 0)
        {
            Debug.Log("BarrieEnemyを倒した");
        }
    }
}

BarrieEnemyの場合、_BarrieIsEnableがtrueだとダメージを与える事ができません。
重要なのは、Player側はダメージを与える側でしかなく、バリアの概念や、バリアに関する処理はBarrieEnemyが管理している点です。

Boss (インターフェースが実装されてる側)

Boss.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Boss : MonoBehaviour, IDamagable
{
    public float _HitPoint = 120.0f;

    public int _RestorableCount = 1;

    public void AddDamage(float damage)
    {       
        _HitPoint -= damage;
        Debug.Log("add: " + damage + "hp: " + _HitPoint);

        if (_HitPoint < 0 && _RestorableCount > 0)
        {
            _RestorableCount -= 1;
            _HitPoint = 120.0f;
        }

        if (_HitPoint < 0 && _RestorableCount <= 0)
        {
            Debug.Log("Bossを倒した");
        }
    }
}

Bossは復活回数の概念と、復活回数に応じた処理がありますが
PlayerCharacterクラスは、それを気にする事無くAddDamageを呼べばいいだけです。
switch文や文字列による判定を行わずに、EnemyとBossで挙動を分ける事ができました。

まとめ

switch文や文字列による判定をする事無く、処理を分ける事ができました。
もしもIDamagableを継承しているのにAddDamageの処理が無い場合はコンパイルエラーになります。
SOLID原則を守りやすくなるという利点があります。

ただし、注意点があります。
まずコードの再利用性がないという事。元々(分けたい!)という欲求から実装されてるのでコードが共通化できないのは当然は当然ですが、設計の段階で本当にインターフェースを分ける必要があるのか?はちゃんと考える必要があります。

Unity(というかゲーム制作)においても、インターフェースを使った方がいい場面はあるので見極めてゆきたい…設計できる自信ない…

参考リンク

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

インタフェース完全に理解した

インタフェースの命名パターンから知るインタフェースの役割

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away