C#とUnityのイベントハンドリングを並べてみる

投稿者: | 2016-11-27

前記事にも書いた通り、最近Viveアプリを作るためにUnityの勉強をしています。
まずは、キーボードやマウスと同じように、Unityイベントシステムを使いイベントドリブンベースでViveコントローラを取り扱いできるようにしたいと思っています。

そのためUnityのイベントシステムを調査しているのですが、参考書等を読んでも理解できたようなできてないようなフワっとした状態ですので、タイトルにもあるとおり、C#のイベントハンドリングと照らし合わせてその仕組みを勉強しました。

前提

UnityやC#のイベントハンドリングを理解するには、最低限デリゲートとイベントを理解しておく必要があります。
(できればラムダ式も)
以下の記事が参考になるかと思います。

C#とUnityのイベントハンドリングについてのサンプルコード

C#のサンプルコードは以下のものとします。
gist:dykarohora/CSharpEventSample.cs
なお、Unity側のコードとできるだけ対称の形にするため、あまり実用的な書き方になっていないと思います。

Unityのサンプルコードは以下のものとします。
gist:dykarohora/UnityEventSample.cs

なお、Unityのサンプルコードは下記gistを参考にしています。
gist:stramit/CustomEvents.cs

コードの比較

イベント発行時に付随させるデータ

C#、Unity共に、送信側から受信側へイベントの発生を通知する際に、イベントに関連するデータ(=イベントデータ)を一緒に送信することができます。
マウスクリックイベントを例に挙げると、クリックされたよー、という通知に加え、クリックされた座標データを一緒に送信することができる、といったイメージです。

C#ではEventArgsクラス、UnityではBaseEventDataクラスがあらかじめ用意されています。しかし、これらのクラスには有用なフィールドがほとんどありませんので、何かしらカスタムしたデータを送りたい場合には、これらのクラスを継承してオリジナルのイベントデータクラスを定義する必要があります。

C#

public class SampleEventArgs : EventArgs
{
    public int x { get; private set; }
    public int y { get; private set; }

    public SampleEventArgs(int x, int y) : base()
    {
        this.x = x;
        this.y = y;
    }
}

Unity

public class SampleEventData : BaseEventData
{
    public string SampleStr { get; private set; }
    public SampleEventData(EventSystem eventSystem, string sampleStr) : base(eventSystem)
    {
        SampleStr = sampleStr;
    }
}

カスタムしたイベントデータの定義方法には、ほとんど違いがないことがわかるかと思います。

送信側

C#ではイベント発行を実現するために、デリゲートとeventキーワードを使用します。
ます、イベントハンドラメソッドを表すデリゲートを用意します。

public delegate void SampleEventHandler(object sender, SampleEventArgs eventArgs);

送信側クラスは以下の通りです。

public class Sender
{
    event SampleEventHandler OnCalc;
    public void eventStart(int x, int y)
    {
        OnCalc(this, new SampleEventArgs(x, y));
    }

    public Sender(List<Receiver> receivers)
    {
        foreach (var receciver in receivers)
            OnCalc += receciver.Add;
    }
}

eventキーワードはデリゲート用のプロパティのようなものなのですが、プロパティと違いクラス外部からはメソッドの追加/削除のみを可能とし、クラス内部からのみデリゲート呼び出しを可能とする、といった制御をすることができます。

したがって、このデリゲートに受信側のイベントハンドラメソッドを追加し、イベント発行のタイミングに送信側からデリゲートを呼び出すことによって、受信側へのイベント通知とハンドラの実行を実現することができます。

続いて、Unityのイベントシステムでは、インタフェースとExecuteEventsクラス、BaseInputModuleを継承したクラスを使用します。
以下は受信側に実装する、イベントハンドラメソッドを定義したインタフェースです。C#でのデリゲート定義に相当します。

public interface ISampleHandler : IEventSystemHandler
{
    void OnSampleCode(SampleEventData eventData);
}

送信側クラスは以下の通りです。

public class SampleInputModule : BaseInputModule
{
    public GameObject TargetObject { get; set; }
    public override void Process()
    {
        if (TargetObject != null)
            ExecuteEvents.Execute<ISampleHandler>(TargetObject, new SampleEventData(eventSystem, "sample data"),
                (handler, eventData) => handler.OnSampleCode(ExecuteEvents.ValidateEventData<SampleEventData>(eventData)));
    }
}

こちらはC#でのSenderクラスに相当するクラスとなります。
BaseInputModuleクラスは、ユーザからの入力を管理し、入力値等に応じてイベント発行を行うクラスです。

Processメソッドがフレーム毎に呼び出されるので、このメソッド内でイベント発行を行います。
実際に受信側にイベントの通知を行うのはExecuteEventsクラスのExecuteメソッドが行います。
Executeメソッドでは引数にイベント通知の受信者とイベントデータを指定しています。

Executeメソッドの第三引数はvoid <T>(<T> , BaseEventData)のデリゲートであるEventFunctionと呼ばれるものです。Executeメソッド内で、第一引数(受信者)と第二引数(イベントデータ)が、デリゲートの第一引数と第二引数にそれぞれ渡された上で、デリゲートが呼び出されます。
(このあたりの詳細は後日ブログにまとめようと思います)

デリゲートの第一引数は型パラメータで指定したインタフェースを実装したオブジェクトなので、ラムダ式の記述の通りイベントハンドラを呼び出すことができます。

形は違えど、デリゲート呼び出しによりイベントを発行する、というのは共通しているようです。

受信側

受信者のコードはC#、Unityともにシンプルで、イベントハンドラの定義を行っているだけです。
C#では定義したデリゲートに応じたメソッドを用意します。

public class Receiver
{
    public void Add(object sender, SampleEventArgs eventArgs)
    {
        if (eventArgs != null)
        {
            Console.WriteLine(eventArgs.x + eventArgs.y);
            Console.Read();
        }
    }
}

Unityでは定義したインタフェースを実装し、メソッドを定義します。

public class SampleEventMB : MonoBehaviour, ISampleHandler
{
    public void OnSampleCode(SampleEventData eventData)
    {
        Debug.Log(eventData.SampleStr);
    }
}

イベント送信先の指定

C#ではeventキーワードを付与したデリゲートにメソッドの追加を行うことで送信先を指定していました。

foreach (var receciver in receivers)
    OnCalc += receciver.Add;

Unityでは、Executeメソッドを見てわかる通り、受信者オブジェクトそのもの引数で指定しています。

ExecuteEvents.Execute<ISampleHandler>(TargetObject, new SampleEventData(eventSystem, "sample data"),
    (handler, eventData) => handler.OnSampleCode(ExecuteEvents.ValidateEventData<SampleEventData>(eventData)));

ではどうやってその受信者オブジェクトを選ぶのかが問題になるかと思いますが、ここは入力装置等によって工夫するポイントになるかと思います。

例えばUnityが用意しているマウスやタッチ入力を管理するStandalonInputModuleでは、タッチした場所からRayを飛ばし、ヒットしたオブジェクトを送信先にする、もしくはEventSystemが管理している選択状態のオブジェクトを送信先にしています。(このあたりは別途記事にする予定)

マウスやタッチ操作、ジョイパッドだけならStandaloneInputModuleとUnityが用意している各種インタフェースで十分かと思いますが、そのほかの入力装置(モーションコントローラ、Viveコントローラ等)を使用する場合は、BaseInputModuleを継承してオリジナルのInputModuleを用意し、送信先の指定方法を実装するとよいのかな、と思いました。

まとめと今後

Unityのイベントシステムを理解するため、C#のイベントハンドリングと照らし合わせてみました。似たような仕組みとの差分を見てみると、個人的に理解しやすかったのでまとめてみました。
また、Viveコントローラ用のカスタムInputModuleを作るにあたって、Unity UIのソースが非常に参考になったので、次回以降はEventSystems周りのソースのについて整理する、かもしれません。