8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

VContainer/ZenjectでMonoBehaviourのインスタンスをまとめて索引可能にする方法

Last updated at Posted at 2023-01-15

はじめに

紹介したいこと

ヒエラルキー上のMonoBehaviour継承型を収集して、
VContainer、またはZenjectのコンテナに登録する方法を紹介していきます!
また、登録されたものを簡単に索引し、利用できるようにしてみました。

この方法を使うと、
シーン上にある個別のコンポーネントへの参照をDIパターンで注入することができます!!

紹介しないこと

VContainer または Zenjectの基本的な使い方は解説しません!
Interfaceの書き方や実装方法については解説しません!

動作環境

Unity 2021.3.13f
Extenject 9.2.0 (Zenject) or VContainer 1.12.0

おおまかな流れ

以下の流れにそって解説していきます!
途中、VContainerとZenjectで別々に解説します。使用しているフレームワーク版の内容を参照してください。
mokuji.jpg

MonoBehaviourを継承した型を用意する【共通】

収集したいMonoBehaviour継承型を用意しましょう!
このとき、別途IBindableというInterfaceを用意して、実装しておいてください。
後ほど、このInterfaceを目印にしてContainerに登録していきます。

VContainerで実装する場合は、Interfaceの中身は空で大丈夫です。
Zenjectを使う場合は以下の通り、GameObjectを参照できるようにしてください!

    public interface IBindable
    {
        //Zenjectの場合は、GameObjectを参照できるようにする。
        GameObject gameObject { get; }
    }

続いてMonoBehaviourの用意です。
今回は仮に、SpriteRendererのSpriteを置き換えるクラスを用意してみます。
索引用の符丁をつけておきたいので、Nameというプロパティを用意しておきましょう!

    [RequireComponent(typeof(SpriteRenderer))]
    public class SpriteView : MonoBehaviour , IBindable
    {
        [SerializeField]
        private string _name = "name";
        public string Name => _name;

        private SpriteRenderer _spriteRenderer;
        private SpriteRenderer SpriteRenderer => 
            _spriteRenderer?_spriteRenderer:_spriteRenderer = GetComponent<SpriteRenderer>();

        public void ChangeSprite(Sprite sprite)
        {
            SpriteRenderer.sprite = sprite;
        }
    }

以上でクラスの用意は完了です!

Inspectorで各コンポーネントに名前をつける【共通】

先程作ったクラスをGameObjectにアタッチして、
NameFieldに索引名を登録しておきましょう。
今回は、複数のコンポーネントを索引可能にする記事ですので、
同じクラスをいくつかのGameObjectへアタッチして名前をつけておいてください。
(後述しますが、名前は被っていても動作します)

sampleInspecter.png

LifeTime内で必要な型を収集、Registerする【VContainer】

こちらはVContainer使用時の内容です。Zenject利用の場合はこちら

それではヒエラルキー上のコンポーネントを登録していきましょう!
最初に注意点です。RegisterComponentをそのまま使ってはいけません!

VContainerは、
RegisterComponent/RegisterInstanceを使ったインスタンスを、Lifetime.Singletonとして登録します。
そのため、2つ以上同じクラスを登録しようとすると競合を起こしてしまいます。
以下のようにして、自前でIBindableの配列を作り、配列全体を一つのコンポーネントとしてRegisterしましょう。

    public class SampleScope : LifetimeScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
            //このオブジェクトの下層にあるすべてのGameObjectからIBindable実装コンポーネントを取得。
            IBindable[] bindables =
             this.transform.GetComponentsInChildren<IBindable>(includeInactive:true);
            
            //配列をそのまま一つのインスタンスとして登録
            builder.RegisterInstance(bindables)
                .AsSelf();
        }
    }

索引用クラスを用意し、収集したクラスを索引可能にする【VContainer】

こちらはVContainer使用時の内容です。Zenject利用の場合はこちら

それでは、別途索引用のクラスを用意していきましょう!
登録した名前に応じて適切なインスタンスを返す索引クラスです。
Dictionaryに入れても良いのですが、今回はLookupというデータ型を使用します。

Lookupは一つのキーに対して複数の値を保持できるデータ型です。
また、一度生成すると後から内容を変更できません。いずれの特性も今回のケースにマッチします!

▼Lookupのイメージ図

それでは実装です。

    public class SpriteViewLookup
    {
        private Lookup< string , SpriteView > _lookup { get; }

        [Inject]
        public SpriteViewLookup (IBindable[] array)
        {
            _lookup = (Lookup<string, SpriteView>)array
                .OfType<SpriteView>() //SpriteViewにキャスト可能なもののみキャストして返す
                .ToLookup(x => x.Name); //NameをキーにしてLookup型に変換する
        }
        
        //キーを指定してGetできるようにする
        public IEnumerable<SpriteView> Get(string name) =>
             string.IsNullOrEmpty(name) ? null : _lookup[name]; 
        
        //念のため全要素を返すGetAllも作っておく
        public IEnumerable<SpriteView> GetAll() => _Lookup.SelectMany( x => x );
    }

作ったクラスをLifeTimeに登録しましょう!!

    public class SampleScope : LifetimeScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
            //~IBindableのBindは省略~

            builder.Register<SpriteViewLookup>(Lifetime.Singleton)
                .AsSelf();
        }
    }

これで準備完了です!

Zenject版の記事が続くので、こちらに飛んでください!

MonoInstallerで必要な型を収集、Bindする【Zenject】

こちらはZenject使用時の内容です。VContainer利用の場合はこちら

ZenjectではAsCached()を設定することで複数のゲームオブジェクトを直接Bindすることができます!
以下のように登録しましょう。

public class CallingInstaller : MonoInstaller
{
    
    public override void InstallBindings()
    {
        foreach( var bind in _grandParent.GetComponentsInChildren<IBindable>(includeInactive:true) )
        {
            Type bindType = bind.GetType(); // IBindableを実装している具象クラスの型を取得
        
            Container
                .BindInterfacesAndSelfTo(bindType) //具象クラスをそれ自身とInterfaceでバインド
                .FromComponentOn(bind.gameObject) //具象クラスがアタッチされているGameObjectから取得する
                .AsCached();
        }
    }
}

索引用クラスを用意し、収集したクラスを索引可能にする【Zenject】

こちらはZenject使用時の内容です。VContainer利用の場合はこちら

それでは、別途索引用のクラスを用意していきましょう!
登録した名前に応じて適切なインスタンスを返す索引クラスです。
Dictionaryに入れても良いのですが、今回はLookupというデータ型を使用します。

Lookupは一つのキーに対して複数の値を保持できるデータ型です。
また、一度生成すると後から内容を変更できません。いずれの特性も今回のケースにマッチします!

▼Lookupのイメージ図

それでは実装です。

    public class SpriteViewLookup
    {
        private Lookup<string, SpriteView> _lookup;

        [Inject]
        public SpriteViewLookup( IEnumerable<SpriteView> spriteViews )
        {
            _lookup = (Lookup<string, SpriteView>)spriteViews
                .ToLookup(x => x.Name);
        }

        //キーを指定してGetできるようにする
        public IEnumerable<SpriteView> Get(string name) =>
            string.IsNullOrEmpty(name) ? null : _lookup[name]; 
        
        //念のため全要素を返すGetAllも作っておく
        public IEnumerable<SpriteView> GetAll() => _Lookup.SelectMany( x => x );
    }  

続いて、作った索引クラスをBindしておきましょう!

public class SampleInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        //~IBindableのBindは省略~

        Container
            .Bind<SpriteViewLookup>()
            .FromNew() // インスタンスを生成する
            .AsSingle(); //索引用クラスはシングルトンにする
    }
}

完成です!!

索引クラスを注入して、使いたいところで使う【共通】

それでは使ってみましょう!

    public class ChangeBack
    {
        private SpriteViewLookup _Lookup;
       
        [Inject]
        public ChangeBack(SpriteViewLookup lookup)
        {
             _Lookup = lookup;
        }

        public void ChangeBack(Sprite sprite)
        {
            var defaultBacks = lookup.Get("DefaultBack");
            
            //IEnumerableのためforeachで回して処理することができる
            //同名コンポーネントが複数ある場合はすべてに変更を反映する
            foreach(var back in defaultBacks)
            {
                back?.ChangeSprite(sprite);
            }
        }
   }  

これでヒエラルキー上のコンポーネントを索引して使用することができます!

おまけ:索引型をジェネリックにしてみる

理解しやすいようにここまで具象クラスを例に取ってきましたが、
実際には機能ごとに切り出したInterface型で索引クラスを作っておくと良いと思います。
ただ、そうなると索引型を作るのが一苦労ですよね?

ジェネリックでベースクラスを作って楽しましょう!

【VContainerの場合】

    public interface INameable
    {
        string Name { get; }
    }

    public class NameableInterfaceLookupBase<T> : ILookupEnumerable<T> 
        where  T : INameable
    {
        private Lookup<string, T> _Lookup { get; }
        
        [Inject]
        protected NameableInterfaceLookupBase(IBindable[] list)
        {
            _Lookup = (Lookup<string, T>)list
                .OfType<T>()
                .ToLookup( x => x.Name ); //TをINamable継承型に制約することでx.Nameが書ける
        }

        public IEnumerable<T> Get(string name) => name == null ? null : _Lookup[name];

        public IEnumerable<T> GetAll() => _Lookup.SelectMany( x => x );

    }

【Zenjectの場合】

    public interface INameable
    {
        string Name { get; }
    }

    public class NameableInterfaceLookupBase<T> : ILookupEnumerable<T>
     where  T : INameable
    {
        private Lookup<string, T> _Lookup { get; }
        
        [Inject]
        protected NameableInterfaceLookupBase(IEnumerable<T> list)
        {
            _Lookup = (Lookup<string, T>)list
                .ToLookup( x => x.Name ); //TをINamable継承型に制約することでx.Nameが書ける
        }

        public IEnumerable<T> Get(string name) => name == null ? null : _Lookup[name];

        public IEnumerable<T> GetAll() => _Lookup.SelectMany( x => x );

    }

はい! これでベースクラスを継承して、Tを書き換えるだけで索引クラスが作れてしまいます。
大変楽になりました!!

※個別のコンポーネントにもINameableを実装しておいてください!

まとめ

2つのDIパターンで、ヒエラルキー上のゲームオブジェクトを整理、索引する方法を紹介しました!
DIパターンはとても便利なのですが、
特定クラスの複数インスタンスをDIパターンだけで管理しようとするとごちゃごちゃしてしまいます。

索引クラスなどを作って、便利に使い分けていきたいですね!

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?