ZenjectでUnityのPrefabの外部参照を減らす

ZenjectでUnityのPrefabの外部参照を減らす

Extenject / Zenject のID指定を使ってPrefab外の参照を減らしました。

レベル数や所持コイン数のテキストを表示する場合にInspectorで書き込むテキストコンポーネントを指定することがよくあります。

Inspectorで、LevelNumとCoinNumの参照を登録

この例では、GameMain1 が 2つのテキストコンポーネントの参照を持っていますが、参照先が外部の Header Prefab内オブジェクトであるため、Prefabの変更を Apply All しても参照先の情報がSceneファイルに残ってしまいます。

このような状況では、複数人でSceneを編集した場合に衝突が起きやすくなるのでヤメたい。

その場合の選択肢として、参照先をSingletonにするという方法もありますが、既存コンポーネントの場合はそうも行きません。

そこで、Zenjectを使って参照コンポーネントの情報を取得します。


概要

こんな感じで、ボタンを押したらレベルアップしたり、ランダムなコイン獲得したりするサンプルを作ります。

Hierarchy構造はこんな感じ。
GameMain2のコンポーネントから、Header下のテキストコンポーネントを参照する。

直接参照せずに、Zenjectでバインドするので、GameMain2HeaderのPrefabは独立している。

Zenjectの導入

OpenUPM使ってる人は、コマンドラインで下記コマンドで処理

openupm add com.svermeulen.extenject

そうでない人は Unity Asset StoreからExtenject Dependency Injection IOC を導入

Zenjectの IDバインド用 MonoInstallerクラス

using System;
using UnityEngine;
using Zenject;

public class ZenjectIdBinding : MonoInstaller<ZenjectIdBinding>
{
    [Serializable]
    class BindInfo
    {
        public string key;
        public Component component;
    }

    [SerializeField] BindInfo[] m_bindInfos;

    public override void InstallBindings()
    {
        foreach (var info in m_bindInfos)
        {
            Container.Bind(info.component.GetType()).WithId(info.key).FromInstance(info.component).AsCached();
        }
    }
}

やってることは、key として名前を指定してコンポーネントを登録し、keyをIDとしてコンポーネントをInject対象として登録しているだけ

MonoInstallerをシーンへ登録

Hierarchy Viewで右クリックし、ポップアップメニューから Zenject > SceneContext を選ぶ

  • 追加されたSceneContextオブジェクトに、ZenjectIdBinding コンポーネントを追加
  • SceneContextコンポーネントのMonoInstallerに ZenjectIdBinding コンポーネントを登録
  • ZenjectIdBinding コンポーネントに Injectしたいコンポーネントを登録

これで、LevelNumとCoinNum のテキストが登録された。

ZenjectでInjectするコード

using UnityEngine;
using TMPro;
using Zenject;

public class GameMain2 : MonoBehaviour
{
    // ZenjectのIDで値をバインドする
    [Inject(Id = "LevelNum")] 
    TextMeshProUGUI m_levelText;
    
    [Inject(Id = "CoinNum")]
    TextMeshProUGUI m_coinText;

    int m_level = 1; // レベルは1から
    int m_coin = 0;

    // Start is called before the first frame update
    void Start()
    {
        m_levelText.text = $"{m_level}";
        m_coinText.text = $"{m_coin}";
    }

    public void LevelUp()
    {
        ++m_level; // レベルは必ず1ずつ上がる
        m_levelText.text = $"{m_level}";
    }

    public void GetCoin()
    {
        // ランダムな数のコイン獲得
        m_coin += UnityEngine.Random.Range(100, 1000);
        m_coinText.text = $"{m_coin:#,0}"; // コインはカンマ区切りあり
    }
}

重要なのは、[Inject(Id = "LevelNum")] の部分だけ。

LevelUp()GetCoin()は、ボタンを押したときに呼び出されるように登録。

まとめ

ということで完成

ZenjectのID指定を使うことで、最小のコード変更で使いたいコンポーネントを取得することができました。

  1. Zenjectを導入
  2. Scene Contextをシーンに追加
  3. Scene ContextのMonoInstallerに ZenjectIdBinding コンポーネントを登録
  4. ZenjectIdBindingコンポーネントに、使いたいコンポーネントにIDを付けて登録
  5. コンポーネントを利用するコードで、メンバ変数のアトリビュートに [Inject (Id = "ID文字列")]を記述
ZenjectIdBindingクラス、FooABarBクラスとも、依存関係はなく、直接参照も持っていません。
ZenjectIdBindingのみが、各コンポーネントの参照を持っている状態です。
(Inspector上で各コンポーネントの参照を持つ)

これで、自作クラスであれば Prefab外のコンポーネントへの参照をなくすことができるようになりました。
ただし、既存クラスの参照や動的に生成するコンポーネントへの参照はひと工夫必要です。