開発ブログ

株式会社Nextatのスタッフがお送りする技術コラムメインのブログ。

電話でのお問合わせ 075-744-6842 ([月]-[金] 10:00〜17:00)

  1. top >
  2. 開発ブログ >
  3. Unity >
  4. 【Unity】VContainerで依存性注入やってみた
【Unity】VContainerで依存性注入やってみた

【Unity】VContainerで依存性注入やってみた

みなさんこんにちは。ホンジョウです。
今までUnityでの依存性注入はZenjectで行っていましたが、最終リリースが2020年ということで、だいぶ枯れてきたように思います。
成熟したライブラリを使用するのも大変よろしいですが、せっかくなのでまだまだ活発にアップデートが行われているVContainerに乗り換えてみようということで、少し触ってみました。

開発環境

  • Mac OS Sequoia 15.3.1
  • Unity 6(6000.0.29f1)
  • VContainer 1.16.8
  • R3 1.3.0
  • UniTask 2.5.10
VContainerの導入方法に関しては公式ドキュメントがわかりやすいです。
しれっとR3、UniTaskを入れていますが、これは今回作成する処理で使うものであり、VContainer自体は単体で導入することができます。
これらの導入方法は他ブログにも掲載されているので、以下に紹介します。

作成するもの

今回の記事では、シーンの切り替えなどで使用できるフェード処理を作成します。
そのために必要なModel、View、Controllerのコードを作成していきます。

FadeView.cs の作成

今回はuGUIのImageを使い、Imageの透明度を変更してフェード機能を実現しようと思います。 まずはMonoBehaviourを継承したGameObjectにアタッチするためのViewを作成します。

using UnityEngine;
using UnityEngine.UI;

public sealed class FadeView : MonoBehaviour
{
    [SerializeField] private Image image;

    public void SetImageAlpha(float alpha)
    {
        var tempColor = image.color;
        tempColor.a = alpha;
        image.color = tempColor;
    }
}

FadeViewをアタッチするオブジェクトの名前は「Fader」としておき、以下のようにコンポーネントを設定しておきます。
fader.png
Imageは画面全体が覆われるようにサイズを調整しておきましょう。
image.png

FadeModel.cs の作成

フェード機能の具体的なロジックをFadeModelに記述していきます。
今回は透明度を変更する処理に加え、透明度の変更を通知するReactivePropertyを用意して、透明度の変更をViewに反映できるようにします。

using System.Threading;
using Cysharp.Threading.Tasks;
using R3;
using UnityEngine;

public sealed class FadeModel
{
    public Observable Alpha => alpha;
    private readonly ReactiveProperty alpha = new (0);

    public async UniTask FadeInAsync(CancellationToken token)
    {
        alpha.Value = 1;
        while (alpha.Value > 0) {
            alpha.Value -= Time.deltaTime;
            await UniTask.Yield(token);
        }
        alpha.Value = 0;
    }

    public async UniTask FadeOutAsync(CancellationToken token)
    {
        alpha.Value = 0;
        while (alpha.Value < 1) {
            alpha.Value += Time.deltaTime;
            await UniTask.Yield(token);
        }
        alpha.Value = 1;
    }
}

FadeController.cs の作成

ControllerクラスではModelクラスとViewクラスのつなぎこみを行います。

using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using R3;
using VContainer.Unity;

public sealed class FadeController : IStartable, IDisposable
{
    private readonly FadeModel fadeModel;
    private readonly FadeView fadeView;
    private readonly CompositeDisposable disposables = new();
    private readonly CancellationTokenSource cancellationTokenSource = new();

    public FadeController(
        FadeModel fadeModel,
        FadeView fadeView
    )
    {
        this.fadeModel = fadeModel;
        this.fadeView = fadeView;
    }

    public void Start()
    {
        fadeModel.Alpha
            .Subscribe(fadeView.SetAlphaImage)
            .AddTo(disposables);
        // 試しに動くかここで確認
        fadeModel.FadeInAsync(cancellationTokenSource.Token).Forget();
    }

    public void Dispose()
    {
        cancellationTokenSource.Cancel();
        cancellationTokenSource.Dispose();
        disposables.Dispose();
    }
}

RootLifetimeScopeの作成

さて、ここからが記事のメインパートになりますが、Controllerのコンストラクタで必要としているModel, Viewを注入するために、VContinerを使用します。
今回はゲーム中のすべての場面で使用できるフェード機能なので、すべてのLifetimeScopeの親である、RootLifetimeScopeにフェードのDIを記述していきます。

using UnityEngine;
using VContainer;
using VContainer.Unity;

public sealed class RootLifetimeScope : LifetimeScope
{
    [SerializeField] private FadeView fadeView;

    protected override void Configure(IContainerBuilder builder)
    {
        // 他のクラスから依存されていなくても必ずインスタンス化される。
        // IStartableなどのVContainerエントリポイントを実装している場合は、自動的にライフサイクルに組み込まれる。
        builder.RegisterEntryPoint<FadeController>();

        // 基本的な登録処理。他のクラスから依存されている場合にFadeModelを注入して解決する。
        builder.Register<FadeModel>(Lifetime.Singleton);

        // 他のクラスから依存されている場合に、FadeViewがアタッチされているPrefabを生成して、そのオブジェクトを注入して解決する。
        builder.RegisterComponentInNewPrefab(fadeView, Lifetime.Singleton);
    }
}

FadeViewをアタッチしているオブジェクトは、Prefab化しておきます。
そして、RootLifetimeScopeを手頃なGameObjectにアタッチし、それもPrefab化します。
RootLifetimeScopeのPrefabにFadeViewのPrefabを登録しておきます。
root-lifetime-scope.png
Createメニューから「Vcontainer」->「Vcontainer Settings」をクリックし、VContainerSettings.assetを生成します。
v-container-settings.png
VcontainerSettingsの「Root Lifetime Scope」にRootLifetimeScopeがアタッチされたPrefabを指定しておきます。
この状態で実行を押すと、画面がフェードして表示されることが確認できます。

問題点

実行したシーンのHierarchyビューを見てみると、生成されたFadeViewのGameObjectが生成されていますが、それがDontDestoryOnLoadになっていないことがわかります。
このままでは、シーン移動時にFadeViewが破棄されてしまうので、シーン遷移以降の処理でフェードが機能しなくなってしまいます。
それを解決するため、LifetimeScopeのコードを修正します。

using UnityEngine;
using VContainer;
using VContainer.Unity;

public sealed class RootLifetimeScope : LifetimeScope
{
    [SerializeField] private FadeView fadeView;

    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterEntryPoint<FadeController>();
        builder.Register<FadeModel>(Lifetime.Singleton);

        // 生成時にFadeViewをRootLifetimeScopeのGameObjectの子にする。
        builder.RegisterComponentInNewPrefab(fadeView, Lifetime.Singleton).UnderTransform(transform);
    }
}

このように記述することで、FadeViewがDontDestoryOnLoadになっているRootFiletimeScopeの子になるので、FadeViewもDontDestoryOnLoadオブジェクトになります。
これでシーン間を移動してもFadeViewが破棄されず、フェード機能が問題なく動作するようになります。
(RootLifetimeScopeで生成したGameObjectはすべてデフォルトでDontDestroyになっていてほしいなと思いますが、なぜこのような仕様になっているのかはよくわかりません...。)

おわりに

VContainerを使用したDIの方法に関しての記事でした。まだ実感はありませんが、VContainerは非常にパフォーマンスに優れていることがこちらのページでも紹介されています。
作者も日本の方で、質問やフィードバックのハードルが低いと感じました。VContainerのますますの発展をお祈りしております。
  • posted by りっちゃん
  • C# , Unity
TOPに戻る