Skip to content

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

@SimonNordon4

Description

@SimonNordon4

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions