[Unity]メッセージシステムで送信先を意識せずイベント配信する(C#)

## はじめに
Unityでは特定のゲームオブジェクトの処理を呼び出すメッセージング機能として

- [SendMessage](https://docs.unity3d.com/ja/current/ScriptReference/GameObject.SendMessage.html)
- [ExecuteEvents.Execute(メッセージシステム)](https://docs.unity3d.com/ja/current/Manual/MessagingSystem.html)

が存在します。
どちらもイベント送信元が送信先ゲームオブジェクトを指定して処理を呼び出すものです。
直接メソッドを呼び出すわけではないので、送信元→送信先は疎結合？なのかもしれないですが
送信先を意識せずにイベントをブロードキャストしたい場合に使いづらいな…と感じました。

そのため`ExecuteEvents.Execute`の仕組みを利用しつつ、送信先を意識せずにイベント配信できるように拡張モジュールを作成しました。

## 環境
- Unity : 2019.3.0f6 64bit
- Editor: Visual Studio 2019 Community


## 概要
通常のメッセージシステムでは

**[受信側]**
　特定のインターフェースを継承して処理を実装
**[送信側]**
　イベント送信先(受信側)のゲームオブジェクトを取得した上で
　特定のインターフェースを指定して実行

ですが、以下のように利用できるようにしました。

**[受信側]**
　特定のイベント種別を指定してもらうように自身(別ゲームオブジェクトも可)を事前登録
**[送信側]**
　イベント送信先(受信側)を指定せずにイベント送信
　　→事前登録されたゲームオブジェクトにイベントがブロードキャストされる


## ソースコード
https://gist.github.com/zeffy1014/94f6c3c353b14c36f2cfad72deec5ad6

諸々の処理をラップ・共通化している拡張イベントハンドラがこちらです。
各種イベント情報クラスのインスタンスを生成して送信すると
最終的には`ExecuteEvents.Execute`で、事前に受信登録(`AddLitner`)されているゲームオブジェクトすべてにイベント送信されます。
そして`functor`に指定している関数(`Callback`)で、各イベント情報に対応したインターフェースが呼び出されるようになっています。


```csharp:拡張モジュール
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

/*** メッセージシステム受信用のInterface ***/
interface IEventReceiverEx : IEventSystemHandler
{
    void OnXxxEvent();                                         //引数なしIF
    void OnXxxIntParamEvent(int value);                        //int値を渡すIF
    void OnXxxMultipleParamEvent(int value1, float value2);    //int, float値を渡すIF
}

/*** イベント種別 ***/
enum SendEventType
{
    XxxEvent,
    XxxIntParamEvent,
    XxxMultipleParamEvent,

    EventNum
};

/*** イベント情報 ***/
/*** 基底クラス ***/
abstract class EventDataExBase
{
    protected SendEventType type;
    public EventDataExBase() { }
    public SendEventType GetEventType() { return type; }
}
/*** 各種イベント情報クラス ***/
class XxxEventData : EventDataExBase
{
    public XxxEventData() { type = SendEventType.XxxEvent; }
}
class XxxIntParamEventData : EventDataExBase
{
    public int Value { get; }
    public XxxIntParamEventData(int value)
    {
        type = SendEventType.XxxIntParamEvent;
        Value = value;
    }
}
class XxxMultipleParamEventData : EventDataExBase
{
    public int Value1 { get; }
    public float Value2 { get; }
    public XxxMultipleParamEventData(int value1, float value2)
    {
        type = SendEventType.XxxMultipleParamEvent;
        Value1 = value1;
        Value2 = value2;
    }
}

/*** イベント送受信モジュール ***/
class EventHandlerEx
{
    static List<GameObject>[] listnerList = new List<GameObject>[(int)SendEventType.EventNum];  // イベント受信登録者List
    static EventHandlerEx()
    {
        for (int i = 0; i < (int)SendEventType.EventNum; i++)
        {
            listnerList[i] = new List<GameObject>();
        }
    }

    // イベント受信登録
    public static void AddListner(GameObject go, SendEventType type)
    {
        listnerList[(int)type].Add(go);
    }

    // イベント送信処理
    public static void SendEvent(SendEventDataBase eventData)
    {
        // イベント種別と対応するListenerを取得
        SendEventType type = eventData.GetEventType();
        if (null == listnerList[(int)type]) return;

        // 実際の各種処理
        void Callback(IEventReceiverEx receiver, BaseEventData data)
        {
            switch (type)
            {
                case SendEventType.XxxEvent:
                    receiver.OnXxxEvent();
                    break;
                case SendEventType.XxxIntParamEvent:
                    try
                    {
                        // 派生クラスに変換してパラメータを取得
                        XxxIntParamEventData ev = (XxxIntParamEventData)eventData;
                        receiver.OnXxxIntParamEvent(ev.Value);
                    }
                    catch (InvalidCastException)
                    {
                        // イベント種別は送信側で設定しないのでダウンキャスト失敗しない想定だが一応
                        Debug.Log("Invalid Event...");
                    }
                    break;
                case SendEventType.XxxMultipleParamEvent:
                    try
                    {
                        // 派生クラスに変換してパラメータを取得
                        XxxMultipleParamEventData ev = (XxxMultipleParamEventData)eventData;
                        receiver.OnXxxMultipleParamEvent(ev.Value1, ev.Value2);
                    }
                    catch (InvalidCastException)
                    {
                        // イベント種別は送信側で設定しないのでダウンキャスト失敗しない想定だが一応
                        Debug.Log("Invalid Event...");
                    }
                    break;
                default:
                    break;
            }
            return;
        }

        // メッセージシステムで順次処理を呼び出す
        foreach (GameObject listner in listnerList[(int)type])
        {
            if (null != listner)
            {
                ExecuteEvents.Execute<IEventReceiverEx>(
                    target: listner,
                    eventData: null,
                    functor: Callback
               );
            }
        }
    }

}
```

実際に上記モジュールを用いてイベント送受信する際は以下のようにします。

```csharp:イベント送信
/*** イベント送信側(例) ***/
public class EventSenderSample : MonoBehaviour
{
    void SendXxxEvent()
    {
        int i = 100;
        float f = 0.5f;

        // 送信先を意識せずに配信
        EventHandlerEx.SendEvent(new XxxEventData());
        EventHandlerEx.SendEvent(new XxxIntParamEventData(i));
        EventHandlerEx.SendEvent(new XxxMultipleParamEventData(i, f));
    }

    /*
     * 従来メッセージシステムの記述例(送信先GameObjectとIFを特定して実行)
    public GameObject go;
    void Start()
    {
        go = GameObject.FindWithTag("xxx");
    }
    void SendXxxEvent()
    {
        int i = 100;
        float f = 0.5f;
        ExecuteEvents.Execute<IEventReceiverEx>(
            target: go,
            eventData: null,
            functor: (receiver, eventData) => receiver.OnXxxEvent()
        );
        ExecuteEvents.Execute<IEventReceiverEx>(
            target: go,
            eventData: null,
            functor: (receiver, eventData) => receiver.OnXxxIntParamEvent(i)
        );
        ExecuteEvents.Execute<IEventReceiverEx>(
            target: go,
            eventData: null,
            functor: (receiver, eventData) => receiver.OnXxxMultipleParamEvent(i, f)
        );
    }
    */


}
```

```csharp:イベント受信
/*** イベント受信側(例) ***/
public class EventReceiverSample : MonoBehaviour, IEventReceiverEx
{
    void Start()
    {
        //イベント受信登録
        EventHandlerEx.AddListner(this.gameObject, SendEventType.XxxEvent);
        EventHandlerEx.AddListner(this.gameObject, SendEventType.XxxIntParamEvent);
        EventHandlerEx.AddListner(this.gameObject, SendEventType.XxxMultipleParamEvent);
    }

    /*** イベント受信処理 ***/
    public void OnXxxEvent()
    {
        Debug.Log("XxxEvent Received.");
    }
    public void OnXxxIntParamEvent(int value)
    {
        Debug.Log("Receive XxxIntParamEvent. param:" + value);
    }
    public void OnXxxMultipleParamEvent(int value1, float value2)
    {
        Debug.Log("Receive XxxMutipleParamEvent. param:" + value1 + ", " + value2);
    }

}
```

## 余談
上記例では受信用インターフェースは1種類(`IEventReceiverEx`)ですが
通常、メッセージシステムを使用する場合は恐らくイベントの種類に応じてインターフェースを分けた上で、受信側で必要に応じてインターフェースを(多重)継承するのだと思います。
(そうすることで必要なインターフェースの実装だけ行う)

今回作成した拡張モジュールは、あくまでも1種類のみのインターフェースを想定しているので
イベントの種類が増えた場合に使い勝手が悪いかもしれません。

[C# 8.0ではインターフェースのデフォルト実装が可能](https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-8#default-interface-methods)なので、このあたりの不都合も解消されることを期待しています。。。
UnityでのC# 8.0は開発環境によってサポートされていたりいなかったり？うちの環境では未サポートだったため今後に期待です。


以上です。