A-Nest 2021/11/16 22:17

開発者向け - インターフェイスの使い方2

どうも、プログラマのゆっくりみかんです。
さて、前回の話の続きをしていこうかと思います。
今回の内容は、Unityの2019.3以降である事が前提となっています。

インターフェイスの使い方

実際にインターフェイスを自分で使って行こうとすると、どういう感じで使う事になるでしょうか。
前回も書いた通り、中々イメージがし辛い機能なわけですが、簡単な例を出してみましょう。

まず、uGUIのボタンがあったとします。

こんな具合ですね。
こういった感じでボタンやらスライダーやらを色々と配置してGUIをUnityでは作っていくわけですが、押しても反応して欲しく無い場合があったとします。
例えば、

//ゲームモードの定義
public enum GameMode {
	InGame,		//ゲーム中
	Talking,	//会話シーン
    Menu,		//メニュー
    Pause,		//ポーズ
}

こんな風にゲームモードの定義があって、

//ゲーム全体に係る管理マネージャ、シングルトンクラスとする
public class GameManager : SingletonObject {
	[SerializeField]
    private GameMode _currentGameMode;	//現在のゲームモード
    public GameMode CurrentGameMode => _currentGameMode;
}

こんなゲームのマネージャで現在のゲームモードを管理していたとします。
そして、ゲーム中のボタン・・・ 例えば、魔法を使うボタンとか。
そういうものがあったとして、それは会話シーンとかメニューとかポーズ画面で使えちゃ困るわけです。
そういった時、どうするか?
そのまま、普通の実装だとこうなると思います。

//魔法使用ボタン
public class SpellButton : MonoBehaviour {
	[SerializeField]
    private Button _spellButton;	//uGUIのボタンの参照

	[SerializeField]
	private SpellData _spellData;	//魔法データを持ったスクリプタブルオブジェクト

	private Enemy _targetEnemy;		//ターゲットとなる敵
	public Enemy TargetEnemy {		//公開用のターゲットとなる敵プロパティ
		get => _targetEnemy;
		set => _targetEnemy = value;
     }
    
    void Start() {
    	//onClickイベントへの登録
    	_spellButton.onClick.AddListener(() => {
        	//ゲームモードがInGameの時だけ実行
        	if(GameManager.Instance.CurrentGameMode == GameMode.InGame) {
				_spellData.CastSpell();	//呪文を唱える
            }
		});
    }
}

こんな感じですね。
さて、この実装に問題はあるでしょうか?
まあ別にこれでもいいのですが、問題は
「じゃあ他の似たようなボタンはどうするの?」
という事です。
攻撃ボタンがあったとしましょう。その時も

	//ゲームモードがInGameの時だけ実行
	if(GameManager.Instance.CurrentMode == GameMode.InGame) {
		//~何らかの攻撃処理~
	}

このコードをわざわざ挿入するのでしょうか?
じゃあ、もしゲームモードに変更が入って
「Bossモードを追加して、そっちでも使える様にしよう」
とかなった時、全てのコードを書き換えるのでしょうか?
めんどくさいし、管理が大変ですよね。
さて、考えてみましょう。
攻撃ボタンと魔法ボタンで共通した「機能」は何でしょうか?
それは
「特定の状況でのみクリック可能である事」
です。
さて、インターフェイスとはどういう機能だったでしょうか?
「クラスの機能を明示的に宣言して、共通の形で扱える様にするもの」
です。
先ほどの
「特定の状況でのみクリック可能である事」
というのは、じゃあどの様にインターフェイスとして実装すれば良いのでしょうか?

実装例

まず、
「特定の状況でのみクリック可能である事」
を示すインターフェイスを実装してみましょう。

//クリック可能かどうかを示すインターフェイス
public interface IClickable {
	public bool NowClickable { get; }	//クリック可能かどうかを示すプロパティ
}

とりあえずはこれだけでも十分です。
さて、このインターフェイスをどう実装するかというと、こうなります。

//戦闘中だけクリック可能なIClickable
public class BattleClickable : IClickable {
	public bool NowClickable => GameManager.Instance.CurrentGameMode == GameMode.InGame;
}

こんな感じですね。
では、これをどの様に実際のボタンに実装すれば良いでしょうか?

//魔法使用ボタン
public class SpellButton : MonoBehaviour {
	[SerializeField]
    private Button _spellButton;	//uGUIのボタンの参照

	[SerializeField]
	private SpellData _spellData;	//魔法データを持ったスクリプタブルオブジェクト
    
    [SerializeReference]
    private IClickable _clickable;	//クリック可能かどうか

	private Enemy _targetEnemy;		//ターゲットとなる敵
    
    void Start() {
    	//onClickイベントへの登録
    	_spellButton.onClick.AddListener(() => {
        	//クリック可能な状態か判定
        	if(_clickable.NowClickable) {
				_spellData.CastSpell();	//呪文を唱える
            }
		});
    }
}

こういう感じになります。
SerializeReferenceというのは、インターフェイスや抽象クラスだけで使える特別なシリアライズ属性です。
インターフェイスや抽象クラスではないメンバ変数にこれを付けても、特に意味はありません。
また逆もしかりで、インターフェイスにSerializeFieldを付けても無意味です。
抽象クラスは、唯一このどちらも受け付けますが、それによる使い分けに関してはまた追々。
さて、このボタンスクリプトを実装してコンポーネントとしてボタンに登録すると、どの様になるでしょうか?

こんな感じになります(SpellDataは省略しているのでインスペクタにはありません)。
さて、このインスペクタ画像の

この赤線に注目してみましょう。Nullになっています。
これは、定義が

    private IClickable _clickable;

こうなっており、ただのインターフェイスの入れ物でしかないからです。中身がからっぽなわけですね。
では、ここをクリックしてみましょう。すると、こういったメニューが出ます。

リストに、先ほどサンプルとして挙げたBattleClickableが入っていますね。
これを選ぶと

この様に、インターフェイスの中身としてBattleClickableが入ります。
すると、先ほどの様に

        	if(_clickable.NowClickable) {
				_spellData.CastSpell();	//呪文を唱える
            }

と書いた箇所のNowClickableを呼び出したら、BattleClickableの

	public bool NowClickable => GameManager.Instance.CurrentGameMode == GameMode.InGame;

このコードが呼び出され、bool値として値を返すわけです。
さて、先ほど例として挙げた
「Bossというモードを追加し、そこでも使える様にしたい」
場合はどうすれば良いでしょうか?

	public bool NowClickable =>
    	GameManager.Instance.CurrentGameMode == GameMode.InGame || 
        GameManager.Instance.CurrentGameMode == GameMode.Boss;

この様に、IClickableを継承したBattleClickableを書き換えるだけで良いのです。
さて、こういった事に対して
「クラス継承するだけでも似たような事は出来るジャン? ジャンジャン?」
と思う方もいるかも知れません。
しかし、こちらの方には

  • 一部のロジックを外に出せる
  • 簡単にバリエーションを追加できる

というメリットがありますし、無理に継承で実現しようとすると難しいケースも発生してきます。

インターフェイスのメリット

さて、これらのメリットはどういう事でしょうか?
まず、ロジックを外に出せる、というのは一言で言うと
「使い回しがしやすくなる」
という事です。
「簡単にバリエーションを追加できる」
という例も含めて、実際にコードを書いていきます。

先ほどの例のSpellButtonを
「場合によってクリック出来たり出来なかったりするボタン」
という汎用的なクラスで作りたいとしましょう。
インターフェイスを使うとこんな風に汎用化出来ます。
命名があんまり適切じゃない感じがしますが、ご容赦を。

//クリック出来たり出来なかったりするボタン
public class ClickableButton : MonoBehaviour {
	[SerializeField]
    private Button _button;	//uGUIのボタンの参照

    [SerializeReference]
    private IClickable _clickable;	//クリック可能かどうか
    
    [SerializeReference]
    private IOnClickProcess _onClick;	//OnClickの処理

    void Start() {
    	//onClickイベントへの登録
    	_button.onClick.AddListener(() => {
        	//クリック可能かどうかで分岐
        	if(_clickable.NowClickable) {
				IOnClickProcess.Process();	//何らかの処理
            }
		});
    }
}

さて、Clickableは先ほどの説明通りですね。
そこに加えて、今回はIOnClickProcessというメンバ変数が追加されています。
IOnClickProcessというインターフェイスは、中身はこんな感じです。

//OnClickの処理インターフェイス
public interface IOnClickProcess {
	public void Process();
}

この実装として

//呪文処理
public class SpellProcess : IOnClickProcess {
	[SerializeField]
    private SpellData _spellData;	//呪文データのスクリプタブルオブジェクト
    
    [SerializeField]
    private Enemy _targetEnemy;	//ターゲットの敵

	//クリック時の処理
    public void Process() {
    	_spellData.CastSpell(_targetEnemy);	//呪文を唱える
    }
}

こんな呪文のクラスや

//攻撃処理
public class AttackProcess : IOnClickProcess {
	[SerializeField]
    private Weapon _weapon;	//装備している武器
    
    [SerializeField]
    private Enemy _targetEnemy;	//ターゲットの敵

    public void Process() {
    	_weapon.Attack(_targetEnemy);	//攻撃!
    }
}

こんな実装があるとします。
するとどうでしょう、先ほどの汎用クラスのインスペクタでonClickを選択し、SpellProcessやAttackProcessを選ぶ事によって、どちらのボタンにも早変わりも出来るわけです。
しかも、無効化するタイミングも自由自在です。
IClickableインターフェイスを継承したクラスを書いて、それをインスペクタから選ぶだけです。
ボタンクリック時の処理の場合なら、攻撃、魔法以外にも特技というボタンを作りたければ、IOnClickProcessを実装したSkillProcessというクラスを実装してやれば、後はクリックして設定するだけです。
これが
「簡単にバリエーションを追加できる」
という事です。

そして、ClickableButtonというクラスは、特定のゲームでしか使えない独自の何かを含んでいるでしょうか?
最初の例だとGameModeという列挙型を実行の可否の条件にしていますが、GameModeなどは、今回作るゲームと次回作るゲームで内容が違ってきて当然です。
しかし、ClickableButtonには、そういった
「ゲームによって違う」
コードが極力含まれていません。
これはつまり、次回の作品でもそのまま使い回せる可能性が高い事を意味します。

これを通常の継承で実現出来るでしょうか?
中々難しいと思います。
なぜなら、C#においては
「継承は一つしか出来ない」
という、多重継承を許さない制約があるからです。
上手くやらないと、どこかで破綻が起こります。
それと比べ、インターフェイスはこの例外で、いくつでも継承可能なのです。

ちなみに、こういったインターフェイスを使って独自処理を追い出すやり方を
「依存性注入(Dependency Injection)」
と呼びます。このワードで検索をすると、色々ヒットするので調べてみるのも良いでしょう。

使い回ししやすいコード資産を作る為に

使い回ししやすいコードである、という事は非常に重要な事です。
毎回毎回、ゲーム独自のコードを含んでいると、そのコードを次回利用したい場合に
「これは今回は使わないから消して・・・ 今回はあれがあるから追加して・・・ 今回のこの要素は前回とは別のロジックで組んでるから、ここも書き直して・・・」
みたいな作業が発生して、結局一から書くのと大差ない、みたいになりがちです。
また、そういった作業は新しいバグを仕込みがちです。
インターフェイスを上手く使ってクラスを汎用化しておくと、ゲームに依存する部分だけをインターフェイスの実装として書けば、それで事足ります。
便利!

めんどくさい

インターフェイスの唯一の難点は、めんどくさい感じがする、という事です。
何というか、そのまま素直に書くのと違って遠回りをしている様な気分にさせられる感じがします。
しかし、こういった事はまあ慣れですし、インターフェイスを活用すればするほど、めんどくささも消えて自然にインターフェイスを活用出来る様になっていく事でしょう。

というかまあ、正直僕も
「めんどくせー」
ってなってインターフェイス化せずに書いてしまう事が多々あったりします。
実際問題、わざわざインターフェイス化する程の事でもないものをそうしたって意味がないので、使いどころ、という要素はあります。
その辺りの見極めは僕もまだまだ甘いですが、経験を積めば積むほど
「ここはインターフェイス化しておいた方が後々楽かなー」
みたいな見極めが出来る様になっていくんだと思います。

終わり

というわけで、インターフェイスを実際に使った例でした。
他にも色々プログラムのノウハウを書くかどうかは未定ですが、気が向いたらまた色々書くかも知れません。

開発する皆様、お互い頑張りましょう!
僕も頑張ってプログラミングします!

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

最新の記事

月別アーカイブ

記事を検索