2012年9月19日水曜日

Unity&C#ではインターフェースが使えない?

 次のような状況を考える。
  • Aという動作を行えるオブジェクトと、Bという動作を行えるオブジェクトがある 
  • 重複したオブジェクトもある。
  • Bの動作の可否に関わらず、Aの動作が可能なオブジェクトを1カ所に格納し、それらに適宜、動作を呼び出す
  • Bも同様に格納し動作を呼び出す
Unityではコンポーネントの型による検索機能があるので、AとBをコンポーネント(MonoBeheaviorを継承したclass)とすることで、こういった要求に応えることができる。重複する場合には両方をゲームオブジェクトにアタッチすることで、分かりやすい解決となる。

 しかし、両方の動作を行える性質を持ちながら、それぞれ派生した動作を定義したいという要求が発生した場合どうするのだろうか。
 恐らくUnityの作法としては、Aの派生クラス、Bの派生クラスを定義し、2つをアタッチするのが正解なのだが、これではスクリプトファイルが一気に2つ増えてしまう。また性質を統合しているという意味を明確に示すためにも、1つのスクリプトとしてまとめたい。

 ところが、C#では多重継承が使えない。これは厄介なダイヤモンド継承と、それを回避するための複雑な手順を省きたいという考えからなのだろう。実際上記の例をそのまま多重継承できてしまうと、MonoBehaviorをA、Bそれぞれから2つ継承することになってしまう。

 今回はこういった経緯で...
C#の仕様である、インターフェースを使用して多重継承を行う方法を模索してみた。

 さて、前提条件としてA、Bというコンポーネントが存在した。コードとしては次のようになる。

// script file : ComponentA ------------------------
public class ComponentA : MonoBehavior
{
        public void funcA()
        {
                // do something
        }
}

// script file : ComponentB ------------------------
public class ComponentB : MonoBehavior
{
        public void funcB()
        {
                // do something
        }
}

 C#では多重継承が使えないが、インターフェースは多重継承が可能だ。しかも、1つのクラスと、複数のインターフェースを継承したクラスを定義することが許されている。

 まずA、Bそれぞれに対応するインターフェースを用意する。これはファイルを分ける必要はないので、それぞれのファイルにでも書き足せばいいだろう。

// script file : ComponentA ------------------------
        :
        :
public interface InterfaceA
{
        public void callA();
}

// script file : ComponentB ------------------------
        :
        :
public interface InterfaceB
{
        public void callB();
}

 インターフェースに定義されたメソッド宣言は、テンプレートでいうインターフェースのようなもの(謎)で、これを継承したクラスは同型、同名のメソッドを定義する必要がある。

 そしてUnity&C#で多重継承を模擬したクラスは以下のようなものだ。

// script file : DeriveAB ---------------------------
public class DeriveAB : MonoBehavior, InterfaceA, InterfaceB
{
        ComponentA implA;
        ComponentB implB;

        public void callA()
        {
                implA.funcA();
        }

        public void callB()
        {
                implB.funcB();
        }

        void Start()
        {
                implA = new ComponentA();
                implB = new ComponentB();
        }
}

 果てしなく、ださい。かつ幾つか致命的なトレードオフがあることを説明しなければならない。
  • このようにComponentA、ComponentBの実体をスクリプト内で生成した場合、2つのコンポーネントはUnityの検索対象に入らない。検索したい場合、スタート関数内でgameObjectにコンポーネントとしてアタッチしてやる必要がある。
  • Unityエディタはインターフェース型をインスペクターに表示しない。従って手動で参照を設定することができない。
  • スクリプト内ではインターフェース型を扱うことができるが、検索して得られるComponentAはDeriveABの基底クラスというわけではなく、ただの実体。多態性が存在しない。したがって、DeriveABを検索し、インターフェース型に変換した参照をとらなえれば、派生した動作を行うことはできない。
絶望的である。だが、それぞれのインターフェースの生成時に、別途用意した静的クラスの配列などに参照を格納することはできる。あるオブジェクトの子供のコンポーネントを検索、といった事をするには、相応の前準備が必要となるが、全検索だけなら許容範囲内の手間だろう。

 無い物ねだりだが、Unityにインターフェース型の検索機能がつけば万々歳である。もしくは、せめてインスペクターで表示してくれれば、細かな関連性の設定に使えるのではないだろうか。

 そのようなことがなければ、セオリー通りおとなしく派生クラスを2つ作り、お互いの参照を持たせるのが得策であるようだ。

0 件のコメント:

コメントを投稿