Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Discussion Dependency Injection in Unity - Vampire Survivors uses Zenject!

Discussion in 'General Discussion' started by TheNullReference, Apr 11, 2023.

  1. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,134
    Last edited: May 3, 2023
  2. Noisecrime

    Noisecrime

    Joined:
    Apr 7, 2010
    Posts:
    2,000
    Have you tried this? It seemed a very bizarre approach to create a Minecraft clone to me ( though maybe a good way to test ChatGPT knowledge ), but I was intrigued and figured it could be an interesting project to play around with Entities.

    Can't say it was a good experience in Unity 2021.3.15f. Had to look up how to install Entities ( 0.51) and Hybrid as neither are 'experimental' enough to be included in Unitys package list. Entities locked up Unity while importing, after 6 minutes I forced quit. Seems everything imported alright though as there were no errors afterwards. Tried the script from that project, crashed Unity 'out-of-memory' on a 32GB PC. Digging around a bit, seems the second pass code that uses perlin worms is either highly inefficient/buggy or entities/hybrid is just a buggy mess with a memory leak, or both.

    Edit: Just seen this tucked away in the code '// This was added because ChatGPT's solution didn't work with Entities package 0.51 (also it didn't instantiate)' So I guess to the original reply ChatGPT may not be reliable enough for Entities.
     
    Ryiah likes this.
  3. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,913
    Hahaha... great timing.
    https://twitter.com/Jonathan_Blow/status/1653812007207043073
     
    PanthenEye, spiney199 and Ryiah like this.
  4. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,514
    "Everything should be as simple as possible, but not simpler." Albert Einstein (IIRC)
     
    SisusCo likes this.
  5. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    ChatGPT works like a rubber duck programming.
    It does not generate working code with zero bugs, so don't expect it to.

    It may give some valid ideas though or it may not. Evaluate results against common sense & experience.
     
  6. Noisecrime

    Noisecrime

    Joined:
    Apr 7, 2010
    Posts:
    2,000
    True, but the whole point of the post is that someone did the work on top of it to make it work with Entities and that it still crashed my machine with a memory leak of some sort, which heavily implies that Entities/Hybrid is still not fit for purpose.
     
  7. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    I actually recall doing a minecraft entities tutorials AGES ago. I think Unity Japan released a tutorial. ECS Physics wasn't implemented at that time, so was using a super hacky PhysX workaround.

    It was cool to experiment with Zenject, I'm starting to remove it from the project as the added boiler plate is getting too annoying. I'll keep it in the back of my mind if I find a need for it in the future. Fortunately it's very easy to change the existing logic over, just moving plain old C# classes to monobehaviours and replacing [Inject] with GetComponent<>
     
    ippdev and Noisecrime like this.
  8. Noisecrime

    Noisecrime

    Joined:
    Apr 7, 2010
    Posts:
    2,000
    Interesting, when I get a spare life time ( just too many other things to research/learn atm ) I'll look it up and see if it works any better than the ChatGPT inspired one - though I'm still perplexed as to why you'd use entities for Minecraft terrain.
     
  9. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    Here is the repo: https://github.com/UnityTechnologies/MinecraftECS
    There's a reuploaded video from the original 2018 version.

    From memory they just brute force blocks instead of voxels and chunks.
     
    Noisecrime likes this.
  10. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    If the domain code has issues - that's not the packages problem - that's domain code problem.
    Native memory access comes with a price of potentially shooting yourself to the foot with manual memory management. Which is expected. Its definitely possible to make editor crash.

    Fortunately most of the leaks are caught by the safety system. (make sure that leak detection is enabled)
    +More and more cases are covered by it with each update.

    Most of the crashes related to Entities are usually indicator of an user error.
    In quite rare cases - they're oversights of internal systems, or subscenes (which is the most unstable part of Entities in my opinion).

    Like putting WorldUpdateAllocatorResetSystem into InitializationSystemGroup which will cause Temp allocators to not be disposed at the start of the frame if custom bootstrap removes InitializationSystemGroup. Which will result in silently running out of memory after a while. But I guess that's just wip nature of it.

    For the rare cases you can always launch editor with debug allocator to investigate by passing
    "-debugallocator" as command line parameter.

    TL;DR:
    My point is - if you're new to the DOTS - don't dive deep into some advanced examples.
    Try something simple first.
     
    Last edited: May 4, 2023
  11. Noisecrime

    Noisecrime

    Joined:
    Apr 7, 2010
    Posts:
    2,000
    I apricate all the assistance, the problem I have is that this has come from a 'reportedly' working project, where the author reported all the changes they needed to make to ChatGPT to get it to work, and demonstrated it working via screenshots and discussion such as it taking 10 minutes to produce a 128x128x64 world.

    Although I'm using an editor that is a few patches behind behind (2021.3.15 vs 2021.3.18 ) and URP being 12.1.8 instead of 12.1.10 I don't see how they would cause problems with Entities, which is the exact same version. I couldn't even see much in terms of entities and memory assignment that would explain burning through 20GB in less than a minute.

    Edit:
    In summary its not the specific project that I have a problem with ( even though its not working), its the whole use of Entities/Hybrid aspect of DOTS, in that every time I've tried it there has been nothing but lock ups ( during package install), crashes and bugs. Its just not very user friendly, especially if you want to just play around with it and not have time set aside to make everything work.

    But again as I said, I do apricate the help, there are a few items in your list I might take a look at sometime with regard to the project, but I've already got 4 different projects on the go and have actual client work to get on with.


    EDIT
    BTW Unity have their own fork of zenject on GitHub here. No idea if it has anything over other forks.
     
    Last edited: May 8, 2023
    TheNullReference and xVergilx like this.
  12. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    I feel like the benefits of using dependency injection have been downplayed a lot in this thread. It can in fact give very sizable benefits.

    It's rare to see good, nuanced discussions about the pros and cons of dependency injection, unfortunately. There are a lot of reasons for this, I think, such as people getting somewhat confused over the terminology (dependency injection is not an IoC Container is not dependency inversion is not a service locator) and talking past each other, and some people having had personal negative experiences with how a particular IoC Container was used in a particular project, which has tainted their opinion of the entire pattern as a whole.

    One thing I'd like to point out, is that it's not a requirement to either go all in or abandon the idea completely when it comes to dependency injection.
    You can very well use GetComponent to fetch one dependency (service locator pattern), a serialized field to inject another dependency (DI pattern) and interface injection to have a third one be delivered (also DI pattern). So just because you don't like the idea of using an IoC container to configure all dependencies in a single composition root, it does not mean you can't still get huge benefits from using the dependency injection pattern every now and then.

    That being said, there can be some benefits to using only DI to configure all the dependencies of your classes:
    1. Pretty much everything can become trivial to test by default; a huge benefit, as the more fun and easy it is to create them, the more like they are to get written, and the more bug-free, robust and refactoring-friendly your game can become as a result.
    2. Constructor injection can help make execution order related issues nigh impossible. Similar results can also be achieved with MonoBehaviours as well using the static factory method pattern, for example.
    3. It's easy to identify all the dependencies that an object has, without having to read through every line of code, when all of them are injected via a single constructor or initialization method. This can help surface issues, like classes having too many responsibilities (difficult to read), or data objects containing logic (error prone).
    But even just using dependency injection more sparingly in some strategic places can give sizable benefits:
    1. Can help make your components very reusable. You can share the same modular components between your player character and all other characters, and easily just swap out a few dependencies and rewire them differently to compose different behaviours.
    2. You can swap a globally shared service with another one very easily, if you inject your dependencies using an interface instead of a concrete type. Being able to easily swap e.g. your input handler that was designed for mouse and keyboard to one designed for the gamepad can be really useful. Or swapping all your addressables from high quality assets to low quality ones.
    3. Injecting dependencies as interfaces also makes it possible to use some really useful design patterns like the decorator pattern and null object pattern.
    On the flip side, the biggest downsides to me are:
    1. Worse performance when using interfaces compared to concrete types. There is unfortunately a trade-off between flexibility and performance here, no way around it. Using only interfaces everywhere all the time probably isn't the best choice for many game projects.
    2. Figuring out the best place to compose dependencies can require some thinking, at least at the start of a new project. If you just use methods like AddComponent, FindAnyObjectByType or the singleton pattern, then you don't need to stop and think about it. You may not have the same level of flexibility, but it is very straightforward and easy to read.
    3. Using interfaces instead of concrete types can make code slightly more difficult to read. In my experience it's not that bad though, since stack traces of logged messages will still tell you the exact concrete types, and modern IDEs make it really easy to jump from an interface to the concrete types. Using interfaces can also encourage one to create good abstractions with simple public APIs, and hide away the complexity of implementation details - so it can even end up reducing the overall perceived complexity of the project in some cases.

    The way I personally like to approach DI is by leaning into the strengths of Unity. I feel like the classic "all dependencies should be configured in a single composition root and resolved in one go" approach doesn't fit that well into the Unity ecosystem, or at least the usual workflow in it.

    Unity has a lot of systems designed around the idea of your game being split into scenes and prefabs and addressables that are loaded at different times, and often asynchronously, so wiring all of that together in one place can get complicated, and can require a lot of delegates and builders to be used. Also the Inspector window is already a really powerful and easy-to-use DI tool. So to me it makes sense to keep using all these great tools and just focus on building additional tooling around them to make them more flexible and robust.

    Also configuring everything in one big class sounds to me like a recipe for frequent merge conflicts. But maybe this is potentially avoidable to a large extent using convention over configuration?
     
    Last edited: May 22, 2023
  13. ippdev

    ippdev

    Joined:
    Feb 7, 2010
    Posts:
    3,793
    You mentioned very little I cannot do with a vanilla MB and the proper frame dependent component based architecture. DI may be great for enterprise/console/services but I guarantee if I want to hand off my work to the technical artists and they have tweaks they cannot perform in the Inspector they are going to have to wait for me instead of getting on with their work. DI is opacity and obstructs team members from using the Inspector for their part of the work IMHO. So..your solution will probably be write an Inspector slot or CustomDrawer for them./. Well then..why use them to begin with if you can simply write MB components in Unity style and get on with development. Test? I have Debug.Log and console and the Inspector reveals every value I or another team member needs. All components should be tested ongoing and determination of values and parameters is part of that.
     
  14. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    I actually agree with a lot of what you've said... I'm kind of an inbetweener when it comes to this topic: I love the simplicity of drag-and-dropping references using the Inspector, but I also like using an IoC container to augment that a step further.

    Yes, alternatives to a DI-based architecture, like using singletons or FindAnyObjectByType do also work just fine. I've just found that by utilizing DI I've been able to fix some of the pain points I've experienced in the projects I've worked on in the past.

    Testability is a big one, yes. But also flexibility outside of that.

    Just to give one example:

    I can use an EditorLogger to handle all my logging in the editor by default. This will only print more important information to the console, but will avoid being too spammy.

    During unit tests I can swap the logger to TestLogger, so that I don't get any unnecessary info messages logged to the console, but logged errors will still go through and cause tests to fail.
    And if I want warnings to also cause unit tests to fail? I can simply inject a logger that logs warnings as errors.

    If I'm investigating a bug related to audio, I can select the AudioManager and swap it's logger to DebugLogger which also logs additional debugging information to the Console.

    When I make a release build I can inject a ReleaseBuildLogger that only logs errors and some other critical information. If I want to integrate a cloud diagnostics solution to the project in the future, I can inject a logger that contains the required API calls for that in all the contexts where I want it to be active.

    And this was just one example; having this level of flexibility and convenience everywhere just makes my life so much easier in the aggregate.


    I totally get where you are coming from with your critique about IoC containers being too opaque and getting in the way when trying to make quick changes. I've tried to make this as much of a non-issue as I could in my projects by trying to bring in that transparency with custom inspector tooling. I've also added the ability to swap out the default service with another instance for a particular component by just dragging-and-dropping it in the Inspector.

    dependency-injection.gif

    With these improvements in place I don't really see any reason remaining why I should opt to use a singleton over DI. But on the contrary, I do have many reasons why I want to avoid using singletons. Hence, I opt to go with DI :)
     
    Last edited: May 19, 2023
    CodeRonnie and Ryiah like this.
  15. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,467
    Well, that is definitely not "test driven development". But I agree that unfortunately that is hard to do consequently outside of enterprise environments.
    I do wonder how many small studios do have a code coverage of above even just 50% with automated tests. (For comparison, at my automobile industry day job we require >80% for tools and >90% for product code.)

    That however is something a resource locator could do as well without any dedicated framework though, or am I wrong?

    Fair, everyone has their preference :)
     
    SisusCo likes this.
  16. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    Yes, the service locator is also a really good pattern in my opinion, and gives you most of the same benefits as DI.

    In fact, a service locator and an IoC container can be functionally identical, but the way they are used is just different (with DI only the composition roots use the container and clients receive the services automatically; with a service locator all the clients grab the services directly from the service locator).
     
    spiney199 likes this.
  17. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    I wanted to see how Unity handles it's architecture so I downloaded the BossRoom Demo on Github.

    A few interesting observations. They're using a lot of don't destroy on load game objects to manage the game. Not surprising as this is how most developers do it.

    One thing I did find surprising, is that they're actually using VContainer, in BossRoom!

    Screenshot 2023-05-30 101524.png

    Caught red handed! Boss room is the closest thing I've seen to a full game release from Unity. Pretty interesting they decided to use VContainer for quite a bit of it.
     
    tmonestudio and SisusCo like this.
  18. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    This is probably the most constructive response I've seen so far. I agree with your pros and cons. DI has some BIG promises (having your code super testable and modular) but it also comes with those pitfalls as you mentioned. I do have to stop and think a bit more about my systems as it's easy for a class to be over encumbered with dependencies. It usually isn't a problem for a single monobehaviour to have 8+ [SerializedField] references, but when you're constructor starts wrapping 3 to 4 lines you have to think to yourself "is this too much?".

    Using IoC Container framework like Zenject means you don't need any custom monobehaviours other than for OnCollisionEnter stuff, but then you lose the editor configurability, as well as few nice to haves like auto-disposing subscriptions in UniRx. Its very easy to just write a single monobehaviour script and chuck everything in it. It's acting as your serialized data, runtime data, runtime logic and view all in one. You can jam 3-4 classes worth of logic in it as well. It's fast.

    My current strategy is to just build the game with these spaghetti monobehaviours, and then upgrading them to cleaner code when required. Building clean code is not great when you're still developing the game feel, gameplay etc. Systems get broken often (on purpose). Also considering 85% of written code won't make it to final release, it doesn't make sense to write it all as clean and well thought out. Once there is a chunk of game that is settled then I can see it becoming more important. There's already some systems that are locked into Zenject framework.

    Right now I'm building a pistol weapon for example. It's just an ugly massive monobehaviour, for now.
     
    SisusCo likes this.
  19. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    287
    Admittedly, I have not read every post in this lengthy thread. However, I think I get the gist. I've read many threads like this over the years because I've been somewhat accidentally working on a dependency injection framework for Unity for quite some time. It has only just been submitted for approval, but I believe it addresses these perennial concerns that continue to be raised around dependency injection in regards to Unity.

    First, Unity is a unique development environment with a few idiosyncracies that present common issues that all experienced Unity developers are more than familiar with. Programming most, if not all of your code, as a MonoBehaviour, or at least rooted to a MonoBehaviour somewhere in your composition graph, means that there is an inherent gap between those objects and objects in other scenes, or outside of scenes (if you have any such code.) This is a legitimate issue that doesn't need further explanation from me. The values of your serialized objects are saved in scene files, or as prefabs, and serialized dependencies cannot cross that scene / project boundary.

    That is the inherent problem to solve, and different programmers solve it in many different ways.

    I can't add to what others have said, as far as adding one more opinion to the pile, but I can add a genuinely new and novel approach that I have not seen anyone else take. It is a pattern of my own design, an implementation of the Service Locator pattern that is so highly abstracted that it functions effectively as an interface. I call it the Injector Locator pattern.

    In an environment such as Unity where you are not in control of the constructors of various instantiated objects, and you have no entry point for the composition root of your application, those objects inevitably must reach out for some type of static reference. The Service Locator pattern is essentially unavoidable. Many DI frameworks use various attributes and reflection, but reflection is bad, even baking reflected dependencies is bad, and those random attributes that are specific to the framework are also bad as they themselves represent tightly coupled dependencies on that particular framework.

    Therefore, accepting this one caveat, and reducing it to it's most abstracted possible case, allows for you to pass anything through that one and only bottleneck without any other strong dependency.

    The pattern relies on 1 class and 2 interfaces, totaling 19 lines of code. It is available free and open source here: https://github.com/swipetrack/switchboard/tree/main/interface/injector-locator

    The IInjector interface consists of a single method that returns a dependency based on the generic type parameter requested by the client.

    Because we do not control the constructors of MonoBehaviours method injection becomes necessary. The IInjectable interface simply provide that injection method which recieves in an IInjector argument. From there, the IInjectable object is free to request any type of object by type, including a loosely coupled implementation of an interface. If method injection is not necessary, an IInjector can and should be passed directly into a constructor for pure constructor injection.

    This pattern is only meant to bridge the divide between scene and project in this type of environment that restricts access to constructors, composition root / main entry point, and cannot serialize dependencies between scenes, prefabs or scriptable objects, and plain old C# services that should live outside of scenes.

    The InjectorLocator is a static class with an event and a method. IInjectables can request injection. IInjectors can subscribe to observe the corresponding event. That is your only strongly typed, tightly coupled dependency. My whole framework could be swapped out in an instant without affecting any other part of your codebase. There's no reflection, it all works instantaneously, even for new objects dropped into a scene at run time, even with configurable enter play mode options. You can just press play and instantly have all dependencies satisfied dynamically in a way that is easily traceable.

    Anyway, this isn't only meant to be self promotion, but to AGREE that there is a problem, and AGREE that there are simple solutions that don't involve an overly bloated framework, or abstraction hell. You should only use this solution to bridge that divide presented by the Unity development environment.

    Unity could implement their own implementation of this free, open-source pattern. They could also do many other things, that have been mentioned by others, to make this type of plugin unnecessary. However, until there is a built-in way to overcome the divide between MonoBehaviours serialized in scenes, prefabs and scriptable objects, and a theoretical composition root where the heart of your application actually lives as plain old C# (which only currently exists as an obscure attribute), then the issue will remain a legitimate one. However, most of the "solutions" on the market are too complicated for their own good.

    You can read more of my thoughts on the matter at https://swipetrack.github.io/switchboard/manual/dependency-injection.html.
     
    SisusCo likes this.
  20. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,467
    But.. is that actually a goal worth pursuing?
    I mean look over at Unreal where not even the game mechanics themselves live as a text-based programming language in many games.

    Doing things purely in code/text can be tempting as a coder, but it's a "have a hammer and everything looks like a nail" problem. In Game dev one should be able to look at it with a bit of an abstracted view. After all meshes and visuals are obviously not code. Hardly anyone wants to design UI with code and nobody wants to do complex 3D animations with code.
    So why exactly need the connections of your game modules to be done with code/text?
     
  21. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    287
    To me, the question is not so much whether a solution like this should be used in every case. The bigger issue is whether or not you have the choice. Currently, you do not. Therefore, a limitation exists inherently on what are some very well established and accepted design patterns. Many developers may not be concerned by lack of access to S.O.L.I.D. programming principles in Unity. To each their own.

    Indeed. Meshes and visuals are views. You should be able to separate them from the model logic of your application. If not, your application has a strong dependency on Unity. In theory, you could have your entire game coded so that it could be rendered by the console, rendered in Unreal, or rendered in Godot. A truly modular design would mean copying and pasting the section of your project that represents the actual logic of your game and all of the models that control the function into a different environment, or skinning the application to be rendered differently. Why would you not want your software cleanly abstracted into simple, readable, modular layers if it were easy to do so?

    I don't mean to imply that views should be controlled by models. However, models should be separate from the views that render them. Your game should ideally be able to run headless on a pure C# server with no rendered view of the game. Views should observe the state of the models, and render that state as desired. The dependency of the view on the model should also be loosely coupled, and injected. So, if a character is jumping, that should be represented, maybe by a boolean in the state of their model. The observing animator and renderer components, which have had that particular character's model / state data injected, should play the appropriate animations.

    Admittedly, it does get tricky making those kinds of separations when things like physics from the scene get involved in the logic of the state of models in your game, but it's easier to deal with separating those layers of your software if you are thinking about them than if you are not.

    But, again, this isn't necessarily an argument to do it this way. This is just the way I prefer to deal with bridging the gap between scenes. For what it's worth, I do prefer to treat anything in a scene as a "view" as much as possible. Like I said, it's not entirely the case, but it helps separate concerns and software layers.

    EDIT: I would add that when you have Unity, everything looks like a MonoBehaviour.
     
    Last edited: Jun 5, 2023
  22. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,134
    It's not an either-or situation. You can have both use cases in a project, and with an asset like Init(args) you can even have both use cases at once. Spoiler has an example picked directly from the author's thread.

    Code default:
    ping-service-definition.png

    Using manual override:
    override-service.png
     
    Last edited: Jun 5, 2023
    DragonCoder likes this.
  23. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    287
    Also, I predict the more common use case will be for people to separate the services, usually implemented as some version of singleton MonoBehaviour in a DoNotDestroyOnLoad or other scene, rather than separating model from view on something like a character.

    My question would be, why should something be a MonoBehaviour if it has no logical transform components and no real reason to live in a scene at all?
     
  24. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,467
    What usecase are you thinking of where ScriptableObjects (SOs) are not the valid alternative?

    The only usecase I encountered was wanting to have SOs with some sort of Start() method. There's a solution however: An abstract class that inherits from SO and provides an Init() method (to separate it from Unity's Start()). All your own SOs should inherit from that one.
    Then have a manager in the start scene with an Awake or Start that executes code like this to acquire all instances of that SO and call the Init() method:
    https://answers.unity.com/questions/1425758/how-can-i-find-all-instances-of-a-scriptable-objec.html
    Of course you can cache that list too.
    To make sure that all SOs are actually initialized before any other Start() method add
    [DefaultExecutionOrder(-999)]
    before the class definition of that manager.
     
    CodeRonnie likes this.
  25. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    287
    That's almost exactly what my plugin provides for anyone that wants a working solution out of the box with no additional effort on their part. The inversion of control container is called DependencyInjector. It is a ScriptableObject. You derive from it and define whatever you want to occur.

    However, one key difference is that my plugin launches your desired SO root when the application launches, or when you press play, not when a particular scene or MonoBehaviour is loaded. It provides a hook for deactivation once all game objects have been destroyed. (Application.quitting occurs before OnDestroy() for those who haven't struggled with it. Has anyone here dealt with a singleton MonoBehaviour that lazy loads itself back into a scene after it was supposed to have been destroyed?)

    Also, the Injector Locator pattern enables the static reference to be upon a simple, open-source pattern. Your classes can be written in a modular, loosely-coupled way. The classes they depend on don't even need to exist yet for you to finish coding the dependent class. There is no dependency on any particular framework, or class (except InjectorLocator). The original issue is scoped to that single line of code. You can easily delete it from your classes' Awake() methods if you have an alternative method injection solution. The IInjector / IInjectable interfaces for method injection by type are generic, abstract, and open source. There is no strong dependency on YOUR particular type of scriptable object, nor mine!
     
    Last edited: Jun 5, 2023
  26. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    287
    Here is an example of how a MonoBehaviour can be completely decoupled from any dependency. This isn't intended to show how things should be done, but that no dependencies are required and you can actually decouple your code. You can drop this object in a running scene and it will immediately have its dependencies satisfied and start updating, as long as there is an appropriate IInjector observing the InjectorLocator.

    Code (CSharp):
    1. public sealed class ExampleComponent : MonoBehaviour, IInjectable
    2. {
    3.     private ILogger Logger;
    4.     private ITicker Ticker;
    5.     private IExampleModel Model;
    6.     private MeshRenderer Renderer;
    7.     private InputAction<FrameOfTime> TickAction;
    8.  
    9.  
    10.     private void Awake()
    11.     {
    12.         TickAction = Tick;
    13.         Renderer = GetComponent<MeshRenderer>();
    14.    
    15.         // This line of code is the only tightly coupled strong dependency necessary in any of your classes.
    16.         // It can be removed entirely if any other object can call the IInjectable.InjectWith(IInjector) method,
    17.         // such as a manager object in the scene, or whatever creates this component.
    18.         // This one universal line is a replacement for all specific references to any static class or singleton.
    19.         InjectorLocator.RequestInjection(this);
    20.     }
    21.  
    22.     void IInjectable.InjectWith(IInjector injector)
    23.     {
    24.         // In this example, the first non-null instance injected, from the first IInjector to observe InjectorLocator.InjectionRequested, is maintained.
    25.         if(Ticker == null)
    26.             injector.Inject(out Ticker);
    27.  
    28.         // In the following examples, multiple InjectWith(IInjector) calls result in the later calls overriding the original injection.
    29.         if(injector.Inject(out ILogger logger) && logger != null)
    30.         {
    31.             if(Logger != null && Logger != logger)
    32.                 Logger.LogWarning("Logger override!");
    33.             Logger = logger;
    34.         }
    35.    
    36.        // Note, this will even set the Model to null if the IInjector returns true, merely for example. The IInjectable is in control.
    37.         if(injector.Inject(out IExampleModel model))
    38.             Model = model;
    39.     }
    40.  
    41.     private void OnEnable()
    42.     {
    43.         Ticker?.StartTick(TickAction);
    44.     }
    45.  
    46.     private void Tick(in FrameOfTime time)
    47.     {
    48.         UpdateRenderer();
    49.     }
    50.  
    51.     private void OnDisable()
    52.     {
    53.         Ticker?.StopTick(TickAction);
    54.     }
    55.  
    56.     // All of the changes that update the position and properties of the renderer components in the scene come from the IExampleModel, but it is only a loosely coupled dependency on an interface.
    57.     private void UpdateRenderer()
    58.     {
    59.         if(Renderer != null && Model != null)
    60.         {
    61.             transform.position = Model.Position;
    62.             if(Renderer.material.color != Model.Color)
    63.                 Renderer.material.color = Model.Color;
    64.         }
    65.     }
    66. }
     
    Last edited: Jun 5, 2023
  27. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    287
    I have now read all of the previous posts in this thread, and I would like to address some of the common issues that were raised. I have found this really helpful. In particular, having just finished developing a novel dependency injection framework, I was so focused on showing that any dependency can be decoupled, I forgot that it should be pointed out that such power should be isolated to relevant use cases. You shouldn't just have every object in your application magically injecting itself. I'll need to update my documentation concerning best practices to be more clear on that issue.

    The Core Issue
    When programming in Unity, there is no straightforward entry point for composing the non-game-object classes, or services, that represent the composition root of your application. Also, when a MonoBehaviour is instantiated, there is no access to the constructor. So, pure dependency injection by passing arguments into the constructor cannot be achieved. MonoBehaviours in scenes cannot reference anything outside of that scene, unless it is provided at run time. Prefabs cannot reference anything in a scene at design time. Neither can any coupling be made to a composition root or services that have not been instantied before run time.

    Therefore, in order to satisfy external dependencies, a strongly-typed, usually static reference to an external dependency must be applied, which establishes a tight coupling of the two classes, or dependencies must be injected, ideally using method injection, from an external source before the MonoBehaviour requires them for operation. Tight coupling is bad for separating concerns and unit testing. Unity also does not inherently support serializing object reference fields as interfaces. This means that dependencies injected directly through the inspector at design time cannot be loosely coupled without some type of polymorphic base class. This limits the types of objects that can be implemented as those types of references.

    Unity programmers have dealt with these limitation since the beginning. My personal opinion is that, based on the inherent current limitations of the environment, some application of the Service Locator pattern is the most appropriate. Normally, the Service Locator pattern is rejected as an inherent design flaw, but those opinions come from other programming environments that do not have the same limitations. Many applications, mine included, have used singletons, often implemented as MonoBehaviours, or at least updated by a MonoBehaviour somewhere in the dependency chain, if they are not entirely event based.

    Overuse of SOLID
    Programmers migrating from other development environments may come in expecting to find a composition root, or some standard system for injecting dependencies. The standard .NET application host includes a composition root, dependency injection, a logger, and a configuration interface among other potential services by default.

    Also, other development environments may be event based rather than loop based. When the user interacts with the software an event is invoked and a chain of other functionality occurs as a result.

    Additionally, it may make more sense in other environments to have more clearly defined separation of layers between the model or business logic of the application, which is rendered by a distinctly separate view layer. User interaction is handled by controllers. Many of these principles are still applicable within Unity, but it may not be clear how to apply these concepts to Unity when migrating from another environment. Specifically, some things in a scene could be considered a view, but others are not, and it depends on the specific functionality and design of each inidividual application.

    Underuse of SOLID
    On the other hand, some Unity developers may not have enough exposure to or consideration for loose coupling, dependency injection, separating concerns, unit testing, and other practices that are well established industry standards outside of Unity. Many on these forums would have someone coming in with the aforementioned assumptions disregard their old way of thinking entirely in favor of a simple, generic singleton MonoBehaviour. Also, the misunderstandings of developers attempting to apply patterns from other industries results in some Unity veterans deciding that the whole exercise is folly and there never was an original problem to begin with.

    ECS / DOTS
    I personally am inexperienced with ECS and DOTS. However, I understand the broad strokes and it makes sense to me that the system is data oriented and feeds entities into systems, rather than the other way around, that it would eliminate much of the need for dependency injection. However, I often see that the conclusion of even ECS evangelists is that a hybrid approach may be best in most cases. Therefore, the original issue is not totally resolved.

    Service Locator
    So, my favored conclusion is that one must use a Service Locator. When I say that, I even include singletons as mini service locators. In either case, the MonoBehaviour is making a strongly-type reference to some static dependency. This obviously comes with all of the disadvantages that lead people to reject them in other contexts. However, I feel that they are all but unavoidable until unprecedented changes occur in Unity that would make it no longer necessary.

    You could inject a MonoBehaviour with dependencies, but whatever parent object is doing that will have had to have gotten its own dependencies at some point. Also, this prevents prefabs from being dropped into a running scene within the editor, and having their dependencies injected. It is only when some other parent object has a reference to the MonoBehaviour that it can be injected.

    Injector Locator
    So, having established that MonoBehaviours need a Service Locator to satisfy dependencies under every conceivable scenario, I have created the most abstract, generic service locator that I can conceive of. True, it is a static class, and using it means establishing a tight coupling. However, the entire pattern has an open source implementation that consists of less than 20 lines of code. We know that we need a service locator, so let's abstract that service locator in a way that is universally applicable to any software application. Not just Unity, but any environment with similar constraints.

    IInjector
    The IInjector interface is just a solid way of passing a single argument into a constructor or a method, that can then satisfy any type of dependency based on the Type of object that is requested. If your constructor is wrapping off the page, perhaps you could just collapse it to a single argument with an implementation of IInjector.

    IInjectable
    The IInjectable interface simply defines that a type has a method suitable for method injection of an IInjector argument.

    Composition Root
    From this link provided by the OP:
    https://www.sebaslab.com/ioc-container-unity-part-1/
    "Manual injection would be the preferable solution, but since Unity doesn’t have an entry point where dependencies can be created and injected, manual injections become quite hard to achieve without a good understanding of the Unity limitations."

    My plugin, Switchboard, provides a composition root for your application. At run time, it loads your desired Inversion of Control container, derived from a class I call DependencyInjector, which provides Activation() and Deactivation() methods which the composition root invokes at the start and end of the application.

    One advantage of the composition root that my plugin provides is that it just provides the entry point, and the rest is left for you to control with pure C#. It doesn't define a specific, fluent interface that you must use to bind types and lifetimes to the container in any specific way. I agree with the criticism that some have had of other dependency injection solutions, that they can result in a tight coupling to that particular framework and all of its types and patterns. I would rather just give people a rational place to put their own code without any undue constraints.

    Execution Order
    One issue that many struggle with is proper definition of execution order for a composition root to instantiate before other objects. My plugin has no such issue. It calls your Activation() method when the application begins running. The first scene will be loaded, but no Awake() calls will have occurred. Some have said that dependency injection is good for services, but not game objects. With my solution, there is no issue with creating new game objects in the first scene before Awake(). However, everything in the composition root could also just be a scriptabl object, or a plain old C# class. It may not be necessary for you to create those game objects you thought you needed.

    God Objects
    Some have expressed concern that a composition root would lead toward the creation of a god object. I think that that concern is not really unique to a composition root. Any singleton MonoBehaviour can turn into a god object. A composition root can be well designed with many separate services that spin up elegantly on application launch, without any specific god-style code outside of those separated, modular services. Each one can be its own implementation of scriptable object, and therefore can have its properties tweaked and serialized in the project as an asset. Scriptable object references of various types can be nested within one another and all inspected and modified in place with my Expandable attribute applied to the object reference field.

    Data Oriented Design
    Establishing classes with solid methods that operate on various data is a great way to keep your class hierarchy small and ensure stability and re-usability. If you can define a class that operates on various data sets to define the difference between this or that type of character or inventory item, that is certainly a good practice.

    Configuration
    In another thread on this topic there was discussion of the difference between configuration, scriptable objects representing data, and services. Well, one module that I unfortunately did not have time to complete was for an implementation of an IConfiguration interface. The concrete implementation would define a root for all configuration sources. Users could extend their own config by defining their own config sources, but an INI file implementation would be provided. All of this is certainly possible using a composition root with fully modifiable config sources that get injected automatically at run time.

    Interfaces
    Some have expressed concern over interfaces in and of themselves. It has been pointed out that interfaces impart a cost, but I think those costs can easily be mitigated in how you use interfaces. For example, in my plugin I provide a Ticker that basically just allows you to connect anything you want to the update loop, as several others in this and other threads have shown their personal implementations of. So, you can request the ticker as a dependency via the ITicker interface, but then you subscribe to an event in order to actually receive the updates. Your object need not implement any particular interface. It does require a particular delegate method signature that constitutes part of the public plugin interface, but a wrapper or parent object can easily implement that method and update underlying child objects. So, after method injection, nothing is invoking an interface method. The dependency, which was requested as a loosely coupled interface, has an event to which the dependent object subscribes as an observer. From then on, the dependent object is just update via event invocation. This is as fast for 10,000 objects as 10,000 MonoBehaviours all using Update() in a scene. Actually, it used to be much faster, but Unity has improved in performance. So, it's all in how you use your interfaces, even if they have negligible costs associated.

    Separation of Concerns
    There is an art to separating the responsibilities of your types. One concern that has been pointed out is that separating every single thing out into tiny implementations of some interface can lead to model collapse. I have definitely encountered situations where types are abstracted to death, often with no discernible benefit. Then, as you attempt to debug you find yourself ping ponging from one type to another just to try to comprehend what is even going on.

    So, I would advise everyone to use interfaces and loose coupling only where it makes rational sense, along some type of boundary that contains a logical module. If you have three classes that conceptually cannot do anything without one another, and there is not likely to be some other implementation to swap out for one of them, it makes no sense to design all three as interfaces that are loosely coupled to one another. Doing so is indeed a waste of time, unless you have a well established concern for unit testing.

    Unit Testing, Factories, and Prefabs
    I noticed that OP's design diagrams did seem to separate classes out into a lot of distint modules that could benefit from a more data driven design. However, I noted their concern for unit testing individual MonoBehaviour components. Most others dismissed this concern because, as I would also point out, loosely coupling the interdependencies between separate components on a single game object in that way prevents you from making the most of prefabs, which are already factories, with dependency injection via the inspector object reference field. If you insist on isolating unit tests down to individual components, you will lose the ease of defining a prefab with properties that are easily modified in the editor inspector.

    However, if that is already the case that you need to assemble game object and their components in an abstract way according to some configuration, then you already have a need to apply the Factory pattern. As was suggested, you could probably abstract that factory down to be data driven, and perhaps to just run through the components that were added to the constructed game object, detecting IInjectables and providing an IInjector. Those types of objects are going to lose some of those benefits of the Unity way of doing things, like prefabs as others pointed out.

    Events
    When and where to use events has also been brought up. I am a fan of an event based architecture where it makes sense. They are great for loose coupling and go hand in hand with interfaces in my opinion. Like I said before, my replication of Unity's own Update() loop is event based and performs just as well.

    However, events come with a caveat. They generate garbage. When you add or remove an event observer with += and -= that allocates garbage memory, every time. If adding and removing observers is expected to be limited in scope, as is the case with the Injector Locator pattern, that's probably not a concern. However, if you're creating thousands of objects and they all add and remove themselves as event observers throughout the course of your application, you could be generating more garbage than you realize. Also, assigning an event observer delegate just by referencing its name will generate additional garbage. Lambdas avoid this, but only if they are not closures. If your lambda is accidentally a closure, it's going to be the worst case scenario. I don't think .NET Standard 2.1 in Unity supports static lambdas yet. I am working on a system for garbage free events, but it is still a work in progress.

    Performance
    Performance is another common concern. However, I can tell you that my plugin comes with no additional performance implications on your application. My DI framework does not use reflection at all. I have configurable enter play mode options enabled, and I can literally hit play and be running instantly with all dependencies satisfied. I can drag and drop prefabs into scenes and their loosely coupled interface dependencies are instantly satisfied. There is no performance penalty over using a singleton.

    Best Practices
    So, in conclusion, I think every concern can be fully resolved without disatisfying anyone in this thread. However, you should use dependency injection and loose coupling sparingly along logical module boundaries. If something doesn't have a need for loose coupling you shouldn't waste your time on it. Although, as I've established there is an inherent and unavoidable need to couple your MonoBehaviours to the rest of your application somehow. So, the issue is unavoidable, but the solution should not be treated as a panacea to be sprinkled all throughout your application. If method or constructor injection can be used instead of reaching out for the Injector Locator, then that is preferable. Don't implement a concrete class as an interface unless there is a clear need to do so.
     
  28. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    287
    I forgot to mention a few things I wanted to touch on. I love the example raised in a previous post of Unity as a sandbox for puppet robots and rigid body physics colliders. That is a really great way to ensure people like the OP are thinking correctly about which parts of the scene are necessary model state data. Indeed, it is difficult to network these state changes, such as in a networked multiplayer fighting game. The colliders attached to the skeletons in the scene are part of the model, not the view. However, the particular model skinned to the avatar, VFX, and sound effects are not. It may even be a necessary part of trying to network that type of game efficiently to separate out the components that represent model, and those that represent view, even onto separate game objects. As was mentioned, root motion of the animations could be moving the weapon colliders, and that could all need to be replicated across clients and server. If you want to move your character as model data, and have the animations follow along as separable views, you could end up with a floaty, unnatural feeling character.

    However, I can present an alternate example where much of what is in the scene is actually view layer. Suppose you have a UI menu that displays eight cards on screen at a time. However, the player has many more actual cards than can be displayed at once. The other cards are on other tabs that can be cycled through. In practice, the 8 prefabs that render the specifics of a particular card, in a data driven way, remain on screen the whole time, and new model data for the cards is simply injected into the views when changing tabs. There may be a presenter view model manager type object that regulates this. It can either pass strongly typed card data to a tightly coupled method in the card view prefabs, or pass the data in as a loosely coupled interface that provides the relevant properties to the view. It all depends on your project.

    So, if you were making chess, or some type of enterprise application, you may be able to have everything in your scene purely representing the view layer, but if you are going for Elden Ring style PVP combat you will have model elements operating in the scene unless you intend to fully replace the physics and animation subsystems.

    Also, there was a mention of code stripping, and I wanted to point out that the Injector Locator and IInjector should not have any issues with code stripping because the generic type parameters are inferred at compile time.
     
  29. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    To give a simple practical example where straight-forward use of singletons or serialized scriptable object references won't cut it:

    1. You have a Player component which requires an IInputManager object and is attached to a prefab.
    2. During gameplay you have an InputManager component in your Scene hierarchy which implements IInputManager.
    3. In a unit test for Player you want to substitute InputManager with another implementation.
    4. To make things a bit more interesting, let's say the Player also needs to have access to the IInputManager object already during the OnEnable event.
    Code (CSharp):
    1. interface IInputManager
    2. {
    3.     event Action<Vector2> MoveInputChanged;
    4. }
    5.  
    6. partial class Player : MonoBehaviour
    7. {
    8.     IInputManager inputManager; // <- dependency that needs to be resolved
    9.  
    10.     void OnEnable() => inputManager.MoveInputChanged += OnMoveInputChanged;
    11.     void OnDisable() => inputManager.MoveInputChanged -= OnMoveInputChanged;
    12.  
    13.     void OnMoveInputChanged(Vector2 input)
    14.     {
    15.         if(inputManager.MoveInput != Vector2.zero)
    16.         {
    17.             Move();
    18.         }
    19.     }
    20. }
    21.  
    22. public class PlayerTests
    23. {
    24.     [Test]
    25.     public void Left_MoveInput_Moves_Player_Left()
    26.     {
    27.         var inputManager = CreateMockInputManager();
    28.         var player = CreatePlayer(inputManager);
    29.    
    30.         inputManager.MoveInput = Vector2.left;
    31.         inputManager.Update(deltaTime: 1f);
    32.    
    33.         var expectedPosition = Vector2.left * player.MoveSpeed;
    34.         Assert.AreEqual(expectedPosition, player.transform.position);
    35.     }
    36.  
    37.     ...
    38. }

    And for purposes of comparison, here are some possible solutions for handling this (all with their own pros and cons):
    Code (CSharp):
    1. class InputManager : MonoBehaviour, IInputManager { ... }
    2.  
    3. partial class Player : MonoBehaviour
    4. {
    5.     void Awake() => inputManager = FindAnyObjectByType<IInputManager>();
    6. }
    Code (CSharp):
    1. [DefaultExecutionOrder(-100)]
    2. class InputManager : MonoBehaviour, IInputManager
    3. {
    4.     void Awake() => ServiceLocator.RegisterService<IInputManager>(this);
    5.  
    6.     ...
    7. }
    8.  
    9. partial class Player : MonoBehaviour
    10. {
    11.     void Awake() => inputManager = ServiceLocator.Get<IInputManager>();
    12. }
    Code (CSharp):
    1. class InputManager : MonoBehaviour, IInputManager { ... }
    2.  
    3. [DefaultExecutionOrder(-100)]
    4. class PlayerInitializer : MonoBehaviour
    5. {
    6.     private void Awake() => GetComponent<Player>().Init(FindAnyObjectByType<IInputManager>());
    7. }
    8.  
    9. partial class Player : MonoBehaviour
    10. {
    11.     public void Init(IInputManager inputManager) => this.inputManager = inputManager;
    12. }
    Code (CSharp):
    1. class InputManager : MonoBehaviour, IInputManager { ... }
    2.  
    3. class InputManagerInstaller : MonoInstaller
    4. {
    5.     public override void InstallBindings()
    6.         => Container.Bind<IInputManager>()
    7.                     .FromInstance(GetComponent<IInputManager>());
    8. }
    9.  
    10. partial class Player : MonoBehaviour
    11. {
    12.     [Inject]
    13.     IInputManager inputManager;
    14. }
    Code (CSharp):
    1. class InputManager : MonoBehaviour, IInputManager { ... }
    2.  
    3. [CreateAssetMenu]
    4. class InputManagerInjector : DependencyInjector
    5. {
    6.     private IInputManager inputManager;
    7.  
    8.     protected override void Activation() => inputManager = Object.FindAnyObjectByType<IInputManager>();
    9.     protected override void Deactivation() => inputManager = null;
    10.  
    11.     protected override object GetInstanceOf(Type type)
    12.     {
    13.         if(type.IsAssignableFrom(TheTypeOf<IInputManager>.Type))
    14.         {
    15.             return inputManager;
    16.         }
    17.      
    18.         return null;
    19.     }
    20. }
    21.  
    22. partial class Player : MonoBehaviour, IInjectable
    23. {
    24.     void Awake() => InjectorLocator.RequestInjection(this);
    25.     void IInjectable.InjectWith(IInjector injector) => injector.Inject(out inputManager);
    26. }
    Code (CSharp):
    1. [Service(typeof(IInputManager), FindFromScene = true)]
    2. class InputManager : MonoBehaviour, IInputManager { ... }
    3.  
    4. partial class Player : MonoBehaviour<IInputManager>
    5. {
    6.     protected override void Init(IInputManager inputManager) => this.inputManager = inputManager;
    7. }
     
    Last edited: Jun 7, 2023
  30. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    287
    Wow! Excellent summary with so many real code examples, including from my plugin! Thank you!

    I just want to mention that with Switchboard the InputManager can be created from within the DependencyInjector.Activation() method, rather than having to find it in a scene. However, I know that all examples were based on the same starting scenario of having the InputManager already there in the scen with an early script execution order. If you want to change the InputManager for unit testing you can just swap the normal DependencyInjector for a test version, or if the InputManager is assigned to the DependencyInjector as a scriptable object or prefab it could be easily swapped for a test manager there.
     
    SisusCo likes this.
  31. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,134
    I love the timing of your example as with its release I've been crash coursing my way through DOTS ECS and the subject that I touched on the other day was how to hook up the new input system. In this example inputs are configured through the Input Actions UI interface and the asset is configured to generate a C# script.

    System (SystemBase is managed allowing easier managed access at the cost of performance):
    Code (csharp):
    1. using Unity.Entities;
    2. using UnityEngine;
    3. using UnityEngine.InputSystem;
    4.  
    5. public partial class GameInputSystem : SystemBase, InputActions.IPlayerActions
    6. {
    7.     private InputActions inputActions;
    8.  
    9.     private Vector2 move;
    10.     private float fire;
    11.  
    12.     private EntityQuery playerInputQuery;
    13.  
    14.     protected override void OnCreate()
    15.     {
    16.         inputActions = new InputActions();
    17.         inputActions.Player.SetCallbacks(this);
    18.  
    19.         playerInputQuery = GetEntityQuery(typeof(PlayerInput));
    20.     }
    21.  
    22.     protected override void OnStartRunning() => inputActions.Enable();
    23.     protected override void OnStopRunning() => inputActions.Disable();
    24.  
    25.     protected override void OnUpdate()
    26.     {
    27.         if (playerInputQuery.CalculateEntityCount() == 0)
    28.             EntityManager.CreateEntity(typeof(PlayerInput));
    29.  
    30.         playerInputQuery.SetSingleton(new PlayerInput
    31.         {
    32.             Move = move,
    33.             Fire = fire
    34.         });
    35.     }
    36.  
    37.     void InputActions.IPlayerActions.OnMove(InputAction.CallbackContext ctx) => move = ctx.ReadValue<Vector2>();
    38.     void InputActions.IPlayerActions.OnFire(InputAction.CallbackContext ctx) => move = ctx.ReadValue<float>();
    39. }

    Component:
    Code (csharp):
    1. using Unity.Entities;
    2. using Unity.Mathematics;
    3.  
    4. public struct PlayerInput : IComponentData
    5. {
    6.     public float2 Move;
    7.     public float Fire;
    8. }

    Example of accessing it:
    Code (csharp):
    1. [UpdateAfter(typeof(GameInputSystem))]
    2. public partial struct ExampleSystem : ISystem
    3. {
    4.     public void OnCreate(ref SystemState state)
    5.     {
    6.         state.RequireForUpdate<PlayerInput>();
    7.     }
    8.  
    9.     public void OnUpdate(ref SystemState state)
    10.     {
    11.         var input = SystemAPI.GetSingleton<PlayerInput>();
    12.  
    13.         if (input.Fire > 0.0f)
    14.         {
    15.             // do stuff
    16.         }
    17.     }
    18. }
     
    Last edited: Jun 7, 2023
    CodeRonnie, SisusCo and The_Island like this.
  32. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    @Ryiah What a happy coincidence :D The list wouldn't have been complete without a DOTS example.
     
  33. andyz

    andyz

    Joined:
    Jan 5, 2010
    Posts:
    2,134
    Thanks this is useful - of course this assumes everyone does unit tests! :D
    Also I wonder how often people wish to use interfaces - believe me not everyone does because they know that in 9/10 cases there will be no alternative implementation or if there is in future the code can be refactored.
    Anyway it seems like you put them in order from simple to no-thanks! If FindAnyObjectByType is not hideously expensive it is a very nice beginners alternative to a Singleton!
    (member of Singleton-Users Anonymous)
     
    Last edited: Jun 8, 2023
    SisusCo likes this.
  34. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,009
    I'm using them all the time.
    They restrict me from exposing what should be not and are awesome for unit testing.
     
    CodeSmile likes this.
  35. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    I don't mean to imply by any means that everybody is writing unit tests... nor am I saying that everybody should always use dependency injection and interfaces heavily in all their projects or they are doing it somehow wrong.

    DI is just one tool among many others; I don't think anybody should start applying it everywhere religiously just because some guy in the internet says it can be a useful pattern :D If you haven't experienced any actual pain points in your projects that you feel could be remedied by using it, then by all means, keep doing what works for you - singletons and all.

    All I am saying, is that - especially so in larger scale projects spanning multiple years of development - making strategic use of tools like dependency injection and interfaces can give some very substantial benefits. But that does not mean they're some silver bullets that magically make everything better and better the more of them you shove into your project.

    Personally I don't use interfaces everywhere in my projects. I think having classes be tightly coupled together is in many cases perfectly fine; as long as it doesn't spiral out of control into a spaghetti monster of doom, with its spooky-action-at-a-distance tentacles entangled all over your project.

    But I do find interfaces superbly useful in certain situations. Just to name a few use cases:

    Generic Reusable Code:
    Interfaces can be a game changer when it comes to creation of flexible systems that can work with objects of many different types, which still all share some particular quality.

    For example, rather than creating separate extension methods for
    List<T>
    ,
    T[]
    ,
    HashSet<T>
    , etc., I could potentially use
    IEnumerable<T>
    instead and only write those methods once.

    Similarly, I often find it useful to have a modular system of triggers and effects that can be hooked together for various effects, especially in the UI layer. Instead of writing components like OnEnablePlayAudio, OnDialogEndOpenPanel, OnCollisionBroadcastEvent, I can create triggers like OnEnable, OnDialogEnd and OnCollision, and then plug them into any component, scriptable object or plain old class object that implements an interface like ITriggerable.

    Would it be possible to create a game without using such a system? Yes.
    Does it make my life easier when I don't need to have the same code duplicated in multiple places? Yes.

    I often find that very simple abstractions for low-level objects, defining only one or two members, can be especially handy:
    IEnumerable<T>
    ,
    IValueProvider<TValue>
    ,
    ISaveable
    ,
    IInteractable
    and so on.

    Swappable Modules:
    In some situations I want to add a layer of abstraction between my own assemblies and some third party asset that I integrate into the project. Especially so if I think it has a higher-than-normal chance of getting substituted with some other solution at some point during the project's lifetime.

    For example, if I wasn't sure if I wanted to use Unity's input system, or some third party solution, I could start the project off using whichever one, but put an interface in-between it and the rest of the codebase, so that swapping it out later with a different one later won't be too painful.
     
    Last edited: Jun 8, 2023
    PanthenEye, rdjadu, CodeSmile and 3 others like this.
  36. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    287
    Also, interfaces allow you to easily modify any class to suit your loose method coupling means. This is a great way to keep many different things in one collection at run time. (Edit: Or, attach and swap many different types of objects to one type of reference.) You don't have to derive the relevant class from a particular base class in order to specify its capabilities. You can add as many interfaces onto a class as you like, and you can always create a wrapper class that implements the interface and talks to the concrete class, even if you didn't write it. That's what makes them so handy.

    In fact, it sort of makes your object oriented code more like ECS by creating a data set that describes various different types with a uniform schema, then feeding them into systems at a single location that operate on all of the elements. Obviously, the performance is completely different, but similar patterns become available.
     
    Last edited: Jun 10, 2023
    SisusCo likes this.
  37. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,580
    Deleting own posts in mid conversation is very unwelcome and unfriendly behavior to the community.
     
    MadeFromPolygons likes this.
  38. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    My issue with Scriptable Objects was when dealing with hundreds if not thousands of data points. You can't pre-serialize n number of dynamically spawned objects in your Unity Project. You can create an instance of the ScriptableObject at runtime, but then it's just like a big ugly plain C# class. You can do work arounds like using plain C# classes and just using the ScriptableObject to store a list of those classes, but that felt like a bad work around.
     
  39. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    I will also double post because it's topical. I found myself purely using VContainer to inject interfaces, but that was causing DI to spread throughout my project like overuse of async might, where as I wanted to just have DI for higher level stuff, so I created this tool that allows me to Serialize Interfaces in the inspector (so long as they're a MonoBehaviour or ScriptableObject)

    https://forum.unity.com/threads/ser...nterfaces-in-the-editor-version-0-01.1465469/
     
    SisusCo likes this.
  40. ippdev

    ippdev

    Joined:
    Feb 7, 2010
    Posts:
    3,793
    Q: How many enterprise coders does it take to change a lightbulb?
    A: 17 interfaces and DI.
     
    Ryiah and Saniell like this.
  41. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    Um... you must be remembering that joke wrong. The whole point of interfaces and DI is to make it as easy as pie to change things :p

    Actual A:
    case-study.gif
     
  42. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    Besides, a more pertinent question would be:

    Q: If it takes a DI framework one minute to change one light bulb,
    how many minutes does it take it to change 100 light bulbs?

    Also one minute.
     
  43. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    EDIT: Was asked to remove images of code. I somehow acquired something similar to vampire survivors code (in minecraft)


    Summary of removed images - proof they use Zenject.


    There are also quite a lot of signals declared in a global signal bus, looks like Zenject was used for events.

    There are also some other interesting things not Zenject related worth mentioning. Almost all classes had serializable versions that only contained data (save/load I assume).

    Inheritance was used quite a bit, I think there's 5 parents between each enemy type and the MonoBehavior's.

    There was also no escaping the megascripts, both the enemy and character controller have 30+ methods.

    It seems that zenject was mainly used to manage the life cycle of non UnityEngine.Objects, which is arguably overkill and could be achieved with a more simple system, one that doesn't currently exist. The use of megascripts tells me there's still a need for building more modular systems even with OOP monobehaviour.
     
    Last edited: Aug 22, 2023
    SisusCo likes this.
  44. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,467
    A little tip regarding Megascripts if they really don't seem splittable in a meaningful way (e.g. by introducing static (extension) methods in helper classes): Make use of the "partial" keyword and split the class into multiple files at least.
    There ALWAYS is some way to group some of the functions.

    Large numbers of methods isn't the main identifier of the monolith antipattern, otherwise e.g. the Unity Vector3 classes would count too.
     
  45. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    Just bothers me that it breaks open/close. If the Vampire Survivors dev ever want to add new functionality to their characters / enemies, there's a ripple effect, at the very least they have to update all their serialization scripts, and because their type definitions are all enums as well, some of those will have to be updated, and finally because it's inheritance base, they'll likely have to add that feature to every enemy type.
     
  46. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,114
    I'm very curious to hear if you have any thoughts about what such a simpler system might look like?
     
    Last edited: Aug 22, 2023
  47. Macro

    Macro

    Joined:
    Jul 24, 2012
    Posts:
    45
    I dont often get involved in these conversations much as its like immovable objects and unstoppable forces, but I completely agree with a load of the points the OP raised, and as always a lot of the flack against the idea of DI/IOC seems misguided from a lot of people here, like you can still use ECS style paradigms while using DI. I maintain a C# ECS system which is built around the notion of DI and first class events etc, and you can still get memory/cache prefetch, contiguous memory etc while using DI as you are just orchestrating high level dependencies, not actual data.

    I think this is one of the main things that annoyed me looking through the thread, all that your DI framework should be doing is orchestrating higher level logic blocks, for all intents and purposes it shouldn't matter if a DI system newed up 10 classes for you or you manually newed up 10 classes, the same 10 classes are newed up. So in ECS it doesnt really matter how your Systems are orchestrated, be it via DI or some other mechanism, that has no real bearing on how the underlying entities and components are processed, its how they execute which is the important bit and how the component data is stored/fetched, and again you can setup an entire ECS framework by DI and still get great performance and all the benefits of the approach (again talking ECS in general not unity specific) as long as your data is laid out in the right way and accessed in the right way, and again DI really has no bearing on that.

    Some people seem to be talking about service location in the same context as DI as if they are the same thing, they are opposite ends of the spectrum in most cases. So yes at a high level both approaches (service location and dependency injection) are both doing the same thing, they get an instance of something for you from some container/root but the way they go about doing it is totally different and I would say ServiceLocation is inferior and a last resort, but all of that hinges on an understanding of Inversion of Control and the ability to be able to adhere to IOC which you cant other than properties and attributes in MonoBehaviours.

    I get that its complex, and people could go down a rabbit hole of trying to do too much with DI instead of just setting up high level infrastructure, but the same concerns exist in every facet of development. Its like when someone first learns to use inheritance, and suddenly everything can be inherited, or unit testing and suddenly we need 100% coverage, or singletons and suddenly everything can be wrapped and exposed as such. These are all great approaches but like with anything if it is used outside of its intended use case or to solve a problem that could potentially be solved a better way then it just leads to problems in most cases.
     
    Last edited: Aug 22, 2023
  48. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    Don't get demoralized again, because it doesn't seems like Zenject is even required for this case.
    Cause in the end its just plain MonoBehaviour manager galore.

    While VS has potential, it still doesn't break limits of what fully multithreaded application could pump.
    At 25+-30 minutes mark it can turn into a 20 fps slideshow on some devices. And numbers of entities are still pretty low. Lots of room for improvement.

    Yeah, that's pretty much what usually all of us do for default "MonoBehaviour" based scripts.
    They have to communicate somehow. Interfaces aren't the only thing for the decoupling.
    Events and in this case signals are.

    Then again, if you use ECS properly - you don't actually need events. Everything is decoupled by default.

    DI is still popular in the industry.
    Albeit more controversial with all "who owns Zenject".
    What DI framework to pick, and if there are any that doesn't eat up all performance on boot.
    Reason why its popular is different - its an attempt to make codebase more maintable in the long run (like 5-10+ years). It usually fails at some point though.

    Embrace refactoring as a process and make a game you like. YAGNI.

    My advice - use Entities in a hybrid setup. You'll have best of both worlds.
    Decoupling, testing support, free visual / physics layer, and no releflection overhead.
    Once you're set on how things should work - move more of the stuff to the ECS side.
     
    Last edited: Aug 22, 2023
  49. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    I looked over your initargs which I didn't have a proper look at before. I think your [Service] usage more or less does it. In the examples of Zenject usage I've seen it's mainly because people wanted a scene scope or application scope shared class, so seemed overkill to use Zenject for that.


    Are you saying those benefits of no reflection, decoupling, testing support etc come from ECS, but I should start coding OOP then move it to ECS once I'm happy? I got a little confused with this sentence.

    There's a few things that have bothered me about the ECS samples so far. I don't like how all systems have poluted the same "world". Every system is running even if they have nothing to do with the scene being tested. I hope it's easy to setup and change worlds to keep things clean.

    I've also really hated how they handled UI binding.

    I also find it funny because people say you shouldn't use [DefaultExecutionOrder] because it makes your code hard to follow, but then they'll have like 5 [ExecuteBefore] attributes on an ECS system. I hate that it's sorted out all relative to each other, why isn't there just a single source to define system execution order.

    Also why does it have to be so verbose. In Bevy a system execution is a single function, in dots ECS it's a function inside a struct inside a struct with like 5 attributes.

    Finally, I'm a pretty visual thinker, coming from a 3D modelling background, and probably my only thing I love about monobehaviours is having UI representation of my game.

    This has turned into more of a learning exp for me than anything else. I might just have to build a micro game in the framework's I've touched to get a feel of what I prefer.
     
    Last edited: Aug 22, 2023
    SisusCo likes this.
  50. PanthenEye

    PanthenEye

    Joined:
    Oct 14, 2013
    Posts:
    1,763
    The benefits of Zenject are lost on me in this use case, which comes down to a single scene of a relatively simple roguelite. The heavy use of inheritance also implies a lot of virtual calls in the main loop, which is slow code by default. To me, the biggest challenge seems to be reaching sufficient performance across a wide variety of hardware. And you don't even need ECS to get great performance for a Vampire Survivors-like. Just write performant code with data oriented design in mind:
     
    xVergilx likes this.