【Unity】VContainerで依存性注入やってみた
みなさんこんにちは。ホンジョウです。
今までUnityでの依存性注入はZenjectで行っていましたが、最終リリースが2020年ということで、だいぶ枯れてきたように思います。
成熟したライブラリを使用するのも大変よろしいですが、せっかくなのでまだまだ活発にアップデートが行われているVContainerに乗り換えてみようということで、少し触ってみました。
しれっとR3、UniTaskを入れていますが、これは今回作成する処理で使うものであり、VContainer自体は単体で導入することができます。
これらの導入方法は他ブログにも掲載されているので、以下に紹介します。
そのために必要なModel、View、Controllerのコードを作成していきます。

Imageは画面全体が覆われるようにサイズを調整しておきましょう。
今回は透明度を変更する処理に加え、透明度の変更を通知するReactivePropertyを用意して、透明度の変更をViewに反映できるようにします。
今回はゲーム中のすべての場面で使用できるフェード機能なので、すべてのLifetimeScopeの親である、RootLifetimeScopeにフェードのDIを記述していきます。
そして、RootLifetimeScopeを手頃なGameObjectにアタッチし、それもPrefab化します。
RootLifetimeScopeのPrefabにFadeViewのPrefabを登録しておきます。

Createメニューから「Vcontainer」->「Vcontainer Settings」をクリックし、VContainerSettings.assetを生成します。

VcontainerSettingsの「Root Lifetime Scope」にRootLifetimeScopeがアタッチされたPrefabを指定しておきます。
この状態で実行を押すと、画面がフェードして表示されることが確認できます。
このままでは、シーン移動時にFadeViewが破棄されてしまうので、シーン遷移以降の処理でフェードが機能しなくなってしまいます。
それを解決するため、LifetimeScopeのコードを修正します。
これでシーン間を移動してもFadeViewが破棄されず、フェード機能が問題なく動作するようになります。
(RootLifetimeScopeで生成したGameObjectはすべてデフォルトでDontDestroyになっていてほしいなと思いますが、なぜこのような仕様になっているのかはよくわかりません...。)
作者も日本の方で、質問やフィードバックのハードルが低いと感じました。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
しれっと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」としておき、以下のようにコンポーネントを設定しておきます。
Imageは画面全体が覆われるようにサイズを調整しておきましょう。

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を登録しておきます。

Createメニューから「Vcontainer」->「Vcontainer Settings」をクリックし、VContainerSettings.assetを生成します。

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のますますの発展をお祈りしております。