View on GitHub

adventure-cube

インターフェース

設計で使用する、中級者向けの考え方です。

「多重継承」という問題

抽象化や処理の共通化をやっていくにつれて、「羽のある動物」「哺乳類」「足が8本ある動物」みたいなくくり方を行った結果、じゃあコウモリは「羽のある動物」と「哺乳類」の両方の実装が必要だよね…みたいな状況になることがあります。

このとき、一応C++では「多重継承」という仕組みを使用して、2つのクラスから派生されるクラスを実装することができました。
※C#では多重継承はできません。

そこで、そういう処理については、インタフェースという仕組みを使用して実装するといいよ、としているのがC#です。
また、C++にはインタフェースの仕組みはありませんが、多重継承のやり方を工夫することで真似をすることはできるので、そのように実装するのが良いよ、とされています。

ゲーム中の実装から確認するinterfaceの便利な使い方

adventure-cubeでも、実装のいくつかでinterfaceを使用しています。
とくにキューブの実装については、interfaceの活用例の一つです。

その時の手法を説明していきます。

キューブの仕様

このゲームにおける、キューブの仕様を事前に伝えておく必要があります。


キューブの仕様

各キューブは必ず1つの数字を持っていて、その数字に何かの意味がある。 この数字を「フィギュア」と呼ぶ。
figure: 数字/形

数字に伴い何かの効果が発揮される。
通常攻撃やスキルなども、全部キューブの効果によって発生する。
組み合わせ次第でゲーム展開が色々変化する。

コアキューブ
中心になるキューブ。
破壊されるとキャラクターは死亡する。
自分のキャラクターのコアが破壊されるとゲームオーバーとなる。

アタックキューブ
オート攻撃をするキューブ。

スキルキューブ
スキル攻撃をするキューブ。

シールドキューブ
敵からのダメージを肩代わりしてくれるキューブ。
通常ダメージはコアに入るが、このキューブを持っていると、コアは無傷で済む。
多くの場合ダメージを肩代わりできる数がfigureになる。

パッシブキューブ
プレイヤーのステータスを底上げしたり、バフをかけたりする。


この仕様から、インタフェースを使用する理由はどこにあるでしょうか?
答えは、コアキューブ、パッシブキューブの取り扱いにあります。

コアキューブはゲームオーバー条件である者の、あらゆる”強力な”キューブ効果を持てる想定があり、
パッシブキューブの「ステータスをプラスする」処理は、あらゆるキューブで使用される可能性がありました。

つまり、この仕様をクラスで作るとき、他のキューブと組み合わせる際に多重継承(デキナイ)をする必要があるか、または複数のコンポーネントを1つのオブジェクトにつける形をとる必要があります。

今回、1つのキューブに複数のキューブクラスをつけることは想定しないつくりとしました。
なぜなら、管理上、キューブの情報は1つのオブジェクトについて1つであってほしいわけです。

それをしたい場合、キューブ本体クラス(パラメータ管理)と効果クラスに分ける必要があります。別にそれでもよかった(むしろ、組み合わせでキューブ作れる分、効果次第ではそっちのほうがいいかも)のですが、それだとインタフェース活用の実例がなくなるのと、シンプルな作りを目指したかったので、こうしています。
また、「表面上は」表示しないものの、効果上必要な処理にも使うことができます。

interface ICoreCube
{
}
interface IPassiveCube
{
  void PassiveCheck(CubeParam param);
}
interface IHookInventory
{
  bool AddCubeHook();
  bool DestroyCubeHook();
}
class HumanCore : MonoBlock, ICoreCube, IPassiveCube, IHookInventory
{
  ...
}

パッと見て、インタフェースたくさん付けすぎィ! と思ったかもしれません。
私も書いてて思いましたが、これはインターフェイス分離の原則を考えるとこれでいいといえます。

インタフェースは定義するときは単機能にします。
なので、結構もりッとした実装では付けすぎになるケースは出てくると思います。
このあたりは、それいがキモいなぁってなる場合は別のやり方を考えるなどするといいと思います。
(この辺りは、書き方や管理の方法が好きか嫌いかの世界になると思う)

その他の便利な使い方

多重継承とかじゃないけど、関心ごとを考えた便利な使い方です。
adventure-cubeでも使っていきたいけど、まだそこまで実装できてないやって部分でもある。

ターゲット指定可能クラス

interface ITarget
{
  bool SearchTarget(Vector3 from, float distance);
  void HitToTarget(好きな変数の型);
}

たとえばこういうインタフェースを用意します。
SearchTargetは、特定の場所から自分を見つけられるかを返す関数。
HitToTargetは攻撃などでインタフェースを持っているオブジェクトと判定をとりたいときに呼び出す関数とします。
判定は2Dと3D、やり方で変わると思うので、都合のいい型で渡すようにしましょう。

Unityでの最高に便利な使い方として、**インタフェースをGetComponentで拾ってくることができます。
そのため、以下のようなコードで場にいるすべてのターゲットを集めて、ターゲットを探すことができます。
※少し重いので、毎フレーム実行するとかはやめてください。

var targets = GetComponents<ITarget>();
foreach(var tgt in targets)
{
  ...処理
}

これは、MasterCubeの実装にもあります。

セーブデータ

オープンワールドゲームとかで、3Dオブジェクトの状態などを復元するときの実装って、考えるだけで面倒ですよね。
仕様変更があると、さらに大変です。

関心の分離から、おおくのひとはセーブデータクラスを別に用意して管理してます。
ただし、セーブデータの関心は、セーブデータクラスが持つべきだろうか?
という考え方もあると思います。

最近の私は後者の考え方をすることが多く、セーブロードだけじゃなく、いろいろなタイミングで次のような実装をします。

interface ISave
{
  void Save(SaveData save);
  void Load(SaveData save);
}

分かりやすいインタフェースですね。

Saveはセーブ時に自分が持っている、保存すべきパラメータを渡す関数。
Loadは逆で、自分がゲームに復帰するときに必要な変数を拾ってきて、適切な処理をする、というものです。

アクセシビリティの面で、セーブデータを使って何かを復元するときはいろいろな変数にアクセスすることが多く、
セーブデータクラスからキャラクラスに変数のアクセスすることは好ましくないことが多いです。

この形であれば、Load関数は外部からコールされることがあるものの、そこで行われる処理はクラスの内部で行うことができます。
よって、きれいな設計でオブジェクトを復元することができます。
また、関心の高いクラス内にあるので、仕様変更でセーブ対象のクラスの実装が変わった際も、セーブ処理を一緒に修正しやすいですよね。
そういう部分で便利に使うことができます。

ただし、ゲーム中のオブジェクトにまつわらないものに関しては、これは使用できないので、作る用事はないと思ってください。