スポンサーリンク
UnityUnityメモ

Unityのコードをきれいに保つ!依存性注入(DI)の基本と実装方法

Unity

1. はじめに

Unityでゲームを開発していると、プロジェクトが大きくなるにつれて「コードが複雑になって読みにくい」「どこで何を参照しているのか分からない」といった問題に直面することが多いですよね。特にクラス同士が密接に結びつきすぎると、ほんの少しの修正でも大きな影響が出てしまい、保守や機能追加が難しくなってしまいます。

こうした課題を解決するためのアプローチが依存性注入(Dependency Injection / DI)です。DIを使うことでクラス間の依存を整理し、より柔軟でテストしやすいコード設計が可能になります。実際、DIの考え方は大規模なUnity開発に限らず、個人制作の小さなゲームでも「後から修正がしやすいコード」を作るうえで役立ちます。

ただしUnityには独自の制約があり、一般的なオブジェクト指向言語で語られるDIの方法をそのまま使うことはできません。本記事では、UnityにおけるDIの基本的な考え方から、実際にDIコンテナ(Zenject / VContainerなど)を活用した実装方法、さらにUnity Cloud CodeにおけるDIサポートまでをわかりやすく解説していきます。

DIの設計思想やUnityにおける実践的な応用をもっと深く理解したい方には、以下の書籍もおすすめです。ゲームプログラミング全般の知識を整理するのに役立ちますよ。

Unityゲーム プログラミング・バイブル 2nd Generation
Amazonでチェックする|✅ 楽天でチェックする


2. UnityにおけるDIの背景と課題

まずは「依存性注入(DI)」がなぜ必要とされるのかを理解しておきましょう。DIは「制御の反転(Inversion of Control)」を実現するための手法で、クラス同士を直接結びつけるのではなく、外部から依存するオブジェクトを注入することで疎結合化を図ります。これにより、テストがしやすくなり、将来的な仕様変更にも柔軟に対応できるのです。

2-1. 一般的なDIの目的

  • クラス間の依存を減らすことで再利用性を高める
  • 実装ではなくインターフェースに依存することで、差し替えを容易にする
  • ユニットテストでモックを注入しやすくする

2-2. Unityにおける特殊な事情

ところがUnityには独特の事情があります。最も大きいのは、ゲームオブジェクトにアタッチするクラスであるMonoBehaviourがコンストラクタを利用できないことです。一般的なDIではコンストラクタを使って依存を注入しますが、Unityの仕組み上それができないため、独自のアプローチが必要になりました。

2-3. よくある依存関係の解決手法

Unityで依存関係を解決する方法はいくつか存在します。それぞれにメリットとデメリットがあるため、状況に応じて適切な選択をする必要があります。

手法メリットデメリット
Find() 系(例: GameObject.Find()直感的でわかりやすい。インスペクター設定不要。ヒエラルキーの構造に依存しやすく、実行時エラーの原因になりやすい。
シングルトンパターンどこからでも参照できるため便利。インスタンスが一つであることを保証。「神クラス化」しやすく、テストや拡張が難しくなる。
[SerializeField] を利用インスペクターからアタッチできるため、直感的かつ安全。動的生成されたオブジェクトには対応できない。MonoBehaviour必須。
コンストラクタを利用もっともオブジェクト指向的。Pure C#コードでは柔軟に使える。MonoBehaviourでは使用できない。初期化関数を作る必要がある。

このように、Unityの開発では一般的なDIをそのまま使えない場面が多く、独自の工夫が求められます。次の章では、こうした課題を解決するために登場したDIコンテナについて見ていきましょう。




3. DIコンテナの役割とUnityでの発展

Unityの開発で依存関係を整理するうえで欠かせない存在がDIコンテナです。DIコンテナは、オブジェクト間の依存を自動的に解決してくれる便利なフレームワークで、代表的なものに Zenject(Extenject)VContainer があります。

一般的なオブジェクト指向プログラミングにおけるDIでは、コンストラクタを通して依存を注入するのが基本ですが、Unityでは MonoBehaviour の制約によりそれができません。そのため、Unity向けDIコンテナは「MonoBehaviourのライフサイクル管理」を肩代わりする形で独自に発展してきました。

3-1. [Inject]属性と糖衣構文

ZenjectやVContainerでは、[Inject]属性を使って依存関係を自動的に埋め込むことができます。これは一見「DIの本質的な機能」のように見えますが、実際にはUnity特有の仕組みを楽に扱うためのシンタックスシュガー(糖衣構文)にすぎません。

具体的には、[Inject]をつけたフィールドに、自動的にインスタンスを割り当ててくれるため、「Find()」や「シングルトン」のように自前で参照を探す必要がなくなります。結果としてコードはすっきりし、保守性が高まります。

3-2. DIコンテナ導入で得られるメリット

  • オブジェクトの生成とライフサイクル管理を一元化できる
  • クラス同士を疎結合にでき、テストや差し替えが容易になる
  • 将来的な拡張や機能追加にも柔軟に対応可能

3-3. 開発効率をさらに高めるアセット

DIコンテナを導入するとコードの依存関係は整理されますが、実際の開発では「インスペクターの可視化」や「ログの管理」も重要です。そこで役立つのが以下のアセットです。

  • Odin Inspector & Serializer ─ インスペクターを大幅に拡張し、依存関係の可視化やデバッグを効率化できる人気アセット。
  • Editor Console Pro ─ エラーログやデバッグログを見やすく整理できるツール。DI導入後のエラー検出にも便利です。

このように、DIコンテナと補助アセットを組み合わせることで、Unity開発はよりスムーズになり、チーム開発でもトラブルを減らすことができます。


4. Extenject(Zenject)を使った具体的なDI実装例

ここからは、実際にUnityでよく使われるDIコンテナExtenject(旧Zenject)を使ったサンプルを紹介します。 「入力処理」を例に、インターフェースと実装を疎結合にして、後から簡単に差し替えできる仕組みを作ってみましょう。

4-1. インターフェースの作成

まずは入力処理の共通機能を定義するインターフェースを作成します。 プロジェクトウィンドウを右クリックして「Create」→「C# Script」を選び、IInputtableと名前をつけます。


public interface IInputtable
{
    Vector3 GetInput();
}

4-2. 実装クラスの作成

次に、このインターフェースを実装する具体的なクラスを作ります。 例として「キーボード入力」を処理する InputFromKeyboard を用意しましょう。


public class InputFromKeyboard : IInputtable
{
    public Vector3 GetInput()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        return new Vector3(h, 0, v);
    }
}

4-3. MonoBehaviourスクリプトで利用する

実際のゲームオブジェクトにアタッチするクラスでは、[Inject]を使って依存関係を受け取ります。 プロジェクトウィンドウで「Create」→「C# Script」を選び、InputForMoveを作成します。


using UnityEngine;
using Zenject;

public class InputForMove : MonoBehaviour
{
    [Inject]
    private IInputtable _inputObject;

    void Update()
    {
        Vector3 move = _inputObject.GetInput();
        transform.Translate(move * Time.deltaTime * 5f);
    }
}

4-4. Installerで依存関係を定義する

ZenjectではInstallerを使って、どのクラスをどのインターフェースに紐づけるかを定義します。 「Create」→「C# Script」でSampleInstallerを作成しましょう。


using Zenject;

public class SampleInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<IInputtable>()
            .To<InputFromKeyboard>()
            .AsSingle();
    }
}

4-5. Scene Contextとの関連付け

最後に、シーン内に「Scene Context」を作成し、そこにSampleInstallerを登録します。 これでシーンが開始されたときに、IInputtableには自動的にInputFromKeyboardが注入されるようになります。

4-6. メリットまとめ

  • 入力処理の実装を差し替えるのが容易(例:ゲームパッド入力用クラスに変更)
  • 依存関係をコード内にベタ書きしないため、保守性が高まる
  • テスト時にはモックを注入できるため、検証がしやすい

このようにDIコンテナを導入することで、Unity特有の制約を克服しつつ、オブジェクト指向的な設計を取り入れることが可能になります。




5. Unity Cloud CodeにおけるDIサポート

Unityではローカル環境だけでなく、クラウド上で動作するコード(Unity Cloud Code)でも依存性注入がサポートされています。 これにより、クラウド関数に直接依存を埋め込むのではなく、柔軟に差し替え可能な構造を作れるため、テストや拡張がしやすくなります。

5-1. サポートされる依存関係の種類

Cloud CodeのDIでは、以下の3種類のライフサイクルを選択できます。

  • Singleton:一度生成されたインスタンスをサービス存続中ずっと使い回す。ただしスケールアウトした場合はリクエスト間で異なるインスタンスになる可能性がある。
  • Scoped:リクエストごとに一度だけ生成され、そのリクエスト内では同じインスタンスを共有。
  • Transient:呼び出されるたびに新しいインスタンスを生成。

5-2. 設定方法

Cloud Codeで依存関係を設定するには、ICloudCodeSetupインターフェースを実装したクラスを作成し、Setupメソッドの中で依存を登録します。


using Unity.Services.CloudCode;

public class ModuleConfig : ICloudCodeSetup
{
    public void Setup(ICloudCodeConfig config)
    {
        // Singletonとして登録
        config.Dependencies.AddSingleton<IRandomNumber, LockedRandomNumber>();

        // Scopedとして登録
        config.Dependencies.AddScoped<IService, ScopedService>();

        // Transientとして登録
        config.Dependencies.AddTransient<ILogger, ConsoleLogger>();
    }
}

5-3. 実際の利用方法

DIで登録した依存関係は、Cloud Code関数にコンストラクタで注入できます。 その際、メソッドには[CloudCodeFunction]属性を付ける必要があります。


using Unity.Services.CloudCode;

public class RandomNumberFunction
{
    private readonly IRandomNumber _random;

    public RandomNumberFunction(IRandomNumber random)
    {
        _random = random;
    }

    [CloudCodeFunction("GetRandom")]
    public int Execute()
    {
        return _random.Next();
    }
}

このように、Unity Cloud CodeのDIを利用することで、クラウド上の処理でも疎結合な設計を実現できます。 特にバックエンドのテストや拡張を考えた場合に大きなメリットを発揮します。


6. まとめ

本記事では、Unityにおける依存性注入(DI)について、基本的な考え方からUnity特有の制約、さらにDIコンテナやUnity Cloud Codeでの活用方法までを解説しました。

  • DIはクラス間の依存を整理し、保守性やテスト性を高める手法である
  • UnityではMonoBehaviourの制約があるため、独自に進化した仕組みを使う必要がある
  • Zenject(Extenject)やVContainerといったDIコンテナを使えば、疎結合な設計が可能
  • Unity Cloud CodeでもDIがサポートされ、クラウド処理においても柔軟な構造を実現できる

DIを導入することで「将来の仕様変更に強い設計」を実現でき、コードの可読性やチーム開発での効率も大幅に向上します。 さらに以下のようなツールや書籍を活用すれば、より一歩進んだ開発環境を整えることができます。

開発を助けるおすすめアセット

  • Odin Inspector & Serializer ─ インスペクター拡張で依存関係を可視化し、デバッグを効率化。
  • Editor Console Pro ─ エラーログを整理しやすく、依存性解決後の挙動チェックに便利。

さらに理解を深めたい方へ

DIや設計パターンをしっかり学びたい方には、以下の書籍もおすすめです。Unityの設計やプログラミング全般を体系的に学ぶことができます。

Unityゲーム プログラミング・バイブル 2nd Generation
Amazonでチェックする楽天でチェックする

DIは最初は少しとっつきにくく感じるかもしれませんが、仕組みを理解してしまえば「コードをきれいに保つための強力な武器」になります。ぜひご自身のUnityプロジェクトにも取り入れてみてくださいね。


あわせて読みたい

DIやコード設計に興味を持った方は、以下の記事もあわせて読むと理解がさらに深まりますよ。


よくある質問(FAQ)

Q
DIを使うメリットは本当にあるの?
A

はい、あります。特にプロジェクトが大きくなるほど恩恵は大きくなります。 依存を整理することでコードがシンプルになり、テストしやすく、修正や機能追加も楽になります。 小規模プロジェクトでも「後から仕様変更に対応しやすい」点は十分なメリットです。

Q
小規模なゲームでもDIを導入すべき?
A

必須ではありませんが、「将来的に拡張する予定がある」「チーム開発を考えている」場合には導入をおすすめします。 逆に「短期間で作りきる小さなゲーム」なら、[SerializeField]による依存設定で十分なケースもあります。

Q
ZenjectとVContainerはどちらを選べばいい?
A

両者とも優秀なDIコンテナですが特徴が異なります。 Zenject(Extenject)は歴史が長く、情報が豊富。大規模プロジェクトにも導入実績があります。 VContainerは比較的新しく、軽量で高速なDI処理が特徴。パフォーマンス重視の開発に向いています。 どちらも基本的なDI機能は揃っているので、プロジェクト規模や好みに合わせて選ぶとよいでしょう。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク