【Unity】MessagePipeのScopeについて2

前回はDIContainerごとに異なるスコープを持つことがわかりました。(Zenject使用時

今回は実際にMessagePipeのBroker, Scope, Lifetimeについて調べながら書いていこうと思います

github.com

(Readmeが丁寧に書かれているのでここを読むのが一番早いですが..!)

  • Unity
  • Zenject
  • MessagPipe v1.6.1


Broker

Publisher, Subscriber はDI毎に管理され、スコープごとに異なるBrokerを持てる。

そして、スコープが破棄されるときにすべての購読も解除されるためリーク防止となる。 (Readmeから)


イベントは BindMessageBroker メソッドで登録をします。

そこでDIContainer毎に異なるBrokerが生成され、DIContainerが破棄されるときにすべての購読も解除されます。

ちなみに同じDIContainerに対して同じ型は一度だけしか登録できないので以下のようにするとエラーが出ます

var option = _container.BindMessagePipe();

// 同じ型の二重登録
_container.BindMessageBroker<MyEvent>(option);
_container.BindMessageBroker<MyEvent>(option);


BindMessageBrokerの引数に指定するoptionの中には

生存範囲を決める 'InstanceLifetime' というものを決めれました。

InstanceLifetime

Singleton

Scope


しかしZenjectをDIContainerに指定している場合はZenjectの実装上Singletonは選択できない。そうです(コードを見るとSingletonを指定していても必ずScopeになるようになっていました)


つまりZenjectで使用する場合はZenjectで言うところの AsSingle 的な使い方はできず、イベントは常にScopeとともに生き死をともにする。

ということかな? と


ゲーム全体へのイベント通知

MessagePipeを調べていくうちに 「ゲーム全体にイベントを投げたいときはどうするの?」

という疑問がわきました。


例えば、何かしら通知を行うクラスを考えてみます。

// 通知用
public class NoticeEvent
{
    public string Message;
}

そしてManager的なところでNoticeEventを受け取ってごにょごにょするような以下のSubscriberを想定

// NoticeEventを受け取る

subscriber.Subscribe(ev => 
{
    var message = ev.Message;

    // UIに表示したり
    // デバッグでコンソールに送ったり
    // 通信でログ送ったり
....
};

複数のDIContainerが存在している場合(Zenject的に言うとSceneContextが複数)、あるシーンから別のシーンに通知イベントを投げたい。

しかし、前回テストしたように通知を受け取るためには同じDIContainerを使用しなければならない。



そこでこのときに使用できるのが GlobalMessagePipe

その名の通りグローバルな使い方ができるMessagePipeであると。


とりあえず使ってみます。

使用する前に SetProvider メソッドでDIContainerから取得したServiceProviderを設定する必要があります。


GlobalMessagePipe.SetProvider(_container.AsServiceProvider());

一度 GrobalMessagePipe に登録すると、あとはこのクラスの静的メソッドを利用して Publisher と Subscriber を取得できます。

// どこかで受ける
var subscriber = GlobalMessagePipe.GetSubscriber<NoticeEvent>();
subscriber.Subscribe(ev => 
{
    var message = ev.Message;

    // UIに表示したり
    // デバッグでコンソールに送ったり
    // 通信でログ送ったり
....
};

そして送る方

// どこかで送る
var publisher = GlobalMessagePipe.GetPublisher<NoticeEvent>();
publisher.Publish(new NoticeEvent { Message = "テストログ" });

静的メソッドからイベントの受信と発行ができるので場所を問うことはなくなりました。



GlobalMessagePipe.SetProvider

SetProvder に渡している _container.AsServiceProvider() こちらですが、どのContainerを渡すべきか

GlobalMessagePipe.SetProvider(_container.AsServiceProvider());


マルチシーン開発であれば削除されないManager的なシーンについているSceneContextのContinerから取得してもいいと思います。


一般的には ProjectContext を使用するのが良いかなと。

ProjectContextはゲーム全体に常駐するContextであり、Globalに設定するには適切なものであると考えました


つまり、ProjectContext.prefab を作成したあと以下のMonoInstallerスクリプトを作成してそのPrefabにアタッチ

using Zenject;

namespace  XXX
{
    public class ProjectContextInstaller : MonoInstaller
    {
        public override void InstallBindings()
        {
            var option = Container.BindMessagePipe();

            // Option設定変更するならここ

            GlobalMessagePipe.SetProvider(Container.AsServiceProvider());


            // NoticeEventをBind
            Container.BindMessageBroker<NoticeEvent>(option);
            

            ... あとその他
        }
    }
}

あとはGlobalMessagePipeから Publish, Subscribe を取得してゲーム全体でイベント発行!!

// Manager的な人がゲーム全体で発行されたNoticeEventを管理する
public class Manager : Monobehaviour
{
    public void Awake()
    {
        var subscriber = GlobalMessagePipe.GetSubscriber<NoticeEvent>();
    subscriber.Subscribe(ev => 
    {
        UnityEngine.Debug.Log(ev.Message);
    });
    }
}

で、送る

// バトルシーン
public class BattleScene
{
    public void Damage()
    {
    var publisher = GlobalMessagePipe.GetPublisher<NoticeEvent>();
    publisher.Publish(new Common.NoticeEvent { Message = "ダメージ発生した" });
    }
}

これだけでイベント配達ができました。

ものすごく楽


終わりに

今回はDisposeやフィルター機能などは省いています。

MessagePipeはただのイベント発行だけではなく、Disposeやフィルター機能、Asyncによる非同期待機処理などの便利機能の価値が高いものと思います。

むしろその機能を使いこなせたとき更にゲーム開発効率が上がると信じています。


また少しずつ調べた内容を記事化していきMessagePipeをもっと便利に使いこなせるようにしていきたいと思います