Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
SimonNordon4 opened this issue Apr 6, 2023 · 3 comments

Comments

@SimonNordon4
Copy link

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
Copy link
Author

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.

@TMPxyz
Copy link

TMPxyz commented Oct 4, 2024

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;
Loading

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.

@SimonNordon4
Copy link
Author

That's a good point, and you're correct, it would be a bad idea to destroy an object that has dependents. I think what I should have said is that it doesn't make sense to re-inject a scope.

In traditional IoC Container you can create and destroy scopes easily, so long as it's not the Singleton Scope, but in Unity we don't want to do that because object instantiation is really expensive, as opposed to a traditional application.

I'm even a little confused by original post (was over a year ago) but a simpler break down is.

  • I want to instantiate an unknown number of game objects at run time.
  • I give each of these gameobjects their own scope, so that it's responsible for it's own dependencies.
  • I can also inject these local game object scopes with external dependencies (like a game manager for example)

That works all nice and dandy, but we're dealing with Unity, so we want to pool our Game Objects.

  • When we don't need the gameobject, and return it to the pool, we dispose all the dependencies.
  • However that means we need to somehow 'force reinject' the game-object when we release it from the pool.

Needless to say I don't use Zenject anymore, I don't think it plays nicely with Unity and should probably be reserved for high level logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants