【Unity】EventManagerを作成してシンプルなメッセージングシステムを作成しよう【ソフトリファレンス化】


:::note info
ここでは、[アダム・バフナー氏の講義](https://learn.unity.com/tutorial/create-a-simple-messaging-system-with-events#)を和訳しつつ、その背景も含めて紹介します。
:::
上記のアダム氏の設計方法を、ぜひ日本語圏にも広めたいなと思って今回の記事を執筆しました。
Unity歴１年程度の新参者なので間違いがあるかもしれませんが、その際指摘して頂けると助かります！

# 対象
<strong>` GameObject.Find("オブジェクト名") `や` [SerializeField] GameObject ` のようなハードリファレンスから脱してソフトリファレンスなプロジェクトを設計したい！ </strong>　という方にオススメです！

# 背景
###### ハードリファレンスとは？
` GameObject.Find("オブジェクトX") ` や　` GameObject.Find("オブジェクトX") `の問題点はなんでしょうか？
<strong>もしオブジェクトXに変更が加わったら、参照しているものを全て探し出してコードを修正しないといけない！</strong>というメンテナンス上の問題点があります。

せっかくのオブジェクト指向ですので、<strong>他のオブジェクトが変更が入っても大丈夫な設計</strong>にしたいですよね。
そのためには<strong>ソフトリファレンス</strong>にしないといけない。

<strong>ハードリファレンス</strong>とは、簡単に言うと` GameObject.Find("オブジェクトX") ` や　` GameObject.Find("オブジェクトX") `のように「コードに直接他のオブジェクトについて参照している、絡みついている」ことを指します。（簡単に言うと！！）

一方、<strong>ソフトリファレンス</strong>とは、ハードリファレンスとは違って、コード内で直接他オブジェクトに絡みつかずに他オブジェクトと交信することを指しています。
それを実現するのが今回のEventManagerです。

##### Eventベースの設計
ソフトリファレンス化する提案として、Eventベースでオブジェクトを設計することを提案します。
Eventベースというのは、下記のような感じです。

<strong>「プレイヤーが敵Aを倒したら次の敵Bが出現する」を実装したいとき</strong>
> 敵B（まだ未登場）：「敵に当たった」イベント待ち、スタンバイ中
> プレイヤーが敵Aを倒す
> プレイヤーオブジェクト：「敵Aが倒された」イベント発信！
> 敵B：「敵Aが倒された」イベント受信！
> 敵B：発生！！

上記なら、例えプレイヤーや敵に変更が加わっても敵Bは「敵Aが倒された」イベントさえあればまともに動作します。
それに加え、もし特殊エフェクトの発生条件を「敵Aが倒された」から「敵Aのライフが半分以下」に変えようと思ったら、敵A側のスタンバイするイベントをそのように変更できればすぐ解決します。

それでは実装してみましょう。

:::note alert
上記の例のハードリファレンスな例は、敵Aが敵Bをコードの中に組み込んでいて、敵Aが倒されたら` 敵B.SetActive(true)` するようなコーディングです。
:::

# 実装

ではさっそく、アダム氏が書いたEventManagerのコードがこちらです。（日本語化のため一部改変、コメントは筆者注）
```c#:EventManager.cs
using UnityEngine;
using UnityEngine.Events;
using System.Collections;
using System.Collections.Generic;

public class EventManager : MonoBehaviour {
    //イベント待ちを記録するDictionary
    private Dictionary <string, UnityEvent> eventDictionary;
    
    //シングルトン化処理
    private static EventManager eventManager;

    public static EventManager instance
    {
        get
        {
            if (!eventManager)
            {
                eventManager = FindObjectOfType (typeof (EventManager)) as EventManager;

                if (!eventManager)
                {
                    Debug.LogError ("このシーン内に有効なEventManagerが存在していません！");
                }
                else
                {
                    eventManager.Init (); 
                }
            }

            return eventManager;
        }
    }

    void Init ()
    {
        if (eventDictionary == null)
        {
            eventDictionary = new Dictionary<string, UnityEvent>();
        }
    }

    //イベントスタンバイ開始
    //listerに関数名を渡すことで、
    //eventNameイベントがTrigger（下記）されると関数が呼び出されます
    public static void StartListening (string eventName, UnityAction listener)
    {
        UnityEvent thisEvent = null;
        if (instance.eventDictionary.TryGetValue (eventName, out thisEvent))
        {
            thisEvent.AddListener (listener);
        } 
        else
        {
            thisEvent = new UnityEvent ();
            thisEvent.AddListener (listener);
            instance.eventDictionary.Add (eventName, thisEvent);
        }
    }

    //オブジェクトを破棄するときは
    //これでスタンバイ状態を解かないと
    //スタンバイ記録がずっと堆積していってしまいます
    public static void StopListening (string eventName, UnityAction listener)
    {
        if (eventManager == null) return;
        UnityEvent thisEvent = null;
        if (instance.eventDictionary.TryGetValue (eventName, out thisEvent))
        {
            thisEvent.RemoveListener (listener);
        }
    }

    //イベントをTriggerします
    //AddListnerで登録していた関数全てが呼び出されます
    public static void TriggerEvent (string eventName)
    {
        UnityEvent thisEvent = null;
        if (instance.eventDictionary.TryGetValue (eventName, out thisEvent))
        {
            thisEvent.Invoke ();
        }
    }
}
```

このスクリプトを、<strong>空オブジェクトにアタッチしてください</strong>

関数についてはコメント通りですが、一度流れをまとめておくと、

* AddListnerで発生させたい関数を登録して、スタンバイ。
↓
* TriggerEventでイベントを発信。
* するとAddListnerで登録していた関数が発生する。
↓
* いらなくなったらStopListnerで登録を解除（解除しなかったらずっと登録が累積してしまいます）

といった感じです。

# 使用例
アダム氏が実際にデモンストレーションで使用したコードをご紹介します。

下記のコードを空オブジェクトAにアタッチしてください。
```c#:EventTest.cs
using UnityEngine;
using UnityEngine.Events;
using System.Collections;

//AddListnerを行う方です
public class EventTest : MonoBehaviour {

    private UnityAction someListener;

    void Awake ()
    {
        someListener = new UnityAction (SomeFunction);
    }

    void OnEnable ()
    {
        //ここでAddListnerでイベントを登録しています
        EventManager.StartListening ("test", someListener);
        EventManager.StartListening ("Spawn", SomeOtherFunction);
        EventManager.StartListening ("Destroy", SomeThirdFunction);
    }

    void OnDisable ()
    {
        //事後処理を忘れずに
        EventManager.StopListening ("test", someListener);
        EventManager.StopListening ("Spawn", SomeOtherFunction);
        EventManager.StopListening ("Destroy", SomeThirdFunction);
    }

    //EventManagerに登録する関数たち

    void SomeFunction ()
    {
        Debug.Log ("Some Function was called!");
    }

    void SomeOtherFunction ()
    {
        Debug.Log ("Some Other Function was called!");
    }

    void SomeThirdFunction ()
    {
        Debug.Log ("Some Third Function was called!");
    }
}
```

次に、下記のコードをオブジェクトBにアタッチしてみてください。
```c#:EventTrigger.cs
using UnityEngine;
using System.Collections;

public class EventTriggerTest : MonoBehaviour {


    void Update () {
        if (Input.GetKeyDown ("q"))
        {
            EventManager.TriggerEvent ("test");
        }

        if (Input.GetKeyDown ("o"))
        {
            EventManager.TriggerEvent ("Spawn");
        }

        if (Input.GetKeyDown ("p"))
        {
            EventManager.TriggerEvent ("Destroy");
        }

        if (Input.GetKeyDown ("x"))
        {
            EventManager.TriggerEvent ("Junk");
        }
    }
}
```
これでプログラムを実行して、q, o, p, xキーを押すとしっかりログにメッセージが表示されるはずです。