概要
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 (インターフェース部分)
public interface IDamagable
{
void AddDamage(float damage);
}
「私はダメージを与えれますよ!」というルールを決めたインターフェースです。
中身はコレだけで「IDamagable」を継承したクラスは「floatを引数にしたAddDamageメソッドを持っている」という意味になります。
つまり、IDamagableを持ってるかどうかの判定を行えば、固有のAddDamageメソッドを呼び出せる事が分かります。
PlayerCharacter (インターフェースを呼ぶ側)
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 (インターフェースが実装されてる側)
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 (インターフェースが実装されてる側)
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 (インターフェースが実装されてる側)
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(というかゲーム制作)においても、インターフェースを使った方がいい場面はあるので見極めてゆきたい…設計できる自信ない…