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

Feedback Memory efficiency for a Dependency Injector

Discussion in 'Scripting' started by BenjaminApprill, Sep 6, 2023.

  1. BenjaminApprill

    BenjaminApprill

    Joined:
    Aug 1, 2022
    Posts:
    106
    I am working on a dependency injector for writing scripts. I've got the concept down for injection.

    What I am not so sure about is removing instances from memory when they are no longer referenced.

    I am considering a counter for keeping track of the references, but I've read that C# automatically tracks this, and deletes the instance once it doesn't have any references... If I can remove the instance from the collection, C# should ideally clean up the instance on its own?

    This is at least a step in the right direction, but I don't want to over-complicate it if C# already does this by default to some degree. Curious what kind of feedback I can get for this approach. Thank you very much!
     
  2. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    Instances of classes that derive from UnityEngine.Object need to be destroyed manually.
    Instances of types that implement IDisposable need to be disposed manually.
    Other than that you can rely on the garbage collector to clear up the objects from memory once they are no longer being referenced by anything.

    The way DI frameworks typically handle this is by having the DI containers implement IDisposable. When you call Dispose on the container, it will call Dispose for all services that were created by it as well.
     
    BenjaminApprill likes this.
  3. BenjaminApprill

    BenjaminApprill

    Joined:
    Aug 1, 2022
    Posts:
    106
    This almost works. But one reference remains in the actual injector collection. Even if all of the containing objects are destroyed, one reference lingers in the collection... This is requiring me to have some kind of check for when the reference count hits zero.

    This is almost what C# does by default... But I still have to remove the instance from the collection for the garbage collector to actually clear them, or so it seems... My idea is to just use a struct with an int counter and the instance, and change the counter any time the reference is requested or released, and react accordingly if the count is zero.

    I am not sure how else to keep track of when the instance needs to be removed from the injector... If there was some kind of callback in C# itself for when it detects reference changes, I could possibly plug into that. I will have to take a look...
     
  4. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    Globally shared services, aka "singletons", usually just remain in memory for the entire lifetime of the application, or until the root container is disposed.

    It wouldn't really make sense (in most situations at least) to dispose these services once they no longer have any active clients, because it could be that they are still going to get new clients at a later point in time. E.g. when unloading one scene and loading a new one, the number of clients could just temporarily dip to zero.

    What you could do instead is couple a container to the lifetime of a scene or a prefab instance for example, and then dispose all its services, and clear the collection with references to them, when that scene or prefab instance is unloaded.

    With transient services, aka ones where a new instance is created with each request, if their type does not implement IDisposable nor derive from UnityEngine.Object, you can just avoid holding any references to them. If you do need to manually dispose or destroy them, then tying the services to the lifetime of a scene or a prefab instance could be the solution here as well.

    You can add a finalizer to your client classes, and it will be called right before the garbage collector releases it from memory.
     
  5. BenjaminApprill

    BenjaminApprill

    Joined:
    Aug 1, 2022
    Posts:
    106
    I am trying to avoid relying on the Editor-side for injection as much as possible... One Scene, no Prefabs. Everything built through systems(services).

    Part of my concern is having irrelevant instances loaded into memory... It makes sense, that you don't need the Start Screen systems while you are in the game for an extended period of time. This can represent game states as well. The idea is to remove those instances that are now out-of-context, for less memory utilization at run-time.

    If there aren't that many instances, you are correct. All of the instances can just get set-up for the lifetime of the application... I suppose it comes down to memory consumption.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,560
    That's unfortunate. Unity's built-in dependency injection works so well and is so tightly integrated into Unity that people don't even realize it is there.

    That's all a scene or prefab is: dependency injection.

    If you are trying to get away from basic scene and prefab dependency injection, I kinda question why you would even consider using Unity. You're basically turning your nose up at one of the most robust game content management systems I've ever seen fielded in order to go with your own homebrew system.

    Don't worry though, it's a common pattern among people who think their code constitutes "The application."

    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

    I hope you are able to find a constructive solution to whatever complex problem you are encountering. Good luck!
     
    Sluggy likes this.
  7. BenjaminApprill

    BenjaminApprill

    Joined:
    Aug 1, 2022
    Posts:
    106
    I've gone with the Editor-side injection a few times. I've decided to try as much instance injection as possible. It doesn't seem like such a big deal that I try this... I am curious about the potential workflow benefits of being able to write abstract systems like this.

    I know this approach works against Unity's natural and inherit qualities. But I am burned after my last attempt to build a game using that approach, and have decided to attempt this sort of "opposite" approach as a result. I don't mind using Unity regardless, as I appreciate the Egalitarian nature of the community and the Engine in-general.
     
    CodeRonnie likes this.
  8. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,560
    This surprises me. Care to offer a post-mortem of any details?
     
  9. BenjaminApprill

    BenjaminApprill

    Joined:
    Aug 1, 2022
    Posts:
    106
    It seemed like focusing on system instances is more efficient than using just Mono OOP. The difference might be negligible, I'm not really sure...

    I needed to apply an Abstract Factory pattern for creation, but I had tried to commit to just using raw Mono OOP. It seemed like I should re-think my approach, and the injector seemed like the way to go after that. It allows me to write systems primarily, while the injector ties it all together for me.
     
  10. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,560
    Just from a distant "first smell" of what you report above, it sounds like you were conflating your model with your view, eg., putting model (eg, the state of your game) into Unity objects that are used to present the state of the game.

    This is almost always going to cause long-term scalability problems. It's cute for game jams, perhaps for little VFX like particles and splashes and other irrelevancia, but not so much for production code.

    There should be a clear partition between model and view. This is often glossed over and blended all into a morass of syrupy mess in most Unity tutorials, resulting in code that lets you rip out a quick demo, but long-term support of something so conflated becomes almost impossible.

    Have you partitioned the notion of your own internal data model of what your game is doing from the chosen Unity presentation layer??

    Ideally your game should be 100% runnable from the command line, if you really wanna get absurd about the partitioning.

    Specifically this means that any point you could destroy EVERYTHING in your scene and then call a method to re-realize your model data into a view to be presented in Unity.
     
    spiney199 likes this.
  11. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Even better if you can serialise this model data too!

    Nothing like being able to just write out the state of parts of your game to disk as well. This does disqualify Unity object references, but that's easily worked around.

    Though 100% agree overall. Been getting more and more into this separation and it really smooths things out. Hell there's plenty of types of games, such as Minecraft/Terraria type games that would not be possible without this separation.

    It can be as simple as the use of scriptable objects to take a more data-driven approach.
     
  12. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    You'd need to introduce some sort of concept of these "contexts" in your codebase, so that you can link services to them and then bulk unload the whole thing when leaving that context.

    Scenes usually serve this function in Unity, but you could add something similar for example as part of your state machine system.
    Code (CSharp):
    1. public class GameState
    2. {
    3.     private readonly DIContainer container;
    4.    
    5.     public GameState(DIContainer container) => this.container = container;
    6.    
    7.     public T Instantiate<T>() => container.CreateInstance<T>();
    8.    
    9.     public void Destroy(object instance) => container.DisposeInstance(instance);
    10.    
    11.     public void Exit()
    12.     {
    13.         OnExit();
    14.         container.DisposeAllInstancesAndServices();
    15.     }
    16. }
     
    CodeRonnie likes this.
  13. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    I am confused by your overall goal, and sadly am confused by technical lingo, like "dependency injection", what are we doing marinating meat? lol..

    But if you mean by keeping instances as classes as a reference, I'm pretty sure those references are just pointers to that specific spot in memory. A prime example of this is Object Pooling, as you need to communicate with the class in order to Enable or Disable that gameObjects active state.

    Then you say you're worried about memory, which I'm having a hard time figuring what you could have in game that would be so resource heavy, unless say a character model with tons of vertices in it's mesh, or maybe even a terrain? Or possibly a bullet hell, to where you don't want 1000+ bullets saved in a pool constantly, I guess it all depends on a case by case basis.

    Do you mean getting prefabs via resources folder, or having them set within another prefab, that lost connections? Personally I make a "ObjList.cs" that houses any and all prefabs, materials, or even color pallets, and with a simple singleton reference to that class I can get whatever I need. And never had any issue with having those set in the Editor.

    And in another case, I made a specific folder within the resources folder, to read all the classes within it and create those classes at runtime. Which were actually "resources"(wood, iron, copper ingot, etc...), to what would be referred to as "mod-able" for later use, but even then they still had a static instance list used for reference upon creation(map load).

    But to be fair, I can't be sure you're worried performance, or just ease of use? And I guess a good example of what exactly you're worried about taking up too much memory? Or are you just mainly worried about code structure?

    As I feel there could be many examples, experiences, or differences in opinion, that could be thrown your way. :)
     
  14. BenjaminApprill

    BenjaminApprill

    Joined:
    Aug 1, 2022
    Posts:
    106
    The first thing that comes to mind when you say Model/View is MVC... Which feels too simple to plug into a game model...

    At some point, the game state has to at least reference to some kind of data update. Completely separating these things, even with "middle men" doesn't seem realistic...

    Basically, anything I can do by hand in the Inspector, code can do faster and ultimately more accurately. That is what I am trying to leverage on this attempt.
     
  15. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,560
    It's actually not bad with simple conduits, like this one for integers:

    Code (csharp):
    1. // just a simple bidirectional link object to
    2. // bidirectionally bridge data with presentation.
    3.  
    4. public class IntegerConduit
    5. {
    6.     public readonly System.Action<int> Set;
    7.     public readonly System.Func<int> Get;
    8.  
    9.     public IntegerConduit(System.Func<int> Get, System.Action<int> Set)
    10.     {
    11.         this.Set = Set;
    12.         this.Get = Get;
    13.     }
    14. }
    You can make ones for just about any quantity to make the view-side "live operate" on some save data that you might later serialize as a single class.
     
  16. BenjaminApprill

    BenjaminApprill

    Joined:
    Aug 1, 2022
    Posts:
    106
    Yes. Unity doesn't really provide a default way to do this well. The drag-and-drop approach leans towards simplicity, which is part of what causes the problems in the long run.

    Pushing for something more sophisticated seems to be through moving away from that default approach, even though Unity is designed for it...

    My current approach is for pushing in that other direction as hard as possible...
     
  17. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    It's not about 'complete separation', is about having distinct layers. This can just be about having a series of plain C# objects that represent the data portion of your game, and have instances of such serialised into Unity objects, of which represent the 'view'.

    I'm working on a game along the lines of the old Motherload flash game. So naturally the 2d tile grid and world generation is done in pure C#, with the top-most object getting serialised into a container monobehaviour.

    The player, too, controls a robot with interchangeable parts. This is represented with a tree-like data structure in pure C#, with, again, the top most object being serialised into a component.

    In both cases I can design parameters in the inspector, click a button, and generate the visual representation like that.

    On the contrary, if you take a data-driven approach, referencing objects in the inspector becomes quite the powerful tool. Using Scriptable Objects with this approach is particularly flexible in a lot of scenarios.
     
    Ryiah and CodeRonnie like this.
  18. BenjaminApprill

    BenjaminApprill

    Joined:
    Aug 1, 2022
    Posts:
    106
    ScriptableObjects can do that, but that requires some hand-crafting in the Inspector... It increases chances to miss references as well. The approach I am contemplating is designed to alleviate these concerns. Both with ScriptableObjects, and GameObjects in the Hierarchy.
     
  19. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Null refs are going to happen, inspector or no inspector. At least when it's an missing reference in the inspector, fixing it takes all of one second.

    Editor/inspector tools can further reduce the chance of this happening.

    I think your concerns are a bit misplaced.
     
    Kurt-Dekker likes this.
  20. BenjaminApprill

    BenjaminApprill

    Joined:
    Aug 1, 2022
    Posts:
    106
    I am referring to writing Single Responsibility systems, but their references get plugged in by the computer. No drag-and-drop referencing. No constructor calls in code. Reflection can be used to make the computer do all of this.

    I feel like this is about efficiency, AND ease of use. This is primarily a workflow solution, but it does come with its overhead. Being wary of the efficiency is important for the scale of games I want to make.
     
  21. BenjaminApprill

    BenjaminApprill

    Joined:
    Aug 1, 2022
    Posts:
    106
    The idea of a null ref ever happening is hard to believe with how I am envisioning the design... It would have to be hardware corruption.

    This idea goes beyond what is craftable in the Inspector. Runtime instancing and referencing is also handled by this "injection" approach. The Unity defaults can be reached for when necessary, but having all of the rest handled through a computer-run "Dependency Injector" has some up-sides I am curious in leveraging.
     
  22. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Null refs happen because humans make mistakes. It's hard to believe that you'll never make a mistake that leads to a null ref.

    In any case dependency injection isn't anything particularly magical. A constructor is dependency injection. A 'initialise' method is dependency injection. Hell, passing a parameter to a method is dependency injection!

    Dependency injection frameworks are just a top-level system to automate that to some degree, but they sure as hell don't stop you from making mistakes.
     
    Kurt-Dekker and CodeRonnie like this.
  23. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    While a DI framework won't make it impossible for null reference exceptions to occur, it can help minimize how often they happen by minimizing the need for humans having to remember to manually drag-and-drop references or manually tweak script execution order settings.

    It can often also help surface potential issues earlier and more reliably, than if dependency injection wasn't being used (either at compile time or immediately on application launch).


    For example, if you used a Logger scriptable object by default for logging messages to the Console in hundreds of components, you could wire up all of these connections with one line of code with a DI framework and then basically never again have to think about it.

    And if in the future you want to change all of these hundreds of components to use a different logger in release builds, that'll be trivial to do as well.

    You can also rename fields and move classes to different assemblies without having to worry about serialized references breaking.

    Yes it is super simple - and I think that's a big part of why it's so appealing :) Just because something is simple doesn't mean it can't also be really powerful. Many programming paradigms are based on super simple ideas like avoiding mutable state, or encapsulation, yet they can have a profound effect on the codebase as a whole when used consistently and with thought.
     
    CodeRonnie likes this.
  24. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,560
    This viewpoint is common in academic circles, aka, "the CIS-100 crowd."

    As you gain more experience you will see the limitations of this viewpoint.

    It sorta comes down to the concept of idiot-proofing.

    The more you idiot-proof it, the more resourceful the idiots become.

    I know because I am a very resourceful idiot. I can violate pretty much every assumption you can imagine.

    A far more robust approach is to guard your code in ever-more-trusted layers of encircled tested boundaries.

    Data coming in off the wire or out of a user's save file? Check and validate every single thing you care about, then probably a few more things that are capable of blowing up your game.

    A built-in shader being DMA-ed off your console DVD into the GPU? No need to check because you would have caught the issue long ago if there was one.

    Almost everything else lies in between those two.

    Go make your game! :)