Unity2021.2b にて読み込んだScriptableObjectの参照が "null" になる

画像撮り忘れたため文字だけ

Unity2021.2b2 を利用していると困ったエラーに遭遇

ScriptableObejctをAddressableAssetBundleで読み込んだ後、参照を保持する。 するとその参照がいつの間にか "null" となる現象が発覚

"null" は Unity でいうところの「中身はまだ生きてるけど null 判定ではtrueになる」というもの 使用すると値も取れて参照も出来るのだが

if (target == null)

のような条件分がtrueとなりエラーログを吐いてたりした


自分のコードが原因かとおもい長時間悩んだがふと 2021.1.16 にダウングレードしたところ不具合が消えた

GitHubActionsでUnity TestRunnerを走らせる 改

以前書いた記事のアップデート版です

現在では以前書いた記事は古くなって正常に動かなくなってる(アップデートしてる)ので追記していきます


https://github.com/marketplace/actions/unity-test-runner

こちらの GitHubAction を利用させてもらいます。テンプレート使って楽しましょう

そしてわかりやすいドキュメント

https://game.ci/docs/github/test-runner


CreateALF

GitHub ActionsでUnityTestRunnerを走らせるためにライセンス認証が必要です

そのためのライセンスファイルもGitHubActionsで作成します。

Dockerを手元で実行するの面倒くさいですし(あとM1Macだとなんかうまく動かない)


f:id:toshizabeth:20210723121248p:plain

CreateALF という専用のリポジトリを作成して、

Actions > Set up this workflow で専用のワークフローを構築していきます

f:id:toshizabeth:20210723121431p:plain


ファイルの名前はなんでも良いです

中身をすべて消して、以下のサンプルをコピペしてUnityEditorバージョンなどCIで使うバージョンに変更して Start commit > Merge してください

name: Create ALF File

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest
    strategy:
      matrix:
        unity-tag: [2021.1.15f1-mac-mono-0] # ← ここは docker image のバージョン。自分のプロジェクトのバージョンに合わせて

    container: docker://unityci/editor:${{ matrix.unity-tag }}

    steps:
    - run: mkdir artifact
    - run: /opt/unity/Editor/Unity -quit -batchmode -nographics -logfile -createManualActivationFile || exit 0
    - run: cp "Unity_v$UNITY_VERSION.alf" artifact
      env:
        UNITY_VERSION: 2021.1.15f1 # ← ここはUnityのバージョンだけ
    - uses: actions/upload-artifact@master
      with:
        name: ${{ matrix.unity-tag }}
        path: artifact

Docker Imageのバージョンですが、以前は gableroux/unity3d を利用させていただきましたが、更新が止まったようです(新しい方に移行してくれとのこと)


なので最新のEditorImageをアップしてくれている unityci/editor の方のimageを利用させていただきます

f:id:toshizabeth:20210723121917p:plain


ここで最新の 2021.1.15f1-mac-mono-0 を利用します

f:id:toshizabeth:20210723122029p:plain


なので先程のコードのunity-tag が [2021.1.15f1-mac-mono-0]になっていたわけですね


注意点として

/opt/unity/Editor/Unity ← ここ前までは大文字の Unity だったんですが、小文字になってました。これに気づかずに失敗の嵐



保存すると勝手にActionが走ります。

f:id:toshizabeth:20210723122201p:plain


成功したらArgifacts(成果物)をダウンロードしてください

認証に必要なファイルが入っています(まだなんの効力も持たないファイル)

f:id:toshizabeth:20210723122301p:plain


Activation

次に先程のファイルを利用して認証ファイルを作成します


Unity - Activation

ここにアクセスして先程の .alf ファイルを送っちゃってください


f:id:toshizabeth:20210723122554p:plain


で、次に進んでごにょっとしてライセンスファイルをダウンロードします


これは自分のIDと結びついているので大事なものです。他の人には見せないようにしましょう

ulfファイルをVisualStudioCodeでもなんでも良いのでファイルを開いて中身をすべてコピーします

これをCI実行したいプロジェクトの Settings > Secrets の右上の 「New repository secret」を選択して

名前を UNITY_LICENSE にして中身を先程コピーした内容を貼り付けます


このsettingsのSecretに書いたものは ${{ secrets.名前 }} で参照できるようになります。

秘密の情報を隠して名前でアクセスできる。重要なものを公開するわけには行かないので隠しましょう

※ 以前は暗号化しないとログに表示されて見られる危険性がありましたが、現在はログにも表示されなくなったので暗号化してないです


で、この状態でライセンス問題はOKなのでCI実行したいプロジェクトでテスト実行コードを書きます



次はCI実行したいプロジェクトがあるリポジトリで Actions タブから同じように New wordkflow を押します

中身は以下のように

name: UnityTestRunner

on: 
  push:
    branches: master
  pull_request:
   types: [opened, synchronize] # ← プルリク開いたときと更新時に反応して走らせる
 
jobs:
  testAllModes:
    name: Test in ${{ matrix.testMode }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        projectPath:
          - . # ここは . じゃないとエラー出る
        testMode:
          - playmode
          - editmode
    steps:
      # Checkout
      - uses: actions/checkout@v2
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          lfs: true
          
      # Cache
      - uses: actions/cache@v2
        with:
          path: ${{ matrix.projectPath }}/Library
          key: Library-${{ matrix.projectPath }}
          restore-keys: |
            Library-
            
      # Test
      - uses: game-ci/unity-test-runner@v2
        id: tests
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} # ← secret で登録した内容がここで出力される
        with:
          customParameters: '-nographics' # 追加
          projectPath: ${{ matrix.projectPath }}
          testMode: ${{ matrix.testMode }}
          artifactsPath: ${{ matrix.testMode }}-artifacts
          githubToken: ${{ secrets.GITHUB_TOKEN }}
          checkName: ${{ matrix.testMode }} Test Results
          unityVersion: 2021.1.15f1 # 何も書かないと自動でProjectVersion見てくれる
          
      # Output
      - uses: actions/upload-artifact@v2
        if: always()
        with:
          name: Test results for ${{ matrix.testMode }}
          path: ${{ steps.tests.outputs.artifactsPath }}

unityVersion は何も書かないと自動でプロジェクトのバージョンに合わせてDockerImage取ってきてくれますが、新しいUnity使用してるとまだDockerImageに上がってない可能性が高いので明示的に指定する事もあります

( マイナーバージョンのズレであれば通るときあります )


2020 や 2019 の最新すぎるものじゃない場合はあると思うのでここの行は消しちゃってもいいです。


これを保存後、プルリク上げると自動でテストが走ると思います


まずはレッドケース、エラーがちゃんと出るか確認してみましょう


適当にエラーが出るテストを作ります

PlayModeとEditModeどちらのフォルダを作って

f:id:toshizabeth:20210723135714p:plain

こんな感じにエラーが出るように

f:id:toshizabeth:20210723135751p:plain

まずはUnity上でエラーが出ることを確認

f:id:toshizabeth:20210723135854p:plain

正常にエラー(?)確認!!


これをプッシュしてプルリク作ります

そうすると反応して勝手にGithubActionが実行されます

f:id:toshizabeth:20210723140044p:plain

実行ログみるためにActionの方見に行きます

f:id:toshizabeth:20210723140133p:plain


記述した通りの順番で実行されていくのログが見えます。見てるだけで楽しい

2回めはキャッシュされて少し速度アップします(一回目のコミットミスがあってやり直してます)


unity-test-runnner@2 というところが今回のメイン

f:id:toshizabeth:20210723140415p:plain

ちゃんとエラーになりましたね!


成果物はこのように詳細が表示されます

今回、EditTestスクリプトのEditTestSimplePassesが実行されましたが、期待されている結果はtrueなのにfalseが渡されていてテスト失敗しています

f:id:toshizabeth:20210723140530p:plain


今度はこれを直してみましょう


f:id:toshizabeth:20210723140642p:plain


プッシュして再度テストが走ることの確認


f:id:toshizabeth:20210723140725p:plain


今度はうまくいきました!!!

f:id:toshizabeth:20210723141030p:plain


緑色になって気持ちいい...

f:id:toshizabeth:20210723141058p:plain


プッシュするたびにテストが走るようになってコード修正も少し安心できます

しかし、機能テストコードを書かなければ意味がないのでそこは頑張りましょうということで..

安心したらマージしましょう


テスト通らないとマージできないようにする設定もあるのでそこはお好みで!


その他

うまく行かない場合、ymlに記述したUnityバージョンが正しいか確認しましょう。

バージョンがずれてるとうまく行かないケースが高い。

Unity2021.1.X 系と Unity2021.2.X系 もあかん


※ 自分のプロジェクトでエラーが出たのでメモとして残しておきます

パッケージ解決時に com.unity.modules.nvidia パッケージが見つからんと怒られる

f:id:toshizabeth:20210723130646p:plain


入れた記憶ないのと使う予定が全く想像できないので manifest.jsonから消しました。

以下のバグで勝手に入ったのか?

issuetracker.unity3d.com


今回使用したテストプロジェクトのURLをおいておきます

github.com

【Unity】MessagePipeの非同期処理

MessagePipe解読3回目

MessagePipeは非同期処理が実装されているということで、それがどういう使い方ができるか。という部分の記事


async/await で待機

async/await で順次処理、並列処理も可能になっているとのことです!

非同期になっているからと言って使い方が変わっているというわけではなく

Publisher → IAsyncPublusher

Subscriber → IAsyncSubscriber

という Async という命名がついたクラスを使用するだけで使い方は変わらないそうです。


非同期処理についていくつかテストをしてみたいと思います。


  • イベントを送信するクラス Publisher
  • イベントを受信するクラス Subscriber
  • イベント MyEvent

とします


イベント送り終わるまで await で待機する

まずはイベントを送る側にasync/awaitを適用させてみます。

イベント送る側(Publisher)が、イベント送り終わるまでawaitするケース。

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

    public void Subscribe()
    {
        // イベントが来たら反応する
        _subscriber
            .Subscribe(ev =>
            {
                Debug.Log($"イベント受信完了");
            });
    }
}

イベント受信する側は前回と同じ。イベントが来たらSubscribeの引数のラムダ式が反応します。

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

    public async UniTask SendAsync(MyEvent ev)
    {
        // Subscriberの購読処理が終わるまで待つ
        await _asyncPublisher.PublishAsync(ev);

        // イベント全て送り終えた
    }
}

イベント送る方であるPublisherの IPublisher が IAsyncPublisher になっています。

この形で、IAsyncPublisherのPublishAsyncでイベントを送ることにより、イベントが送り終わるまで await され続けます。

後はPublisherクラスを作成して SendAsync を呼び出すだけです

{
    // イベント投げる
    var publisher = _container.Instantiate<Publisher>();
    await publisher.SendAsync(new MyEvent()); // ← イベントすべて送り終わるまで待機される
}


「イベント受け取り終わるまで待機...?」 というところに疑問を持たれると思います。

現在はイベント受け取るほうが特に何も変わったことをしていないためawaitは特に何もせずに先に進みます。

特に意味がないケース。


しかしIPublisherをIPublishAsyncにするだけで async/await が使用できるということがわかったと思います。

使い方がほぼ変わらずに適用できるという便利さ

イベントを受信したときに await させたい

次はイベントを受信する側。のクラスでasync/awaitを適用させるパターンを考えます。

イベントを受信したら1秒待機して、ログを出したいケース。


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

    /// <summary>
    /// Subscribe内で待機
    /// </summary>
    public void SubscribeAsync()
    {
        _asyncSubscriber
            .Subscribe(async (x, ctr) =>
            {
                var time = Time.realtimeSinceStartup;

                // ここで非同期処理が可能
                // 大体1秒待機する
                await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: ctr);
                time = Time.realtimeSinceStartup - time;

                Debug.Log($"イベント受信完了: {x.Message} 待機秒数: {time}秒");
            });
    }
}

イベント受け取る方を変えました。送る方はさっきと同じです。

ISubscriberをIAsyncSubscriberに変えて、これで非同期処理が使用可能になってます

_asyncSubscriberのSubscribeのラムダに async がついていることがわかると思います。


つまり、ラムダの中で await が使用できるということ。

ラムダの中で1秒待機する UniTask.Delay を使用して、その後ログを出すようにしました。

流れとしては以下です

  1. PublisherAsyncでイベントを投げた
  2. Subscriberクラスがイベントを受け取った
  3. UniTask.Delayで1秒待機
  4. 待機後ログを出した。
  5. Subscribe終了
  6. PublishAsync の待機が終わり先に進む

ここでひっそりと6番目に処理が追加されていますが、新しくコードを追加したのではなくもともとPublisherクラスは「イベントの送信処理がすべて終了した時先に進む」というawait処理がありました。

今回Subscriberクラスは1秒待機してログを出すようにしています。

つまり、4番目の 「待機後ログを出した。Subscribe終了」が終わるまで Publisherクラスの PublishAsyncが待機される。ということです

f:id:toshizabeth:20210718211455p:plain


イベント送信側で await する理由は、受信側で await される可能性がある時 その処理が終わるまで待つことが出来る。

一度だけイベントをawaitで受け付けたい

イベントを一度だけ待機したい。というパターンは割と多いんじゃないかと思います。

  • 再生したアニメーションが終わるまで待機したい。
  • 撃った弾が消えるまで待機したい

一度だけイベントが来るまでawaitして、イベント来たら処理を先に進ませたい


Subscirbe (() => .... ); でラムダの中に処理を書いても良いんですが、awaitを使用してもう少しモダンに処理をさせたいケースで使えるのが FristAsync

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

    public async UniTask WaitAsync(CancellationTokenSource cts)
    {
        var ev = await _asyncSubscriber.FirstAsync(cts.Token);

        Debug.Log($"イベント受信完了!!");
    }
}

Subscribe で購読処理を書くのではなくFistAsyncを書くことで「MyEvent が送られてくるまで待機する。」という処理が可能になりました。

一度だけ処理したい場合はこちらのほうが見やすく、直感的ではないでしょうか


ただ、イベントが来るまでawaitで待ち続けてしまいます。これは途中で購読をやめたい時には不便です。

このために引数で CancellationToken を受け取るようになっています。

これによりトークンをいつでもCancelすることでawaitの待機処理を破棄することができます

ここらへんもCyshapのUniTaskとの協調性が高くとても使いやすいと感じます。

終わりに

MessagePipeの真骨頂はこのUniTaskと連動した非同期処理にあるものだと言っても過言じゃないと思います。 async/awaitが使用できることによりイベントの送受信だけに収まらず、待機処理まで行えることで処理が分散されないことで視認性が高くなり、バグが出にくいコードにもなると考えます

とても素晴らしい...

MessagePipeのDisposable処理について

Qiita で記事化

qiita.com


以下の環境で実行しています

  • MacOS
  • Unity2020.2.0b2
  • MessagePipe v1.6.1
  • Zenject

MessagePipeはSubcribeでイベントの購読処理を行います。

public class Hoge
{
    public Hoge()
    {
        // MyEventの通知を受け取る
        var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<MyEvent>();
        subscriber.Subscribe(ev =>
        {
            // 購読処理
        });
    }
}

Subscribeの戻り地はIDisposableです。 このIDisposableを適切に処理しなければメモリリークが発生します。

つまり、購読クラスが破棄されたとしても、イベント購読し続けることになります。

(バグが出る可能性が高い!

public class Hoge
{
    public Hoge()
    {
        // MyEventの通知を受け取る
        var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<MyEvent>();
        subscriber.Subscribe(ev =>
        {
            // Hogeインスタンスが消えたとしても、ここは永遠に残り続ける
            // MyEventが来るたびに呼び出され続ける
        });
    }
}

基本的には購読しているクラスが破棄されると同時にイベントの購読も消したいはず。

今回はイベントの破棄を担当するDisposeについて書いていきます。

非Monobehaviourクラスでイベントを破棄したい場合

public class Hoge
{
    private readonly IDisposable _disposable;

    public Hoge()
    {
        var bag = DisposableBag.CreateBuilder();
        
        // MyEventの通知を受け取る
        {
            var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<MyEvent>();
            subscriber.Subscribe(ev =>
            {
                // 購読処理
            }).AddTo(bag);
        }

        // MyEvent2の通知を受け取る
        {
            var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<MyEvent2>();
            subscriber.Subscribe(ev =>
            {
                // 購読処理
            }).AddTo(bag);
        }

        disposable = bag.Build();
    }

    public void RemoveEventAll()
    {
        // Disposeですべて破棄される。必ず終了前に呼び出すこと
        disposable.Dispose();
    }
}

Subscribeの戻り値に対してAddToメソッドを呼び出す。

そこに DisposableBag.CreateBuilder() で作り出した DisposableBagBuilder を渡して Build -> Dispose を呼び出すことでイベントすべての購読が破棄されます。

Disposeは明示的に呼び出す必要があるので注意


Monobehaviourを継承しているクラスでイベントを破棄したい場合

UniRxを組み込んでいる場合、Monobehaviourクラスはもう少し楽に破棄の処理ができます

namespace UniRx; // UniRx必須

public class Hoge : Monobehaviour
{
    void Awake()
    {
        // MyEventの通知を受け取る
        var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<MyEvent>();
        subscriber.Subscribe(ev =>
        {
        }).AddTo(this); // 自分を渡してGameObjectの破棄時に削除をしてもらう
    }
}

AddTo に自分自身を渡してGameObjectのライフサイクルに結びつけます。

これでGameObjectが破棄されるときにイベントも破棄されます。


Subscribeを適切にハンドリングしてない場合エラーを出す

Dispose周りを見てきましたが、『Subcribeを適切に処理しているかどうか』は結局コードを書く人に委ねられている状態です。

忘れないようにしていても忘れたり抜けは必ず発生します。


しかし! なんとUnity2020.2以降であればSubcribeの戻り値をハンドリングしてないものがあれば、ビルド時にエラーになる便利なものが公開されているようです

Roslyn analyzers という、ユーザー独自のコードチェックを組み込める仕組みを利用されている。

(Unityは2020.2以降から使用できる)

docs.unity3d.com


結果からいうと MessagePipe.Analyzer.dll を組み込むことで

Subscribeに対して何も処理をしていない場合 Unityのコンソール上やVisualStudio上でエラーが出るようになります

VisualStudio f:id:toshizabeth:20210704173534p:plain

Unity f:id:toshizabeth:20210704173442p:plain


Subscribeは処理すべきものだと言うことで是非組み込んで有効活用していきたいところ。


導入手順

Unity2021.2.0b2 で動作を確認しています。 Unity2020.3.X系だとUnityでエラーが出ませんでした...(VisualStudioではエラーが出る)

バグが自分の設定が悪いのか.. 調査中


1.MessagePipe.Analyzer.dll を Unityに入れる

MessagePIpe の GithubMessagePipe.Analyzer.dll が上がっています。

(以下のGithubのReleaseを参照) github.com

こちらの MessagePipe.Analyzer.dll をダウンロードします。

f:id:toshizabeth:20210630233610p:plain


そしてAsset以下のどこか適当なディレクトリにぶっこむ。

f:id:toshizabeth:20210704174052p:plain


2. 設定変更

MessagePipe.Analyzer の Inspector を以下のように変更する

  • SelectPlatforms for Plugin のチェックをすべて外す
  • 右下のAssetLabelsボタンをクリックして RoslynAnalyzer という文字を入力してEnterを押してラベルを付ける

f:id:toshizabeth:20210704174456p:plain

ロード終了後、Subscribeを処理していない箇所に対してUnityがエラーを出すようになります


3. Visualstudioでもエラーを出す

VisualStudio上でもエラーを出すためには、Unityに対して更に一工夫する必要がありました。

同じくCysharpが公開しているCsprojModifierを導入します

github.com


公開されている最新バージョンのパッケージをインストールします

github.com


Unity > Project Settings > C# Project Modifier を開き、Add Roslyn Analyzer references to .csproj にチェックを付け、 「Regenerate project files」を押して .csproj を作り直します。

f:id:toshizabeth:20210704175014p:plain

終了後再度VIsualStudioを開き直したらVisualStudio上でも検知できるようになりました!


(現状、Unity2020.2以降のバージョンを使用している場合、VisualStudioCode, Rider ではこの機能は使用できないようです。

MacでVisualStudioでは正常に確認ができました)

Unityでエラーが出る以上実行ができないのでUnityでカバーしていればまあよし!とも捉えられる


MessagePipeDiagnostics を使用する

MessagePipeは更に機能があり(凄い)、実行中のプロジェクトで現在Subscribeされている数や情報を取得できる機能があります。(MessagePipeDiagnosticsInfo)

そして気軽に情報を見れるようにSubscribeしている箇所のモニターが出来る拡張機能が用意してあります。

Window > MessagePipeDiagnostics

を開くと、EditorWindowが立ち上がります。

f:id:toshizabeth:20210701100112p:plain

そして実行中にWindowを見てみると以下画像のようにSubsribeしている箇所が確認できます(便利

f:id:toshizabeth:20210704181756p:plain


この機能を利用するには GlobalMessagePipe.SetProvider を予め設定しておく事と、optionのEnableCaptureStackTraceをtrueにしておく必要があります。

f:id:toshizabeth:20210704181941p:plain

【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をもっと便利に使いこなせるようにしていきたいと思います

MessagePipeのScopeテスト1

前回の続きでMessagePipeを使っていきます


使用環境

  • Unity
  • Zenject
  • MessagePipe v1.6.1


イベントのスコープ

Zenjectを使用してイベントを投げていく上で、イベントのスコープが気になったので簡単にテスト


public class ScopeTest
{
    public class Publisher
    {
        [Inject] private IPublisher<string> _publisher;

        public void Send(string message) =>
            _publisher.Publish(message);
    }

    public class Subscriber
    {
        [Inject] private ISubscriber<string> _subscriber;

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

    private DiContainer _contaier1;
    private DiContainer _contaier2;

    [Test]
    public void Test1()
    {
        _contaier1 = new DiContainer();
        _contaier2 = new DiContainer();
            
        _contaier1.BindMessageBroker<string>(_contaier1.BindMessagePipe());
        _contaier2.BindMessageBroker<string>(_contaier2.BindMessagePipe());

        // イベントを受ける方
        var service1 = _contaier1.Instantiate<Subscriber>();
        service1.Setup("Container1");
        
        var service2 = _contaier2.Instantiate<Subscriber>();
        service2.Setup("Container2");
        
        // イベントを投げる方
        var publisher1 = _contaier1.Instantiate<Publisher>();
        publisher1.Send("Test");
    }
}


Container1 と Container2 を生成して、それぞれからイベントを受ける用インスタンスを生成。

// イベントを受ける方
var service1 = _contaier1.Instantiate<Subscriber>();
service1.Setup("Container1");
            
var service2 = _contaier2.Instantiate<Subscriber>();
service2.Setup("Container2");
            

そしてイベントを送る用のクラスはContainer1から生成して、送る

// イベントを投げる方
var publisher1 = _contaier1.Instantiate<Publisher>();
publisher1.Send("Test");

結果は "Container1 Test" と表示されたので、同じDIContainer同士じゃないとイベントが受け取れないことがわかりました

f:id:toshizabeth:20210626195843p:plain


DIContainerでスコープが別れているのは直感的にわかりやすい!


次は、DIContainerが異なる場合でも全体にイベントを投げる場合はどうするか。

ということを調べて書いていく

MessagePipe を Unity Test で使用してみる

github.com

MessagePipeが巷で話題になってきています。

自分のプロジェクトでは自分が作成したイベント発行クラス↓

github.com

を使っていましたが、あのCyshapが作成したライブラリ!!!

使わない選択肢がないということで使ってみました。


まずは触りだけ

Unityで使用する上で

まずはパッケージをインストールします。

通常のパッケージとあわせて VContainer, Zenject を選ぶ選択肢があります。

MessagePipeはDIContainerと連携しているということで、自分のプロジェクトがVContainerかZenjectどちらを使用しているかによって追加でパッケージをインストール。

(UniTaskも必須


最少実行

using NUnit.Framework;
using Zenject;
using Assert = UnityEngine.Assertions.Assert;
using MessagePipe;
using UnityEngine;

namespace 
{
    public class SimpleTest
    {
        // メッセーシを送る側
        public class MessageService
        {
            // Zenjectによって自動的にInject
            [Inject] private IPublisher<string> _pubLisher;

            public void Send(string message)
            {
                _pubLisher.Publish(message);
            }
        }

        // メッセージを受け取る側
        public class MessageHub : System.IDisposable
        {
            // Zenjectによって自動的にInject
            [Inject] private ISubscriber<string> _subscriber;
            
            private System.IDisposable disposable;

            public void Setup()
            {
                var bag = DisposableBag.CreateBuilder();
                
                _subscriber.Subscribe(x => Debug.Log(x)).AddTo(bag);
                
                disposable = bag.Build();
            }

            void System.IDisposable.Dispose()
            {
                disposable.Dispose();
            }
        }

        private DiContainer _contaier;

        [SetUp]
        public void Setup()
        {
            _contaier = new DiContainer();
        }

        // メッセージを送れるかテスト
        [Test]
        public void Test1()
        {
            InstallBindings(_contaier);
            
            var hub = _contaier.Instantiate<MessageHub>();
            hub.Setup();
            
            var service = _contaier.Instantiate<MessageService>();
            
            // Testという文字列をを送る
            service.Send("Test");
        }

        private void InstallBindings(DiContainer builder)
        {
            var options = builder.BindMessagePipe();

            // 使用するためにはDiContainerにバインドをしなければならない
            builder.BindMessageBroker<string>(options);
        }
    }
}

他のMessagePipeの記事を挙げられている方を参考に。

一旦このテストコードでメッセージを送られることを確認しました

f:id:toshizabeth:20210626182307p:plain


処理順序は

  1. builder.BindMessageBroker<string>(options); で string型のメッセージを送信/受信できるようにする
  2. MessageHub, MessageService を Zenjectから生成しBindしてもらう。
  3. あとは文字列を送ると MessageHub の _subscriber.Subscribe(x => Debug.Log(x)).AddTo(bag); Subscribeで受け取る


一旦最少コードは理解できたので次は上記コードの謎を少しずつ解消していこうと思います!