Search Unity

Question Zenject questions

Discussion in 'Scripting' started by Neriad, Jan 4, 2023.

  1. Neriad

    Neriad

    Joined:
    Feb 12, 2016
    Posts:
    125
    Hey there!

    I'm getting to Zenject and I have questions I can't find the answer to. I'm starting to understand the whole concept, but it don't get some stuff related to MonoBehaviours with Zenject.

    Basically, the guy that introduced me to Zenject was using a way that the Zenject documentation does not recommend, using mostly field injection. As I went through the documentation, I understood that this is not the best practice, and that it's better to use Constructor/Method injection instead.

    What I was doing until now :

    Code (CSharp):
    1. public class MyMonoInstaller : MonoInstaller
    2. {
    3.     [SerializeField] private FlowManager _flowManager;
    4.     [SerializeField] private UIManager _uiManager;
    5.     [SerializeField] private CameraManager _cameraManager;
    6.  
    7.     public override void InstallBindings ()
    8.     {
    9.         Container.Bind<MyNativeClass>().AsSingle().NonLazy();
    10.         Container.Bind<FlowManager>().FromInstance(_flowManager).AsSingle().NonLazy();
    11.         Container.Bind<UIManager>().FromInstance(_uiManager).AsSingle().NonLazy();
    12.         Container.Bind<CameraManager>().FromInstance(_cameraManager).AsSingle().NonLazy();
    13.     }
    14. }
    15.  
    16. public class GameStarter : MonoBehaviour
    17. {
    18.     [Inject] private MyNativeClass _myNativeClass;
    19.     [Inject] private FlowManager _flowManager;
    20.     [Inject] private UIManager _uiManager;
    21.     [Inject] private CameraManager _cameraManager;
    22.  
    23.     private void Start ()
    24.     {
    25.         _myNativeClass.Init();
    26.         _flowManager.Init();
    27.         _uiManager.Init();
    28.         _cameraManager.Init();
    29.     }
    30. }
    31.  
    32. public class MyNativeClass
    33. {
    34.     public void Init ()
    35.     {
    36.      
    37.     }
    38. }
    And it worked fine. But now that I do it the "good way", I don't understand how this code above could even work.
    What I do now :

    Code (CSharp):
    1. public class MyMonoInstaller : MonoInstaller
    2. {
    3.     [SerializeField] private FlowManager _flowManager;
    4.     [SerializeField] private UIManager _uiManagerPrefab;
    5.     [SerializeField] private CameraManager _cameraManagerPrefab;
    6.  
    7.     public override void InstallBindings ()
    8.     {
    9.         Container.Bind<MyNativeClass>().AsSingle().NonLazy();
    10.         Container.Bind<FlowManager>().FromInstance(_flowManager).AsSingle().NonLazy();
    11.         Container.Bind<UIManager>().FromComponentInNewPrefab(_uiManagerPrefab).WithGameObjectName("UIManager").AsSingle().NonLazy();
    12.         Container.Bind<CameraManager>().FromComponentInNewPrefab(_cameraManagerPrefab).WithGameObjectName("UIManager").AsSingle().NonLazy();
    13.     }
    14. }
    15.  
    16. public class GameStarter : MonoBehaviour
    17. {
    18.     private MyNativeClass _myNativeClass;
    19.     private FlowManager _flowManager;
    20.     private UIManager _uiManager;
    21.     private CameraManager _cameraManager;
    22.  
    23.     [Inject]
    24.     public void Construct (MyNativeClass myNativeClass, FlowManager flowManager, UIManager uiManager, CameraManager cameraManager)
    25.     {
    26.         _myNativeClass = myNativeClass;
    27.         _flowManager = flowManager;
    28.         _uiManager = uiManager;
    29.         _cameraManager = cameraManager;
    30.     }
    31.  
    32.     private void Start ()
    33.     {
    34.         _myNativeClass.Init();
    35.         _flowManager.Init();
    36.         _uiManager.Init();
    37.         _cameraManager.Init();
    38.     }
    39. }
    40.  
    41. public class MyNativeClass
    42. {
    43.     public void Init ()
    44.     {
    45.      
    46.     }
    47. }
    Now I don't understand why the FlowManager even call Inject on its own class, as it's already present in the scene and I drag the reference on the MonoInstaller. I understand it does for UIManager and CameraManager as they are instantiated via Zenject, so it calls the "fake constructor". I've seen on the documentation that, when you bind via instance, you must use Container.QueueForInject() so that Zenject can do his work. But it works without doing it, so I'm a bit confused. Also, as it's already present in the scene, I'm not sure to understand what .NonLazy() does for FlowManager.

    Something else I don't understand is that I have a MonoBehaviour (let's call it UiThing) class that's inside a GameWindow, which is inside UIManager. On that UiThing, I have a method injection, and it's called, and I don't understand how it can be called, as there is nowhere any binding of UiThing on the Container. Is it somehow because UIManager is binded into the container so it also works for his childs ? Would seem strange, plus, if I do try to Inject that UiThing elsewhere, it doesn't work, so it's not binded, which seem logical.


    Can anyone help me understand ? :3

    Thanks.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,656
    Zenject and Unity? Run screaming. You have been warned.

    https://forum.unity.com/threads/arc...ableobject-monobehaviour.749114/#post-4991642

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

    https://forum.unity.com/threads/res...-problem-when-using-ioc.1283879/#post-8140583

    On top of that, basically NOTHING derived from ScriptableObject or MonoBehaviour could ever be
    new
    -ed up by Zenject. Instances of those classes MUST be made by the correct Unity factories.
     
    Bunny83 likes this.
  3. Neriad

    Neriad

    Joined:
    Feb 12, 2016
    Posts:
    125
    I'm not sure to understand why I should be running away from Zenject, but I get your point of view.

    And yes, I know that scriptable objects or monobehaviours can never be newed, though my questions on Zenject. Before deciding if it's something I wanna go with, I'd like to understand how it works, at least the basics, but on my post, there are few things I consider as strange, and I'd like to have the explanation if you have it somehow! :)
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,656
    I would recommend ignoring 100% of what a "Standard I am the application" approach to Zenject recommends and instead focus on Zenject tutorials specific to Unity and all the gotchas of being just a "little bit of guest code along for the ride inside another application."

    Also remember that anything you do in Zenject will absolutely need to "play nice" with every third party plugin and Unity package you install forever and into the future. Things like Firebase are EXTREMELY fragile finicky systems that are hard to integrate in the first place, and require precise integration. Any deviation generally ends in disaster.

    Good luck. We had a team of 14 guys who couldn't paper over all the problems.
     
  5. Neriad

    Neriad

    Joined:
    Feb 12, 2016
    Posts:
    125
    Well then, what would you recommend to architect the code of your mobile game ?
    Mostly, to avoid having singletons and coupled code, to have a clear control over the initialization flow, etc... ?
    Till now, I've tested things like an Dependency graph, the scriptable object architecture from the famous talk of Ryan Hipple, observer pattern and more. I'm just like, watching my options but I know that I like to work with the inversion of control pattern, and from what I see, it's often coupled with dependency injection.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,656
    Until you have some comfort and familiarity with how scenes and prefabs and assets profoundly affect the way your code runs, you might as well just try a bunch of approaches and see which suits you.

    I recommend structuring your project the way you feel most comfortable that works well with Unity.

    Author as much stuff as you can directly in the editor, and when you must create things out of whole cloth in code, use helpful structures such as these to keep you honest about what a particular item needs for its initialization:

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

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

    Try small tutorials from a few different places online and rip through them, get them going. Then say to yourself "With this codebase, how would I extend it to do X?"
     
  7. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    699
    Rather than using references to other game objects and scripts you can send events and let whatever is listening do their job.
     
  8. Kirayshen

    Kirayshen

    Joined:
    Jul 28, 2017
    Posts:
    23
    I like your way of thinking you are on the right path. DI Framework is must have tool to make seriouse architecture without so much pain. SOLID can not be fully achived without composition root. You will not have any problems with integrating zenject with 3d party tools also. Dont worry. It will not limit you in any way.

    ---------
    "Basically, the guy that introduced me to Zenject was using a way that the Zenject documentation does not recommend, using mostly field injection. As I went through the documentation, I understood that this is not the best practice, and that it's better to use Constructor/Method injection instead."
    ---------
    Best practices are not requirements. You can successfully use inject fields also. It is normal in these days when someone doing things wrong. Not everyone understand tools correctly or do not have time to make it properly. For example I had situations where client wanted his build in 2 hour with his "new beautiful idea" implemented. Of course i didnt have time for creating fields and then inject them in constructor in 30 different classes. I used inject fields but it is technical debt. You should return and fix this later. Why? Only one reason. Because it is a lot easer to re use code with constructor injection later if you will want to use this code without DIFramework. And if you entity is MB than reason is even simpler. You have one nice list of dependencies via method paramters. With fields you are forced to scroll to find all dependencies. There is also one reason but lets ommit it.


    ----------
    Now I don't understand why the FlowManager even call Inject on its own class, as it's already present in the scene and I drag the reference on the MonoInstaller. I understand it does for UIManager and CameraManager as they are instantiated via Zenject, so it calls the "fake constructor".
    ----------

    If I undestood your problem correctly then the situation is simple.
    You need to understand how the system works. It is easy.

    1. If your object (monobehaviour) is not created dynamicly in another words exists in the scene and it has inject fields or methods system will try to inject. Why not? :) You dont need to register your EXISTING entity anywhere to get injections you register entities (bind) only if you need to INJECT them to another entities
    2. If your object (monobehaviour) is created dynamicly and it has inject fields or methods system will not be able to inject. You need different approach. You need to make these game objects via Zenject API. FromComponentInNewPrefab is one way. You create NEW game object from prefab with zenject and thus zenject is able to inject dependecies to them.


    ---------
    I've seen on the documentation that, when you bind via instance, you must use Container.QueueForInject() so that Zenject can do his work. But it works without doing it, so I'm a bit confused. Also, as it's already present in the scene, I'm not sure to understand what .NonLazy() does for FlowManager.
    ---------


    1. Container.QueueForInject is for different things you dont need it in your case. I suggest reading about this. If you will not be able to understand then i will help you. No problem. I just dont want to write a book here :)

    2..NonLazy() is a way to create new entities. In case with FlowManager it is already exists you can ommit NonLazy. It is for entities that you are goind to create yourself throught the code.
    For UIManager it has meaning. When we create entities via zenject without NonLazy it will not create until someone will need it. For example class A needs UIManager type and awaits it within inject method
    If we want to create UIManager anyway even if our code doesnt need it we should set NonLazy.




    ------------
    Something else I don't understand is that I have a MonoBehaviour (let's call it UiThing) class that's inside a GameWindow, which is inside UIManager. On that UiThing, I have a method injection, and it's called, and I don't understand how it can be called, as there is nowhere any binding of UiThing on the Container. Is it somehow because UIManager is binded into the container so it also works for his childs ? Would seem strange, plus, if I do try to Inject that UiThing elsewhere, it doesn't work, so it's not binded, which seem logical.
    ------------

    1. If your object (monobehaviour) is not created dynamicly in another words exists in the scene and it has inject fields or methods system will try to inject. Why not? :) You dont need to register your EXISTING entity anywhere to get injections you register entities (bind) only if you need to INJECT them to another entities

    2. UIManager - like I sad you can get injections with inject attrubute you dont need anything more but to send UIManager to others you need bind UIManager in the container.

    Now lets make it simple.
    In zenject we have CONTAINER we can get entities from it with inject attribute. If your entitie is not within container you are not able to inject it to dependent classes.
    But to get injections we dont need to put entitie to container we only need to tell [inject]. THOUSE entities that we want to get should be bined in the CONTAINER.
     
    Last edited: Nov 18, 2023
    Lekret likes this.
  9. Kirayshen

    Kirayshen

    Joined:
    Jul 28, 2017
    Posts:
    23
    References:
    Hard to maintain with big team and complex project.
    1. People can make mistakes. Wrong link, they forgot link.
    2. It is hard when you try to understand from where this serilized field will get object. You need to make you advanture via entire hierarchy and project. We are not psychic. Reading code is a lot faster. Our code works from top to bottom. It is like book. (if we will not talk about parrallel computation)
    3. I will not even touch issue with SCM and links. Im not against the reference. It is great tool but very dangerous. Use minimally.

    Listeners:
    We need use events (observer) only in few situations. When we need to extend functionality without changing the code. For example that code is closed DLL. We dont have access to it. Events is almost only way to give user controll when something important happens to link our code to existed one. We can not edit initial code. We can only subscribe. This is where events are important.

    Why not everywere?
    We have three problems here (References if we talk about editor) (memory leaks it is very easy to forgot to unsubscribe) and callback hell (i dont want even to talk about it).
     
    Last edited: Nov 18, 2023
  10. D3o

    D3o

    Joined:
    Apr 10, 2021
    Posts:
    1
    Hmm, I have almost 4 years of commercial experience in Unity at many companies, and the biggest advantages of Zenject are avoiding monobehaviours so we don't inherit too much stuff and avoiding singletons, so code is easier to test. I won't argue with you guys, but yeah, Zenject is a little bit outdated, but even though I really like working with it, there are many teams, especially in the mobile industry, that use it.
     
    Lekret likes this.
  11. Lekret

    Lekret

    Joined:
    Sep 10, 2020
    Posts:
    350
    Field injections are pretty ok. Constructor method with [Inject] attribute is ok approach too. First is shorter, second is in some sense more clear, but I usually put all my field injections on top of everything including serialized fields. It's really a matter of taste, I wouldn't say it's bad practice per se.

    The thing with Zenject is it lets you avoid MonoBehaviours altogether, but I saw some people prefer to use field injections in non-MonoBehaviours also because, again, it's less typing.

    First of all I would advice to not use NonLazy. It will create or inject object right away without collecting further bindings so all bindings becomes order dependant, when DI should be order indepdendant by default.

    Normally Zenject works via "recursive resolve". Something is created, it requires dependencies, its dependencies being resolved, their dependencies being resolved and in the end your whole dependency graph is built.
    If you are creating something later in the game and their require their own custom dependencies these dependencies won't be created until requested.
    If you want to create something right away you better use IInitializable interface and Container.BindInterfacesAndSelfTo, so it will be created when container done collecting bindings, simply because container itself will request all IIninitializable.
    Simple rule of order: Register bindings -> Wait for all objects to be completely initialized -> Execute your game logic. You don't want to break it.

    By default Zenject SceneContext is injecting all objects present on scene. You need to bind them only for injecting somewhere else.
    It also injects all components on all child objects when you are binding from prefab or creating it dynamically with IInstantiator. If it doesn't work you are doing something wrong, clearly.

    The code you've shown looks mostly fine to me.
    If you have any other question or problems feel free to ask, I have plenty of experience with Zenject and use it daily.

    And don't listen to people who tell you to run away from Zenject. Unity don't offer any alternatives for architecture. And people who say "don't use Zenject" usually suggest something opinionated, i.e. they don't say anything relevant against Zenject, but just suggest to use their favorite opionated approaches. If you want to use Zenject, use it.
    The only valid critic against Zenject is that it's a bit bloated with features and you usually don't want to use it "full power". Just keep it simple and don't overcomplicate.
    DI in itself is fine for architecting your game, btw VContainer and Reflex are also good alternatives.
     
    Last edited: Jan 10, 2024
    SisusCo likes this.
  12. LightRuno

    LightRuno

    Joined:
    May 2, 2018
    Posts:
    7
    Greetings, @Lekret ! Working with Zenject for a bit, and I glad I found I can ask about it.

    Image I have architecture for 2D game:

    0) Game systems -> some systems that deals with some game logic part, it can be audio, popups, gameplay usecases, analytics, backend communication, etc.

    1) FlowController -> Knows each game system. Initializes the game and it's systems, make systems wait each others initialization if needed (when they are dependent), also Instantiates GameScreens
    2) Game Screens -> Contain some business logic and its view
    3) Game Screens SubComponents -> Some buttons with more logic incapsulated than just addListener, Some subviews to manage screen more easily

    That's what I think: I can think architecture is fine if:

    1) Flow controller knows each game system and help them initialize with proper order
    2) Game screens can inject systems that they need to use. Ideally I would like to prohibit injection of some systems to only certain game screens (like using Auth System only in screens where it needs)
    3) Game Screens SubComponents cannot inject any systems that Game screens can.
    Game Screen need to provide any dependency manually.

    Example: MainMenu inject AudioSystem, pass it into StartGameButtonComponent. Because otherwise component gets its dependencies system 2 architecture levels higher. As I get it - good practice is not to jump architecture levels and make communication only 1 to 2, 2 to 3, etc.
    Another variant: Don't even pass something into SubComponents, instead do communication Screen (subscriber) - SubComponent (event-invoker) and use all the systems in Screens only.

    There's the question:
    1) Can i prohibit to inject some systems to certain game screens? Currently I have DI Container for scene only, I Instantiate Screens with same DI container so they can use everything. To reach prohibiting injecting some of systems to only certain screens I need to use
    WhenInjectedInto

    and mention all the classes individually?
    Or create some interface like IAuthSystemAccessible? Is it practice or nobody doing that?

    2) In point 3 above currently my SubComponents can inject any game systems. It happens because as you said in your prev message - When object is instantiated (with zenject, of course), all of child game-objects are also filled it's dependencies. Isn't it counts as a bad architecture? I don't want some button to be able to use some high level things (not like audio but like some use cases for game logic for example.

    All things I mention are things that of course can be solved by "just don't use if you dont want" (dont use systems in sub-components, dont use auth systems in screen that are not related to auth flow). But I want to make the situation when you not Wanted to do it, but also not Able to do it - to prevent someone tired or less skilled or less dived into architecture enough a possibility to make a mistake in architecturing another feature. It's like using assembly definitions - When you using it and splitting view, business logic and some general systems - you just have to think how to do this, otherwise things like cyclic dependencies prevent you from coding incorrectly.
     
  13. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,321
    I think the most optimal way to isolate types from each other is by doing it on the assembly level.

    This way it becomes impossible to even write compiling code that tries to make use of systems that shouldn't be accessible. If this is only done on the DI container level, it could lead to programmers wasting time writing a component, only to find out that it doesn't work when testing it at runtime, and then potentially wasting more time debugging the problem - unless the error messages make it clear what the issue is.

    For example, UI-related components could be grouped in the assembly MyNamespace.UI, and this assembly could know about the IUICommand interface, but not the IGameCommand or IServerCommand interfaces.
     
    Last edited: Feb 19, 2024
    LightRuno, Ryiah and Bunny83 like this.
  14. Lekret

    Lekret

    Joined:
    Sep 10, 2020
    Posts:
    350
    If I understood your correctly, your whole question is about "should I restrict injection into lower level systems". In that case I would say no.
    If you are concerned about "high/low level" separation, then I would simply follow SOLID - D principle, when you rely on abstractions, i.e you are right, you can make IAuthSystemAccessible or use events.
    That doesn't mean that you should manually configure all bindings to make them as restrictive as possible, i.e. I don't see any problem that AuthSystemAccessibleImpl can be injected into low level classes with uncautious use.

    Such things should be restricted via asmdefs (where only interfaces are exposed to low level classes), via code rules and code review.

    If you want some FeatureXConfig to be injected only in FeatureXService, then sure, you can use WhenInjectedInto, and same goes for cases when you need two different implementations for different classes, but restricting injection like "WhenInjectedInto<GameFlow or GameSystem>" is something I wouldn't use imo.
     
    LightRuno likes this.
  15. LightRuno

    LightRuno

    Joined:
    May 2, 2018
    Posts:
    7
    Big thanks to Timo @SisusCo and @Lekret for sharing the knowledge! I got all I needed!
     
    Lekret and SisusCo like this.