🏎️

[Unity] VContainerのエラーハンドリングについて

に公開

はじめに

UnityでVContainerを使用していて、エラーハンドリングの方法に迷ったので書き留めておきます。

やりたかったこととしては、画面遷移時に新しい画面がVContainerによって構築される際、エラーが吐かれたら遷移をキャンセルするという実装をしたかったのですが、思い外事例が見つけられませんでした。

使用バージョン: VContainer v1.16.6

UniTaskが入った環境を前提としています。

検討した結果

LifetimeScope のBuildを実行して、 IStartable などで処理を開始していきたいケースについては、以下のような実装でエラーが起きたかどうかを戻り値で取得できます。

interface IInitialized
{
    bool Initialized { get; }
}
using VContainer.Unity;

public class MyClass1 : IStartable, IInitialized
{
    public bool Initialized { get; private set; }

    public void Start()
    {
        // 初期化処理をここに書く

        // ここでExceptionが発生するようなケースを想定
        // throw new NullReferenceException();

        Initialized = true;
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using VContainer;
using VContainer.Unity;

public class MyLifetimeScope : LifetimeScope
{
    bool _buildFailed;

    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterEntryPoint<MyClass1>();
        builder.RegisterEntryPointExceptionHandler(
            e =>
            {
                Debug.LogError(e);
                _buildFailed = true;
            });
    }

    public async UniTask<bool> BuildManuallyAsync(CancellationToken cancellationToken)
    {
        try
        {
            Build();
        }
        catch (Exception e)
        {
            Debug.LogError(e);
            return false;
        }

        var initializedObjects = Container.Resolve<IReadOnlyList<IInitialized>>();
        if (initializedObjects.Count > 0)
        {
            await UniTask.WaitUntil(
                () => initializedObjects.All(initializedObject => initializedObject.Initialized) || _buildFailed,
                cancellationToken: cancellationToken
            );
        }

        return !_buildFailed;
    }
}

// 呼び出し
var result = await _lifetimeScope.BuildManuallyAsync(cancellationToken);
Debug.Log(result);

// result: falseなら遷移をキャンセルしてDestroyするなど

IStartable 等で Initialized を実装する必要がでてきますが、現状これが最もシンプルなのではないかとは思っています。毎フレーム Initialized を確認することにはなります。

なぜこのような処理になるか

IStartable などは PlayerLoopSystem を用いて実行されます。

https://vcontainer.hadashikick.jp/integrations/entrypoint

そのため、 Build() の処理が完了したタイミングではまだ発火されておらず、PlayerLoopにスケジュールされた状態になるため、そのままだと正常に処理が完了したかどうかを取得することができません。

GameObject を Instantiate した際に Start でエラーが出ても処理を止められないのと同じ挙動です。

なので、正常に処理が完了したことを確認したい場合は、生成されたインスタンス自身が状態を保持してそれを確認するというのが1つの方法になります。

ExceptionHandler については以下の項目にも記載がありますが、

https://vcontainer.hadashikick.jp/integrations/entrypoint#handle-of-the-exception-that-was-not-caught

  • RegisterEntryPointExceptionHandler しなかった場合は、 LogError が吐かれつつ発生したExceptionがthrowされて処理が完了しないので毎フレーム実行されてしまう。
  • RegisterEntryPointExceptionHandler した場合は、 LogError は吐かれずthrowもされず、Handlerとして登録した処理が実行される。

という挙動になります。

また、Build() 処理は依存解決でエラーになった際にここで処理が止まって戻り値が得られなくなってしまうため、try-catchするようにしています。

おわりに

別の方法もあるのかもしれないですが、同じようにエラーハンドリングを検討している方の助けになれば幸いです。

Happy Elements

Discussion