[Zenject] Global factory that spawns object in scene context

Good morning,

Context
I have this factory

    public class ViewFactory : IViewFactory
    {
        private readonly DiContainer _container;

        [Inject]
        public ViewFactory(DiContainer container)
        {
            _container = container;
        }

        public GameObject CreateView(ViewData viewData, Transform parent)
        {
            return _container.InstantiatePrefab(viewData.ViewPrefab, parent);
        }
    }

installed in the ProjectContext as it’s a dependency of my ViewService class that is also installed there.
Injected container in that factory is of course global container which make my factory always create my views in the ProjectContext.

Problem
My problem is, that my View created by this factory have a dependency of a ViewModel class that is installed in the scene context, which makes it impossible for zenject to inject it.
I understand that it’s not possible as view and its view model lives in a different containers (and it’s a parent container class that requires sub-container dependency, I’m aware that if it was the other way around the problem will probably not occur)

Here is how I install my bidnings in ProjectContext:

Container.BindInterfacesAndSelfTo<ViewFactory>().AsSingle();
Container.BindInterfacesAndSelfTo<ViewsService>().AsSingle();

I install my ViewModels in a same manner but just on the SceneContext installer, nothing fancy here.

So I’m just wondering

  • Can I somehow solve that problem with different way of installing my stuff or some other Zenject Magic tricks i’m not aware of?

  • Is possible to have a factory installed in the ProjectContext that will spawn objects in the current context instead of a ProjectContext?*

*Of course I could do some kind of provider of context to that factory, make sure it’s provided before it creates the object but since i’m trying to learn more about Zenject here, i’m just curious about other perspectives to this problem

Some constraints
I’m open to all ideas, however, there are some things that can’t be changed and I would appreciate if you take them into your considerations before answering

  • Zenject stays (I won’t change it to other DI solution)

  • ViewService have to be installed in ProjectContext

  • ViewService have to be responsible for delegating the “Create view” task (can delegate that to factory, can spawn it itself, but that creation has to go through this service)

  • ViewModel has to stay as a dependency of a View not the other way around

  • ViewModel installation can’t be moved to ProjectContext (unless there is a way that it’ll still be only visible in a scene context)

Thank you very much for your time!

I would recommend simply using the built-in dependency injection that Unity’s scenes (coupled with ScriptableObjects) already provide you.

Zenject might be useful when YOU are making an application, but in Unity that is not the case.

Your code is not the application. Unity is the application. Your code is just a minor guest at the party:

https://discussions.unity.com/t/882111/2

For code-driven factory needs in Unity3D, I always use this pattern to keep a handle on my dependencies and order of initialization:

Factory Pattern in lieu of AddComponent (for timing and dependency correctness):

https://gist.github.com/kurtdekker/5dbd1d30890c3905adddf4e7ba2b8580

1 Like

Even though I’m experienced with Zenject I find it hard to give you the solution considering your strict rules, apart from some ideas you already thought yourself (like making provider to scene).

The thing I don’t understand is why ViewFactory/ViewService are binded in ProjectContext, when you clearly want to instantiate in the current (Scene) context? ViewModel already exists in SceneContext, why do you try to move the rest in the ProjectContext?
Do you want to store some cross-scene data here? Make another service, bind it in ProjectContext and make ViewService and ViewFactory communicate with it from SceneContext?

For me it seems that you try to make Zenject work against its design.
ProjectContext is for things like Serialization, Ads, Notifications, SceneLoading, Time, MetaGame etc. Ideally it shouldn’t know about Scene stuff.
Make “scene” subscribe via events if you want “project” to “call” something.

3 Likes

To be honest I was expecting this answer from you. Before I posted that question I was trying to find an answer in the forum and it seems like your answer to EVERY single Zenject question is : Don’t use zenject.
I need to ask - if you are having a problem in Unity do you switch to Unreal or try to solve it? :slight_smile:
Ok, let’s leave being an asshole aside. I understand you don’t like Zenject and you think this framework is not for making games, but you need to understand and accept one very simple thing: A LOT of game companies are using Zenject and they are expecting you to know it, so let’s just agree to disagree here that Zenject is not good for Unity.

@Lekret
Some more context to help you understand why they are binded like that.
So I also have a SceneService that is responsible for switching scenes. In the scenes I do have an UI that is build from views. When I want to switch the scene I want to first await for all the views to be disposed and closed properly, in order to do that my SceneService needs to depend on the ViewService to be able to wait for all the present views to be disposed.

So, ViewService is used for load/unload some views in the UI, but SceneService is used for switching a bigger context, and I want to ensure that it’ll wait for the views to be properly Disposed before it changes it.

So yeah, if you think still this is all organised against the Zenject principles this is what I wanted to hear, to be frank. As I said, all of that is more an open question, encouragement to share some knowledge. I already moved on with a simple context provider for my factory, but that probably is not the best solution. I really want to hear your opinion here and how it can all be structured differently (even if you have to butcher some of the constraints)

Gentlemens, thank you very much for your answers, @Kurt-Dekker hope you won’t take my harsh answer personally, but please let’s not make this thread a “why you should not use zenject” thread. Thank you!

2 Likes

Please don’t think I take any of this personally. I don’t care. It is all 1s and 0s, nothing else.

You posted this wall of object word soup:

ViewModels, containers, sub-containers, dependencies, parents… just word soup to me.

What are you TRYING to do, like what is your ACTUAL problem, and why would you reach for Zenject to do it when you’re working in Unity?

That’s all my response was trying to point out. To me it seems to be regularly-recurring baffling behaviour that I see posted about regularly here: people think Zenject is the only hammer and everything looks like a nail.

If you’re happy trying to pound that square peg into that round hole, by all means continue. If I had any direct help I could offer I would. The fact remains that in my experience, Zenject has NEVER solved any of my gamedev problems in Unity, and in return it has caused many more problems that never exist when I’m NOT using Zenject.

Keep in mind that every C# object derived from UnityEngine namespace is “special,” not really truly a C# object in the purest sense but a special “scripting shim” into the native engine side. As an engineer, it feels like one should plan and act accordingly to this reality.

All the best in your project.

2 Likes

If you don’t want to use a provider and with your constraints, I’d take a look to scene decorator

https://github.com/modesttree/Zenject?tab=readme-ov-file#scenes-decorator

So, something like that.

Instead of using a project context, use a scene context acting like parent. This installer installs a dummy ViewModel

With scene parenting, load your scene, now you have one scene called main, and other with your current scene.
This current scene uses a decorator installer that rebind ViewModel to the real value in the main installer.

Another possibity is use [InjectOptional], but since the factory is living in the projectContext, you have to inject the projectContext in the sceneInstaller and bind there the viewModel, something like this:

class SceneInstaller: MonoInstaller
{
[Inject] private DiContainer projectContext;

   public override void InstallBindings()
   {
    
projectContext.Bind<ViewModel>().ToSelf().AsSingle()
   }
}

So, to consider.

  • Scene Parenting
  • Scene Decorator
  • [InjectOptional]
  • Different Constructors so the dependencies can be solved for the two different scenarios, with and without a ViewModel bound
  • LazyInject
1 Like

Thanks for sharing more context.
In that case I have different ideas and solutions:

  1. I see you want to make your SceneService “smart” and add extra stuff including scene stuff. In that case, as again, you can move SceneService to SceneContext too. If it doesn’t do anything cross-scene, it will be able to wait until all Views are disposed, call SceneManager and cease to exist, and new SceneService will be created in new scene.
    If something from ProjectContext is relying on SceneService, then it again becomes a problem, but that would be weird. Usually all signals to change scene emerge from input, UI input or game logic and that’s not something that lives in ProjectContext, usually.

  2. Not sure how SceneService is accessing views, if it’s some ProjectContext ViewProvider simply for holding current views and ViewService is responsible for registering and deregistering, then it’s fine solution.
    If you are accessing container directly in some way, then I would invert dependencies and make collection of delegates or interfaces, something abstract which can receive information about changing scene and provide information where or not they finished disposing.

So you either keep SceneService rich and move it to Scene where it can access all required infromation or you make it more “high-level” and thin and let scene be in control of when and how scene changes.

1 Like

I’m not sure about SceneParenting and SceneDecorator, because never really used them, but I would advice OP not using [InjectOptional], different constructors and LazyInject. They are sure solving a problem, and that’s fine, but I consider them a code smell and an indication of greater design issue. I used them a couple of times, but refactored out later, because even on a smaller scale they will introduce a lot of error prone null checks and if-elses. There’s always the way to refactor things so you don’t need them.
I don’t want to sound like code-nazi, good code != good game, but if we are speaking about what is “right” and what is “clean”, then I think they are not really.

1 Like

And the corollary, bad code != bad game

And also that the user never cares about your code, never.

@Lekret
You are absolutely right with moving SceneService to the SceneContext, I have no idea why I didn’t even think of it, feeling super dumb now =). I thought that since it’s a service that is responsible for switching scenes it has to live in the ProjectContext as it’s a more “global” thing, but, as you said, it can actually happily exist in the scene context and change scene only once in its life time, then another SceneService comes into play, as it’s another scene context. Zenject forces you to look at some things from a way different perspective, that is mind-opening, thank you all for help here!

1 Like

When all your problems are about how your tools interact with each other, and none of them relate to your actual video game, then maybe the tools are the problem?

DI frameworks is worship of abstraction taken to absurd levels. There’s nothing clean about needing three classes (ViewFactory, DiContainer, ViewData) in order to achieve Instantiate(prefab, parent);.

And don’t give us this bullshit about “I have questions about Zenject, if you don’t like it, don’t reply”. I have talked to young, impressionable programmers that wonder why their job applications keeps getting rejected, when they’re writing this fantastic, decoupled, clean code. I have to break it to them that spending 2-3000 lines of code to build containers and factories and injections and installers that makes their code unreadable to most people is not a very good idea, and they have to unlearn those habits and write simple, understandable code before they’re ever going to be hired anywhere, or finish a game for themselves.

So, yeah, every time you post about Zenject or other DI frameworks you’re going to get pushback, because it’s a fucking horrible idea, it makes your code into insane garbage, and we don’t want anyone new seeing you write the insanity in your original post and think that that’s what programming is, or should be.

2 Likes

DI frameworks aren’t problems here, they can be pretty simple behaving as a better replacement for ServiceLocator, can be an over-abstracted pile of garbage, it depends on what you bind and what objects you have. DiContainer is a couple of installers, 1% of your game, the zoo of classes is the other 99%.

I’ve seen the other side, with 1000 lines classes which make task estimates x4, lack of any sane architecture which only lead to hacks and workarounds in attempt to fix bugs which shouldn’t even be there, and heard even crazier GameManager type of shit from other developers.

My point is that on this spectrum between “Let’s make whole game MVC” and “YOLO single class game” DI containers don’t change that much. By the definition and the rules of DI, game is mostly separate from it.

And in the end, the guy is just learning. We all fell in that I also fell into this trap as beginners, over-abstracting things, making universal perfect code. It all comes with experience (I hope so) and it’s okay to do crazy stuff while learning new tools. Btw I have a specific Unity project for all kind of inapplicable ideas that comes to my head.
At first job I had MVC, interfaces everywhere, and other stuff the project probably didn’t need. As a more experienced developer I can now reflect and decide myself where I need abstraction for something or where I can cut corners and deliver stuff faster, but this additional knowledge about what patterns can I use or how games can be built didn’t harm me at all, it’s not like I forget how to write stuff in a more simple way if I need to.

4 Likes

Straw man fallacy. There was one problem :smile:

IoC containers are just a tool to help with dependency injection, and not related to the number of abstractions in a project.

One could say the same thing about serialized fields: it’s a complicated, multi-threaded mess, with a huge number of edge-case rules. Different results based on object types, member types, access modifiers, SerializeField, SerializeReference, SerializableAttribute, base class, ISerializationCallbackReceiver, generics, Unity versions. Just making a simple cross-scene reference is a complicated endeavor.

On the flip side, if you take a look at job offerings in LinkedIn, you can see knowledge of dependency injection frameworks being listed as a requirement / plus for quite a few of them. Just because a tool can be misused by people who are just learning the ropes, doesn’t mean the tool can’t be useful to learn :slight_smile:

Being able to write unit tests is very desirable for many game companies, and dependency injection is the industry standard solution for enabling this.

3 Likes

The zenject docs are outdated and give odd advice, I wouldn’t follow everything they suggest, especially when it comes to factories.

For MVC just use a pool, this allows you to inject everything you need and keep a reference to your controller when needed.

You’ll just need to write the zenject .Spawn() logic. Whenever you .Spawn() a new object from the pool it calls the Reinitialize() override.

WindowContext would be a struct that you create for initializing your view with whatever data you need.

public class WindowInstaller : MonoInstaller {
    public GameObject viewPrefab;

    public override void InstallBindings() {
        Container.BindMemoryPool<WindowController, WindowControllerPool>()
            .WithInitialSize(10)
            .FromSubContainerResolve()
            .ByNewPrefabMethod(viewPrefab, InstallController)
            .UnderTransformGroup("Controllers");
        Container.Bind<IWindowHandler>().To<WindowHandler>().AsSingle(); 
    }

    private void InstallController(DiContainer subContainer) {
        subContainer.Bind<WindowModel>().AsSingle();
        subContainer.Bind<WindowView>().FromComponentOnRoot().AsSingle();
        subContainer.Bind<WindowController>().AsSingle().NonLazy();
    }
}
public class WindowControllerPool : MemoryPool<WindowContext, Transform, WindowController>
{
    protected override void Reinitialize(WindowContext context, Transform parent, WindowController item)
    {
        item.SetContext(context);
        item.view.transform.SetParent(parent, false);
        item.SetEnabled(true);
    }
}

...
    public WindowController SetContext(WindowContext context, Transform parentTransform)
    {
        var controller = _controllerPool.Spawn(context, parentTransform);
        _controllers.Add(controller);
        controller.SetEnabled(true);
        return controller;
    }
...

Or you can just rip out the pool logic if you just want to create one MVC. The reason I suggest a pool is lets say you have a inventory and in that inventory you have your item and those items create a InventoryCellView your inventory cell view should be in control of itself, so when the item stack changes you don’t have to handle all that logic, it handles itself and updates all the view components (Text) with the model data.
Now when you have two inventories you can drag and drop the cell into either one and all you need to do is control the inventory data, you don’t have to mess with spaghetti code to try and wrangle all this up in a “manager”, it just handles itself.

" * ViewModel has to stay as a dependency of a View not the other way around"`

There should be ZERO dependency on view or your model, your references are on your controller, all of your logic should be handled by a controller, that’s the entire point of a MVC.

public class WindowController : IInitializable, IDisposable
{
    private readonly WindowModel model;
    public WindowView view { get; }

    public WindowController(WindowModel model, WindowView view)
    {
        this.model = model;
        this.view = view;
    }

    [Inject]
    public void Initialize()
    {
        model.OnIsEnabledChanged += view.OnIsEnabledChanged;
        view.OnIsDirtyChanged += model.OnIsDirtyChanged;
    }
...

Written by hand, may be some mistakes.

I took the time to write it in the editor for anyone else looking for this complex problem, but haven’t tested :smiley:

public class WindowInstaller : MonoInstaller {
    public GameObject viewPrefab;

    public override void InstallBindings() {
        Container.BindMemoryPool<WindowController, WindowControllerPool>()
            .WithInitialSize(10)
            .FromSubContainerResolve()
            .ByNewPrefabMethod(viewPrefab, InstallController)
            .UnderTransformGroup("Controllers");
        Container.Bind<IWindowHandler>().To<WindowHandler>().AsSingle(); 
    }

    private void InstallController(DiContainer subContainer) {
        subContainer.Bind<WindowModel>().AsSingle();
        subContainer.Bind<WindowView>().FromComponentOnRoot().AsSingle();
        subContainer.Bind<WindowController>().AsSingle().NonLazy();
    }
}
public class WindowControllerPool : MemoryPool<WindowContext, Transform, WindowController>
{
    protected override void Reinitialize(WindowContext context, Transform parent, WindowController item)
    {
        item.SetContext(context);
        item.view.transform.SetParent(parent, false);
        item.SetEnabled(true);
    }
}
//View Initalize data
public struct WindowContext
{
    internal Vector2 position;
    internal int index;
    internal bool isActive;
    internal bool isEnabled;
}

public class WindowHandler : IWindowHandler
{
    [Inject]
    private WindowControllerPool _controllerPool;

    private readonly List<WindowController> _controllers = new List<WindowController>();

    public WindowController SetContext(WindowContext context, Transform parentTransform)
    {
        var controller = _controllerPool.Spawn(context, parentTransform);
        _controllers.Add(controller);
        return controller;
    }

    public WindowController[] SetContext(WindowContext[] contexts, Transform parentTransform)
    {
        while (_controllers.Count < contexts.Length)
        {
            var controller = _controllerPool.Spawn(contexts[_controllers.Count], parentTransform);
            _controllers.Add(controller);
        }

        for (int i = 0; i < contexts.Length; i++)
        {
            _controllers[i].SetContext(contexts[i]);
            _controllers[i].SetEnabled(true);
        }

        for (int i = contexts.Length; i < _controllers.Count; i++)
        {
            _controllerPool.Despawn(_controllers[i]);
        }

        _controllers.RemoveRange(contexts.Length, _controllers.Count - contexts.Length);

        return _controllers.ToArray();
    }

    public void RemoveController(WindowController controller)
    {
        if (_controllers.Contains(controller))
        {
            _controllers.Remove(controller);
            _controllerPool.Despawn(controller);
        }
    }

    private void OnDestroy()
    {
        foreach (var controller in _controllers)
        {
            _controllerPool.Despawn(controller);
        }
        _controllers.Clear();
    }
}

When you want to “create” in the sense of configuring and enabling you just do your

var myWindowController = IWindowHandler.SetContext(myData, myCanvas)