Zenject icon indicating copy to clipboard operation
Zenject copied to clipboard

"Re-injecting" dependencies into a prefab when it's spawned from a MemoryPool?

Open SimonNordon4 opened this issue 2 years ago • 3 comments

My goal is to have a somewhat versatile GameObject that contains the following features:

  • Has its own GameObjectContext and installer.
  • Can be placed in the scene or created at runtime.
  • Can have dependencies passed to it whenever it is spawned.

From my research it's possible to pass new dependencies to a Prefab using a Factory, and resolving a monoinstaller from the SubContainer of the prefab. However this doesn't work if the prefab is memory pooled, because the prefab is created at the start of the game, it's container already created and cannot be recreated after the fact.

The documentation says to use the Facade pattern. Where by we manually pass these new dependencies to a facade monobehaviour on the prefab. The only issue with this, is that all other mono behaviours on the prefab will now have to reference the facade for the new data, which kind of defeats the whole purpose of Zenject, seeing as we already "kind of" have a reference to the dependencies via the [Inject] keyword we shouldn't also have to reference the facade.

I've managed to get around the issue somewhat, and thought this solution might help some people:

using System;
using System.Linq;
using UnityEngine;

namespace Zenject.UniversalObject.Example
{
    /// <summary>
    /// Game installer to be used by the SceneContext.
    /// It can ScriptableObject, Prefab or MonoInstaller.
    /// </summary>
    public class GameInstaller : ScriptableObjectInstaller<GameInstaller>
    {
        [SerializeField] private GameObject prefab;
        public override void InstallBindings()
        {
            Container.BindFactory<SomeData,Prefab,Prefab.Factory>().
                FromPoolableMemoryPool<SomeData,Prefab,Prefab.Pool>(pool => pool
                    .WithInitialSize(10)
                    .FromSubContainerResolve()
                    .ByNewContextPrefab(prefab));
        }
    }

    /// <summary>
    /// Spawner gets the Prefab.Factory injected by the SceneContext
    /// which was bound in the GameInstaller.
    /// </summary>
    public class PrefabSpawner : MonoBehaviour
    {
        [Inject]private Prefab.Factory _prefabFactory;
        
        public void SpawnPrefab()
        {
            var newData = new SomeData();
            newData.spellName = "Foo";
            newData.damage = 5f;
            
            _prefabFactory.Create(newData);
        }
        
        public void DeSpawnPrefab(Prefab prefab)
        {
            prefab.Dispose();
        }
    }

    /// <summary>
    /// Random data for demonstration purposes. This can be anything, including ints, strings etc.
    /// </summary>
    public class SomeData
    {
        public string spellName;
        public float damage;
    }
    
    /// <summary>
    /// Prefab Installer to be used on the GameObjectContext component on the prefab.
    /// It can ScriptableObject, Prefab or MonoInstaller.
    /// </summary>
    public class PrefabInstaller : ScriptableObjectInstaller<PrefabInstaller>
    {
        public override void InstallBindings()
        {
            Container.Bind<SomeData>().AsSingle();
        }
    }
    
    /// <summary>
    /// This is the meat and bones of the this system.
    /// Being an IPoolable Monobehaviour, it will be enabled and disabled instead of created and destroyed.
    /// We nest the Factory and Pool inside for convenience.
    /// </summary>
    [RequireComponent(typeof(GameObjectContext))]
    [RequireComponent(typeof(ZenjectBinding))]
    public class Prefab : MonoBehaviour, IPoolable<SomeData,IMemoryPool>, IDisposable
    {
        private IMemoryPool _pool;
        private DiContainer _container;
        private MonoBehaviour[] _dependents;

        private void Awake()
        {
            // We need a reference to the container so that we can use it to Inject mono-behaviours later.
            _container = GetComponent<GameObjectContext>().Container;
            var allComponents = GetComponentsInChildren<MonoBehaviour>();
            
            // We get every mono behaviour in the prefab, excluding Zenject Specific behaviours.
            _dependents = allComponents
                .Where(x => x is not (GameObjectContext or DefaultGameObjectKernel or ZenjectBinding or MonoInstaller))
                .ToArray();
        }

        public void OnSpawned(SomeData data, IMemoryPool pool)
        {
            _pool = pool;
            
            // After spawning, we rebind the new data. Rebind as much data as is required.
            _container.Rebind<SomeData>().FromInstance(data);
            
            // Then Inject every mono behaviour in the prefab with the new GameObjectContext containers bindings.
            // Zenject says this is bad practise, but there seems no other way to it.
            foreach (var dependant in _dependents)
            {
                _container.Inject(dependant);
            }
        }
        
        public void OnDespawned()
        {
            // Clean up code, like Transform.Position = Vector3.zero
        }

        // Return to the pool when the object is disposed of (instead of being destroyed)
        public void Dispose()
        {
            _pool.Despawn(this);
        }
        
        public class Factory : PlaceholderFactory<SomeData, Prefab>
        {
            
        }
        public class Pool: MonoPoolableMemoryPool<SomeData, IMemoryPool, Prefab>
        {
            
        }
    }
    
    /// <summary>
    /// Assuming this is attached to the prefab, it's dependencies will be updated with the new bindings!
    /// </summary>
    public class ExampleDataConsumer : MonoBehaviour
    {
        [Inject] private SomeData _someData;
        
        private void OnEnable()
        {
            Debug.Log(_someData.spellName);
            Debug.Log(_someData.damage);
        }
    }
}

The other solution is to simply only use reference types for the Bindings and update their properties / fields, which is what I'll probably end up doing.

However I was wondering if there was other ways to approach the issue?

SimonNordon4 avatar Apr 06 '23 12:04 SimonNordon4

I think I solved my own problem.

I realize that if we take away monobehaviours. We would never re-inject dependencies into an object, it would be much easier to just destroy and create a new one. Extending that logic to Unity, it doesn't make sense to Re-inject an object. If an object does need New Dependencies, that it should be treated as a new object.

SimonNordon4 avatar Apr 07 '23 03:04 SimonNordon4

We would never re-inject dependencies into an object, it would be much easier to just destroy and create a new one.

Hi, Thanks for your Rebind<SomeData>().FromInstance(xxx) solution, I was trying to re-inject some of my GameObjects too. Your solution is useful.

I'm confused by your comment of "destroy and create a new one", which doesn't sound quite reasonable for me.

flowchart TB;
A --> B;
a & b & c --> A;

Assume A depends on B, and all other objects depend on A.

if we want to make A depends on another instance of B, your suggestion would be destroying A and recreate a new instance of A, which would in turn requires destroying and recreate all the 'a b c' instances. That would be unreasonable.

For example, if my game has 1000 levels (each level has a json setting file), and on every level start, I want to inject the level setting into my MapController to populate the map and set the global settings. I think rebind and inject is the reasonable solution for such use cases.

TMPxyz avatar Oct 04 '24 07:10 TMPxyz