ScriptableObjectを利用したイベントシステムの作成

# ScriptableObjectを利用したイベントシステムの作成

ScriptableObjectを利用したイベントシステムを作成します。
https://unity.com/ja/how-to/architect-game-code-scriptable-objects
上記先駆者の「ScriptableObjectをイベント管理のフラグとして活用するイベントシステム」を参考にしつつ、今回はイベントの発生フラグだけでなく、イベント発生時の処理そのものをUnityEventとしてScriptableObjectに格納するアプローチでイベントシステムを作成します。

## コード
```csharp
//イベントの処理を格納しフラグとしても活用できるScriptableObject
using System.Linq;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using System.Reflection;
[CreateAssetMenu(fileName ="EventDeta",menuName ="Create EventDeta_ScriptableObject")]
public class GameEvent : ScriptableObject,IGameEvent
{
    readonly List<GameEventWaiting> _eventWaitings = new List<GameEventWaiting>();
    [SerializeField,Header( "イベント関連プレハブをアタッチしてください。")]
    GameObject[] _eventPrefabs;
    [TextArea, Header("イベントの補足説明"),SerializeField] string _descriptionText;
    [SerializeField,Header(
        "⚠引数のない関数だけ定義してください⚠" + "\n" +
        "ScriptableObject側で登録がない場合は" +"\n"+
        "EventWaiting側に登録されたUnityEventを実行します" + "\n" +
        "処理がなくても枠がある場合はGameEvent側を実行しようとするので注意" + "\n" +
        "EventWaiting側を実行したければEmptyにすること")]
    UnityEvent _staticevents =null;
    /// <summary>
    /// イベント発生通知
    /// </summary>
    public void EventOccurrence()
    {
        foreach (var waiting in _eventWaitings)
        {
            try
            {
                if(_staticevents.GetPersistentEventCount() == 0)
                {
                    waiting.OnEventOccur();
                }
                else
                {
                    waiting.OnEventOccur(ConvertEvent(_staticevents));
                }
            }
            catch(System.Exception e) 
            {
                UnityEngine.Debug.LogError(e.StackTrace+_staticevents);
                throw;
            }
        }
    }
    
    //静的UnityEventを動的に変換
    UnityEvent ConvertEvent(UnityEvent staticEvent)
    {
        UnityEvent dynamicEvent = new UnityEvent();
        dynamicEvent.RemoveAllListeners(); 
        int length = staticEvent.GetPersistentEventCount();
        for (int i = 0; i < length; ++i)
        {
            UnityAction action = Delegate.CreateDelegate
                (typeof(UnityAction),
                SceneTargetObject(i, staticEvent),
                EventMethodInfo(i,staticEvent)) as UnityAction;
            dynamicEvent.AddListener(action);
        }
        return dynamicEvent;
    }

    //静的UnityEventに結びつけられたプレハブと同名のシーン上オブジェクトを取得
    object SceneTargetObject(int count, UnityEvent staticEvent)
    {
        if(staticEvent.GetPersistentTarget(count).GetType()==this.GetType())
        {
            return staticEvent.GetPersistentTarget(count);
        }
        try
        {
            var sceneobj = GameObject.Find(staticEvent.GetPersistentTarget(count).name);
            var obj = sceneobj.GetComponent<MonoBehaviour>();
            return obj;
        }
        catch(System.Exception e) 
        {
            UnityEngine.Debug.Log(e + "\n" +
                "イベントに定義されたプレハブと同名のオブジェクトがシーンに存在しません");
            return null;
        }
    }

    //動的デリゲートを生成するためのメソッドインフォ取得
    MethodInfo EventMethodInfo(int count,UnityEvent staticEvent)
    {
        var methods = staticEvent.GetPersistentTarget(count).GetType()
            .GetMethods(BindingFlags.Public |
            BindingFlags.Instance |
            BindingFlags.FlattenHierarchy);
        var info = methods.FirstOrDefault(c =>
        {
            return
            c.Name == staticEvent.GetPersistentMethodName(count) &&
            c.GetParameters().Length == 0;
        });

        return info;
    }
    public void Contract(GameEventWaiting waitlist)
    {
        _eventWaitings.Add(waitlist);
    }

    public void Termination(GameEventWaiting waitlist)
    {
        _eventWaitings.Remove(waitlist);
    }
}
```
```csharp
//イベントの発生を通知される側のコンポーネント
using UnityEngine;
using UnityEngine.Events;

public class GameEventWaiting : MonoBehaviour
{
    [SerializeField] GameEvent[] _gameEvents;
    [SerializeField] UnityEvent _notificateEvents;
    private void OnEnable()
    {
        foreach (GameEvent gameEvent in _gameEvents)
        {
            gameEvent.Contract(this);
        }
    }
    private void OnDisable()
    {
        foreach(GameEvent gameEvent in _gameEvents)
        {
            gameEvent.Termination(this);
        }
    }
    /// <summary>
    /// イベント発生（実動作）
    /// </summary>
    /// <param name="event">GameEvent側に定義されたUnityEvent</param>
    public void OnEventOccur(UnityEvent @event)
    {
        @event.Invoke();
    }
    /// <summary>
    /// 通知待ち側のUnityEventを実行
    /// </summary>
    public void OnEventOccur()
    {
        _notificateEvents.Invoke();
    }
}

```
既存のイベントシステムでは、ScriptableObjectは単にイベント発生のフラグとして使用されていました。しかし、この記事ではその使い方を拡張し、イベント発生時の処理そのものをUnityEventとしてScriptableObjectに格納します。
また、ScriptableObjectをイベント発生のフラグとして活用する既存の使い方にも対応しています。

## イベントシステムの新しい運用方法

### UnityEventの格納・変換
既存の活用方法に関しては割愛しますので、先駆者様の記事をご覧になってください。
https://unity.com/ja/how-to/architect-game-code-scriptable-objects


ScriptableObjectにシリアライズされたUnityEventは全て静的メソッドになってしまいます。その状態でUnityEventを実行されても、UnityEventに登録されたプレハブに作用するのみでシーン上に影響を与えるには別途シーン上のインスタンスを取得する必要があります。
ですので、イベント実行時にCreateDelegate関数を利用し、静的UnityEventを全てシーン上同名オブジェクトにバインドした動的UnityEventへ変換する機能を作成しました。
理由は後述しますが引数の無い関数にのみ対応しています。

```csharp
//静的UnityEventを動的に変換
    UnityEvent ConvertEvent(UnityEvent staticEvent)
    {
        UnityEvent dynamicEvent = new UnityEvent();
        dynamicEvent.RemoveAllListeners(); 
        int length = staticEvent.GetPersistentEventCount();
        for (int i = 0; i < length; ++i)
        {
            UnityAction action = Delegate.CreateDelegate
                (typeof(UnityAction),
                SceneTargetObject(i, staticEvent),
                EventMethodInfo(i,staticEvent)) as UnityAction;
            dynamicEvent.AddListener(action);
        }
        return dynamicEvent;
    }
```
### シーン上インスタンスの取得
CreateDelegate関数を用いて動的デリゲートを作成するためのobjectをSceneTargetObject関数で取得します。
その仕様上、シーンには必ず静的UnityEventに登録されたプレハブと完全に同名のオブジェクトが存在しなければなりません。
```csharp
object SceneTargetObject(int count, UnityEvent staticEvent)
    {
        if(staticEvent.GetPersistentTarget(count).GetType()==this.GetType())
        {
            return staticEvent.GetPersistentTarget(count);
        }
        try
        {
            var sceneobj = GameObject.Find(staticEvent.GetPersistentTarget(count).name);
            var obj = sceneobj.GetComponent<MonoBehaviour>();
            return obj;
        }
        catch(System.Exception e) 
        {
            UnityEngine.Debug.Log(e + "\n" +
                "イベントに定義されたプレハブと同名のオブジェクトがシーンに存在しません");
            return null;
        }
    }
```
### 関数情報の取得
CreateDelegate関数を用いて動的デリゲートを作成するためのMethodInfoをEventMethodInfo関数で取得します。
引数付関数の適用を許可するとこの際に同名で引数の異なるオーバーロード関数を併用できなくなり利便性が悪くなるのでこのイベントシステムでScriptableObjectに登録できる関数は引数なしの関数に限定しています。
```csharp
MethodInfo EventMethodInfo(int count,UnityEvent staticEvent)
    {
        var methods = staticEvent.GetPersistentTarget(count).GetType()
            .GetMethods(BindingFlags.Public |
            BindingFlags.Instance |
            BindingFlags.FlattenHierarchy);
        var info = methods.FirstOrDefault(c =>
        {
            return
            c.Name == staticEvent.GetPersistentMethodName(count) &&
            c.GetParameters().Length == 0;
        });

        return info;
    }
```

### 既存の運用方法との併用

当然、既存のフラグとしてだけの運用方法も可能です。
また、ScriptableObject側にUnityEventの処理が登録されていない場合、GameEventWaitingコンポーネント側にシリアライズされたUnityEventを実行するので柔軟なイベント管理が可能です。
当然GameEventWaitingコンポーネント側にシリアライズされたUnityEventには引数の制限などはありません。

```csharp
/// <summary>
    /// イベント発生通知
    /// </summary>
    public void EventOccurrence()
    {
        foreach (var waiting in _eventWaitings)
        {
            try
            {
                if(_staticevents.GetPersistentEventCount() == 0)
                {
                    waiting.OnEventOccur();
                }
                else
                {
                    waiting.OnEventOccur(ConvertEvent(_staticevents));
                }
            }
            catch(System.Exception e) 
            {
                UnityEngine.Debug.LogError(e.StackTrace+_staticevents);
                throw;
            }
        }
    }

```

## まとめ

以上、UnityでScriptableObjectを利用したイベントシステムの作成について解説しました。既存のフラグとしてだけの運用方法から、ScriptableObjectに処理のデータコンテナとしての役割を持たせることで、より複雑なイベントシステムを効率的に管理することができます。