Unityで学ぶインターフェース

インターフェースって難しそう、何に使うの?便利なの?
この疑問にサンプルを通して体感してみましょう

基本

ストーリー

  • プレイヤーとエネミーが戦うゲームを考えます
  • プレイヤーはエネミーにダメージを与えます
  • ダメージを与えられたエネミーにhpが減ります(どのくらい減るかはプレイヤー側でメッセージを送ります)

シーン

空のゲームオブジェクトを2つ作って、それぞれ名前をPlayer、Enemyとします
また、PlayerにはAttackerスクリプトを、EnemyにはStatusスクリプトをアタッチします

コード

Attacker.cs

エネミーオブジェクトを取得するコード
StatusスクリプトがEnemyオブジェクトにアタッチされていますので、

using UnityEngine;

public class Attacker : MonoBehaviour
{
    GameObject enemyObj;

    void Start()
    {
        enemyObj = GameObject.Find("Enemy");
    }
}

Status.cs

hpフィールドの宣言までのコード

using UnityEngine;

public class Status : MonoBehaviour
{
    int hp = 100;
}

攻撃の振る舞いを追加

プレイヤー側

プレイヤーがエネミーを攻撃して、ダメージを与える

PlayerゲームオブジェクトのAttackerスクリプトに次のコードを追加
EnemyのStatusスクリプトにDamageメソッドがあることを想定
先にこのコードを追加すると、もちろんエラーになります

Attacker.csに追加するコード

enemyObj.GetComponent<Status>().Damage(3);

エネミー側

プレイヤーからエネミーのDamagaメソッドが呼ばれ、ダメージを受ける

EnemyゲームオブジェクトのStatusスクリプトに次のコードを追加

Status.csに追加するコード

練習として、メソッド名が間違うとプレイヤー側でどのようなエラーが出るか確認してみましょう
Damage → Demegeの間違い

public void Demage(int point)
{
    hp -= point;
}

確認できたら、正しいものでも確認しましょう

public void Damage(int point)
{
    hp -= point;
}

開発を同時進行(チーム開発)することを考える

プレイヤーとエネミーを同時に作って、開発効率を上げる場合を考えましょう

困ること

  1. エネミー側のコードが先に作られている必要があります
  2. メソッド名が間違っていると(DamageがDemageになっているなど)、エラーになります
  3. プレイヤー側から見ると、エネミーにアタッチされてるEnemyスクリプトであることが条件になります
    例えば、将来、壁にもダメージを与えて壊すことができるようにする場合、大きく変更する必要があります

3.については、壁オブジェクトを作った場合でもプレイヤー側のコードを変更しなくていいような仕組みにできないのかな?ということです

改善

ここで、登場するのがインターフェースという仕組みです
早速、作ってみましょう

IDamageable.cs

ダメージを受けるという意味の名前をつけます

  • classの代わりにinterfaceとします
  • メソッドのシグネチャー(メソッドの1行目の書き方)を書きますが、ブロック({}で囲まれたところ)は省略します
    これは、このメソッドをみんなで共有しようねってことを言いたいだけなので・・・
public interface IDamageable
{
    void Damage(int point);
}

Attacker.csの変更

次のコードを変更します

enemyObj.GetComponent<Status>().Damage(3);

型パラメータ(Unityの場合、コンポーネント名(スクリプトもコンポーネントの一種)をインスタンス名へ変更
これで、エネミーのStatusクラスを先に作成しなくてもエラーになりません
つまり、プレイヤー側の担当者からは、Statusクラスの存在は見え無くなりました

enemyObj.GetComponent<IDamageable>().Damege(3);

Status.csの変更点

MonoBehaviourを継承しているところに、IDamageableインターフェースの実装を宣言します

サンプルとして、メソッド名を間違えた状態にしています
Visualstudioなどの統合開発環境でコーディングしていると、IDamageableに赤いアンダーラインが引かれ、エラーであることが表現されています

using UnityEngine;

public class Status : MonoBehaviour, IDamageable
{
    int hp = 100;

    // よく見るとメソッド名が間違い(DagmegaがDemage。DaとDeの違い)
    public void Demage(int point)
    {
        hp -= point;
    }
}

インテリセンス(VisualStudioの自動修正機能)を使って、implement interface(インターフェースの実装)を選びます

Damageメソッドが追加されました
よく見ると、正しいスペルのメソッド名で新しく作成されていますね。
コードのブロック内を入れます

修正後

using UnityEngine;

public class Status : MonoBehaviour, IDamageable
{
    int hp = 100;

    public void Damage(int point)
    {
        hp -= point;
    }
}

効果

期待されたことが実現できたのではないでしょうか

改善したかったこと

  1. エネミー側のコードが先に作られている必要があります
  2. メソッド名が間違っていると(DamageがDemageになっているなど)、エラーになります
  3. プレイヤー側から見ると、エネミーにアタッチされてるEnemyスクリプトであることが条件になります
    例えば、将来、壁にもダメージを与えて壊すことができるようにする場合、大きく変更する必要があります

どう改善出来た?

1.は、それぞれ作成していてもエラーにならなかったですね。これは、決まり事(インターフェース)に沿って作っていくメリットです
2.は、インターフェースの実装(クラス名の右に:で追加しましたね)により、強制的に正しいメソッド名が必要とすることで回避できました
3.は、クラスを特定しない(メソッド名でアクセスするだけ)なのでエネミーに限らず、アクセスできるようになりました(Stateクラスとプレイヤーには書かなかったですね。IDamageableとは書きました)

他にも使える?

今は、プレイヤー側のコードにエネミーにゲームオブジェクトが固定されていますが、ここを差し替えることで、どのようなオブジェクトでも、IDamageableが実装されていればダメージを与えることができるようになりました

エネミーの差し替えとして、Unityでわかりやすいのは、OnTriggerEnterなどのイベントですね。
引数(例えば、otherなど)から、

other.gameObject

で、接触した相手のオブジェクトの参照が入手できるので、これのGetComponentで使えば効果的面です

おまけ

これまでみてきたことは、依存性の逆転の法則と言われるいわゆる定石の1つになります
5大定石の1つですので、イメージをつかんでおきましょう

SOLID原則

SOLID原則とは、ソフトウェア設計の5つの原則の頭字語を取ったものです。ソフトウェアをより理解しやすく、より柔軟に、よりメンテナナンス性の高いものにするために考案されました。

  1. 単一責任の原則(Single Responsibility Principle)
  2. オープン・クローズドの原則(Open/closed principle)
  3. リスコフの置換原則(Liskov substitution principle)
  4. インターフェース分離の原則(Interface segregation principle)
  5. 依存性逆転の原則(Dependency inversion principle)

オブジェクト図

クラス図

プレイヤーは、エネミーのStatusに依存しています

プレイヤーもエネミーもインターフェースに対して依存しています
依存性が逆転しています

色違い

Unity

Posted by hidepon