MessagePipe(+Zenject)でとりあえずイベントの送受信してみた

Cysharpが公開したMessagePipeですが「個人的にUniTask, UniRx同様に使いこなしたい！」
ということで調べた内容を少しずつ記事にしていきます

https://github.com/Cysharp/MessagePipe

以下の開発環境でMessagePipeを使用しています

- Unity
- Zenject
- MessgePipe v1.6.1

`MessagePipeとは？`
という所は他の方が素晴らしい記事を挙げられているのでここでは書きません
`Zenject`についても同じく

以下の記事がMessagePipeについて詳しく載せられています！

https://qiita.com/yoship1639/items/32ca894bda3b0db54a7b


今回はZenject, Unity環境でとりあえずイベントを送受信を行う。
という部分のみの記事です

## UnityTestRunnerで触ってみる
いきなりですが「覚えるためには実践する」ということで
とりあえずUnityTestRunnerでいじって見ることにします

MessagePipeとZenjectの組み合わせでUnityTestRunnerを実行してみました

```C#
using NUnit.Framework;
using MessagePipe;
using Zenject;
using UnityEngine;


public class Test
{
    // イベント送る方
    public class Publisher
    {
        [Inject] private IPublisher<MyEvent> _publisher;

        public void Send(MyEvent ev) =>
            _publisher.Publish(ev);
    }

    // イベント受け取る方
    public class Subscriber
    {
        [Inject] private ISubscriber<MyEvent> _subscriber;

        public void Setup() =>
            _subscriber.Subscribe(x => Debug.Log($"{x.Message}"));
    }

    // 送るイベント
    public class MyEvent 
    { 
        public string Message; 
    }


    private DiContainer _container;

    [Test]
    public void SimpleTest()
    {
        _container = new DiContainer();
            
        _container.BindMessageBroker<MyEvent>(_container.BindMessagePipe());

        // イベントを受ける方
        var sub = _container.Instantiate<Subscriber>();
        sub.Setup();
        
        // イベントを投げる方
        var publisher = _container.Instantiate<Publisher>();
        publisher.Send(new MyEvent { Message = "テストメッセージ" });
    }
}
```
<br>

これで テストメッセージ という文字列がログに表示されました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553138/f7ead8eb-37c3-313d-029d-e94c0a5916c7.png)

**流れ**
1. ZenjectのDIContainerを生成する
2. `BindMessageBroker<MyEvent>(_container.BindMessagePipe());` でMyEventイベントを送受信できるようにする
3. _container.Instantiate でPublisherとSubscriberを生成しながらInjectもしてもらう
4. Publisherでメッセージを送ればSubscriberが反応してDebugLogが表示される

めちゃめちゃシンプルですがこのコードだけでイベントの送受信ができました。

`MyEvent`というものをもう少し現実味がある

```C#
public class LogEvent
{
    public string logMessage;
    public ErrorType errorType;
}
```
や

```C#
// 弾が命中したとき
public class BulletHitEvent
{
    public Enemy enemy; // 命中した敵
    public int damage; // あたったときのダメージ量
}
```

にして、このイベントを送受信出来るようにします。

```C#
// まずはイベント登録
// どこかのSceneContext
public class SceneContextInstaller : MonoInstaller
{
	public override void InstallBindings()
	{
		var option = Container.BindMessagePipe();

		// BindMessageBrokerでイベント登録
		Container.BindMessageBroker<LogEvent>(option);
		Container.BindMessageBroker<BulletHitEvent>(option);
	}
}
```

実際にイベントを送受信するクラス

```C#
// 弾クラス
public class Bullet
{
    // Zenjectが勝手にInjectしてくれる
    [Inject] IPublisher<BulletHitEvent> _bulletHitEvent;
    [Inject] IPublisher<LogEvent> _logEvent; 

    // 敵にあたったとき呼び出される
    public void Hit(Enemy enemy)
    {
        _bulletHitEvent.Publish(new BulletHitEvent { enemy = enemy, damage = 10 };
    }

    // なにか致命的なエラーが起きたときなど
    public void NanrakanoError()
    {
        _logEvent.Publish(new LogEvent 
            { 
                 logMessage = "何らかのエラーが起きました", 
                 errorType = ErrorType.Error 
            };
    }
}

....

// どこかのSceneでログを受け取る
public class Scene
{
    // Zenjectが勝手にInjectしてくれる
    [Inject] ISubscriber<BulletHitEvent> _bulletHitEvent;
    [Inject] ISubscriber<LogEvent> _logEvent; 

    public void Setup()
    {
      // Subscribeしてイベントを受け取る
      _bulletHitEvent.Subscribe(....);
      _logEvent.Subscribe(....);
    }
}
```
などなど..
<br>

少しずつコードに組み込んでいくことで
イベント駆動を使用する場面も徐々に思い浮かんでいくかと思います


## GlobalMessagePipeでメッセージを送受信する

Zenjectを使用してMessagePipeを利用するのでイベントの範囲もZenjectがInject出来る範囲と同一である。
と考えられます。
つまり`SceneContext`がイベントが送受信できる範囲。

`SceneContext`によらずプロジェクト全体でイベントを送受信できるようにする場合は`ProjectContext`内のInstallerでBindMessageBrokerを使用すると良いということになります。

そしてもう少し手軽にメッセージの送受信を出来るようGlobalMessagePipeを使用してみます。

GlobalMessagePipeとはその名の通りグローバル空間で使用できるものです。


```c#
using Zenject;
using MessagePipe;

namespace XXX
{
	// ProjectContext.Prefabにこのスクリプトをアタッチ
	public class ProjectContextInstaller : MonoInstaller
	{
		public override void InstallBindings()
		{
			var option = Container.BindMessagePipe();

			// LogEventをProjectContextに登録。これでどこでもLogEventが投げられるようになる
			Container.BindMessageBroker<LogEvent>(option);
			
			// GlobalMessagePipeを使用する前にSetProviderに設定する必要がある
			GlobalMessagePipe.SetProvider(Container.AsServiceProvider());
		}
	}
}
```

これでGlobalMessagePipeが使用可能になります。
使用するためにはGlobalMessagePipeにDIContainerのServiceProviderを登録する必要があるのですが、ここで登録するのが`ProjectContext`

ProjectContextはシーンによらず生きているものなのでGlobalMessagePipeに登録するものとしては適切かと考えました。

登録した後はGlobalMessagePipeの静的メソッドで気軽にメッセージの送受信ができます。

```C#
// Manager的な人がゲーム全体で発行されたLogEventを受信する
public class Manager : Monobehaviour
{
    public void Awake()
    {
        // GlobalMessagePipeから直接LogEventのSubscriberを取得できる。そして即購読
        var subscriber = GlobalMessagePipe.GetSubscriber<LogEvent>();
        subscriber.Subscribe(ev => 
        {
            // コンソールに出したり、通信でログサーバーに送ったり
        });
    }
}
```

```C#
// バトルシーン
public class BattleScene
{
    public void NanrakanoError()
    {
        // 同じようにPublisherも直接取得して即時イベント発行
        var publisher = GlobalMessagePipe.GetPublisher<LogEvent>();
        _logEvent.Publish(new LogEvent 
            { 
                 logMessage = "何らかのエラーが起きました", 
                 errorType = ErrorType.Error 
            };
    }
}
```

これで更に気軽にメッセージの送受信が出来るようになりました。
イベントは場所を気にせず瞬間的に発行したいことがあるので割とこちらのほうが実際には使うケースが多いのではないでしょうか


### 終わりに

Disposeやフィルター機能などその他便利機能については取り上げていません。
メッセージを送受信するだけではなくMessagePipeに備わっている機能を使いこなすことで更にゲーム開発効率の向上が出来ると思います。

また実際に使用して気づいたことや便利な内容は記事にしていきます。

----

次 Disopose周りについての記事

https://qiita.com/Toshizabeth/items/20f951fd38920c73b050