Dependency Injection in Unity [Inversion of Control Container]

No. I would argue that the point is an attribute shouldn’t do anything unusual behind the scene. I would also argue that the name of the attribute is non-descriptive. Inject what? Where? How? Why?

An attribute is supposed to be “metadata”, something attached to the type of the field. In case of unity, there’s extra data for the serializer. Similar thing happens with data configuration for C# marshaller.

With Inject, the intent and functionality is not obvious.

[SerializeReference] says hi.

This stuff is only “BlackMagicVoodoo” until you take the time to get familiar with it… just like the voodoo of Unity calling my private Update() methods, etc.

If you’ve decided to use DI and imported a framework and are using it, then it’s Inject attribute should definitely be defined and known at that point! :wink:

Listen? Sure. Follow them blindly…? I hope not.

This whole thing makes me think of The Law of the Hammer, i.e. “If the only tool you have is a hammer, it is tempting to treat everything as if it were a nail” - Abraham Maslow. Learn a bunch of approaches, then pick the best one for each job on a case by case basis. Familiarity is a valid benefit, but it shouldn’t be the sole decider.

5 Likes

Or you could write that same logic in OnValidate, no dragging required. No black-box injects required.

Clean, obvious, and easy to swap without the need to dig into bindings that someone accidentaly messed up later down the line. Unity already has what you’re asking for without overburdening Awake’s or using reflection.

I mean, I get it. Business apps doesn’t care about performance.
Business devs like to throw all kinds of weird patterns into game dev to then figure out they:

  • Do not work;
  • Work but actually make things worse to maintain in the long run;
  • Work but ruin the performance;
  • Rage post on the forum;

Instead of just learning the engine they attempt [and fail] to use.

And regarding non-reflection binding - I’m yet to see fast enough injections that does not rely upon reflection.

2 Likes

It seems the problem here is, lack of the design ahead.

Unity for prototyping is great indeed. But shuving exact prototype into production should be no go. That regardless of paradigm used.
Created assets should be structured carefully, so no entering into spagetti and circural reference mess.

Later refactoring may be much easier and safer, as it most likely will happen at some point into production.

I found ECS to be slow and require a lot of boilerplate up front at first.

But once the game is more than like 5% complete, I find that “make it up as you go” OOP just falls apart. Production speed decreases rapidly because in order for anything to be extensible it has to go through lots of abstraction processes and the exact process can be different for every little thing. Bug hunting becomes a nightmare because each system is made in a unique way, so the first thing you have to do is familiarize how the bugged system works.

The major thing that pushed me to ECS though was that I was seeking standardization. Working on a team with other programmers, I think it’s bad if each person just carves out little fiefdoms and does things their own way. You end up with a project that nobody understands and rather than team boosting each other, they instead become dependent on each other. As in, one person cannot easily debug a problem if another person designed it using a unique paradigm.

That was the major attraction about ECS because it has simple rules for how data flows from one class to another. Then if I find a bug while playtesting and its in code that somebody else wrote, I dont face down time to have to reverse engineer their stuff - I know how it all works.

What I’ve enjoyed about ECS so far is that it seems to build an assembly line such that the further you get into project, the more speed boost you get. Lots of reuse across systems and also since you are employing the same patterns over and over, it can become second nature how to link things together, so then you only have to think about how systems work - not how they communicate.

Anyway, that’s my spiel about ECS not because it’s the exact subject here, but only to add a counter point that maybe it does indeed require dedication, but I think its less dedication (in the long run) than fighting what I call a make-it-up-as-you-go design.

note that when first prototyping some new system I often do just put code whereever is easiest to work with immediately, and sometimes that means a more OOP way, but as soon as I verified how it works I categorize it to systems/components using ECS style paradigm. So its not like can only choose one tool and use it 100%.

6 Likes

If Unity supported Interface serialisation without hacks or Odin it might help to avoid DI frameworks: Feature Request - Serialized interface fields - Unity Forum

4 Likes

How do you handle things like UI, Animation, Sound and possibly working with 3rd party services database, user logins etc? I assume there’s some parts that need to be OOP

Well, first thing I should mention is that 99% of my coding has been done in unreal, and most of that using visual scripting. So I am not doing “real” ECS but I take the principles of it as far as I can.

Some things are easier to do in OOP way simply because the engine is setup that way. For instance, if I am working on a team and I need to have some system isolated from others to prevent merge conflicts (thats more of a visual scripting issue because they are binary assets), then I put that system into an component and it can attach to some actor/gameobject.

So at a low level that isn’t accomplishing anything that ECS is meant to, but at high level the same principles of how systems and components communicate can still be accomplished.

For UI I have been able to run it all the same as most other code. Basically a system just listens for events and then feeds data into widgets. Same thing for input - for instance Unreal has several ways to handle input, but I only read input in a single place and then that directs it whereever it needs to go via events.

Sound gets a big benefit from ECS architecture. Before I just had calls for SFX to happen strewn all throughout code. A complete nightmare to solve any issues with that. Now I just make systems like “Player SFX System”, “Enemy SFX System”, “Ambient SFX System”. Of course you can make systems smaller or broader as you need, its totally arbitrary. Usually I try to keep them as large as possible and only split up if really necessary.

These systems simply listen for events. So there may be a “projectiles system” and whenever a projectile is fired, it puts out an event. The SFX systems may be listening for that, and then they can react.

So the major benefit here is that, lets say some months from now I notice some bugs related to SFX when a missile fires… I just open the system associated with that and I can be sure that they problem will be in there. I dont have to think about a nearly infinite number of ways a missile game object might be interacting with other things, and if any other class and have to jump throughout codebase like a detective, wasting all my precious brain juices.

I keep all of the events in an abstract, globally accessible class. I guess you might call that a singleton? In unreal its either the game mode or play controller - I am certain unity has some equivalent. Basically any class can easily find this global class and then request to call one of its events. Then any system anywhere is going to receive that, but because all any class knows about is this one singleton, no systems know about each other. So systems can be added or dropped and its never going to break any code.

One area where I mix in some OOP is with animations. There is a lot of animation focused features that you can only get if you work within specific animation focused classes, and those are associated with an actor (aka gameobject). So it is just simpler to use that, though as much as possible I try to have my systems pump data and events into the animation handling class. The reason I prefer it that way is so that if I have a bug or I need to refamiliarize how things work with the animations, I dont want to have to jump around 50 places. I want to view one place that was easy to find with text search and be done.

I dont know a lot about 3rd party databases - i looked briefly at SQLite but ended up not needing it. But I dont expect that would be any different - just a system which has the responsibility to get/send data to and from the database, right?

I probably use a lot of terminology wierdly- when I saw OOP, what I mean is that both logic and state (e.g. logic = code that calculates stuff, and state = variables) are housed within a class and that class it mostly responsible for itself. Then if classes need to interact in some way, thats where my problems occur. Becomes too complicated fast. So I like a more top down approach where I can just direct what I want to happen and I can be sure that if there is something happening with, say, Missiles, I will see everything about that in the Missiles System. If it happens that having a bit of code actually happen in some gameobject is easier, I ofcourse just do that, its not the end of the world to break protocol occassionally.

An example is in an enemy AI system I’m working on. 90% of the code happens within two systems: Enemy Lifespan System and Enemy Behavior System.

But for each enemy, I use a few timers that makes them change behavior at different rates. For instance, every 3 seconds I just flip a bool and enemies that rush player use that to alternate which side they flank around.

In a System, I’d need an array or map (aka dictionary) to associate each enemy actor/gameobject with that bool, or more like, a struct of various data. That gets a bit unwiedly, so just for that timer I let it run in the enemy class, that way each instance can hold a bit of unique state and I dont have to do unwieldy data management so much. This is just convenience though and I think the wonkiness is mostly an issue of visual scripting - it’s just not great for maps, arrays, and struct stuff.

So, TLDR: Yes I still use a bit of OOP - basically whenever it is more convenient to do so, and that is usually when not using OOP means that I wouldn’t be able to use some feature of the engine easily.

I think this system makes most sense to me, because my young adult life was spent in the miitary. I think this has a lot in common with military communication methods. Basically, i dont want for privates in the field to make decisions. They should report what is happening, and then somebody in central command who is listening to all the goings-ons will tell the soldiers in the field what to do. That’s pretty much the gist of it really. If soldier A bumps into soldier B, I never want for them to make a decision what to do. They should only report that a bump has occured, and then the Commander of Bumps is going to decide what happens next.

For me as the programmer, this seems to make it simply a lot easier to read the code and know how it works with the least amount of effort.

I see what you mean. I guess the ECS you’re talking about isn’t Unity DOTS ECS. Seems like more of a observer pattern architecture.

I think I’ve had a break through with IoC Containers. I was reading through the VContainer docs and was confused about this line “It’s recommended to inject MonoBehaviour instead of into MonoBehaviour”. Took me while to realise that you use monobehaviour components purely as Views.

Here’s an example of my character animation handler:

public class CharacterAnimatorHandler : ILateTickable
    {
        private readonly Animator _animator;
        private readonly CharacterMoveData _moveData;
        private readonly CharacterAnimatorData _animData;
       
        private float _animatorSpeed = 0.0f;
        private Vector3 lastPos = Vector3.zero;

        [Inject]
        public CharacterAnimatorHandler(Animator animator, CharacterMoveData moveData, CharacterAnimatorData animData)
        {
            _animator = animator;
            _moveData = moveData;
            _animData = animData;
        }

        public void LateTick()
        {
            var playerSpeed = _moveData.velocity.magnitude;
            lastPos = _moveData.position;
            _animatorSpeed = Mathf.Lerp(_animatorSpeed,  playerSpeed , Time.deltaTime * _animData.animatorAcceleration);
           
            _animator.SetFloat("MovementSpeed", _animatorSpeed);

            var trans = _animator.transform;
           
            trans.position = _moveData.position;
           
            // Rotate the y axis about the player direction
            var angle = Mathf.Atan2( _moveData.velocity.x, _moveData.velocity.z) * Mathf.Rad2Deg;
            if(_moveData.velocity.magnitude > 0.1f)
                trans.rotation = Quaternion.Slerp(trans.rotation, Quaternion.AngleAxis(angle, Vector3.up),
                    Time.deltaTime * _animData.rotationSpeed);
           
            _animData.position = trans.position;
            _animData.rotation = trans.rotation;
            _animData.velocity = _moveData.velocity;
        }
    }

What I really like about this, is unlike monobehaviour, It uses a constructor, I can abstract if I need to, all my fields are readonly, I’m only implementing the player loop methods I actually need and the lifetime of the handler is guaranteed. It’s never going to disappear unexpectedly.

Here is the registration builder / composition root:

public class CharacterInstaller : MonoInstaller
    {
        [SerializeField] private Transform viewTransform;
        [SerializeField] private Animator animator;

        [SerializeField]private CharacterStatsData characterStatsData;
       
        public override void Install(IContainerBuilder builder)
        {
            // Register Data.
            builder.RegisterInstance(characterStatsData);
            builder.Register<CharacterInputData>(Lifetime.Scoped);
            builder.Register<CharacterMoveData>(Lifetime.Scoped);
            builder.Register<CharacterAnimatorData>(Lifetime.Scoped);
           
            // Register view components.
            builder.RegisterComponent<Transform>(viewTransform);
            builder.RegisterComponent<Animator>(animator);
           
            // Register Entry Points.
            builder.RegisterEntryPoint<CharacterInputHandler>(Lifetime.Scoped);
            builder.RegisterEntryPoint<CharacterMoveHandler>(Lifetime.Scoped);
            builder.RegisterEntryPoint<CharacterViewHandler>(Lifetime.Scoped)
                .WithParameter(typeof(Transform), viewTransform);
            builder.RegisterEntryPoint<CharacterAnimatorHandler>(Lifetime.Scoped)
                .WithParameter(typeof(Animation), animator);
        }
    }

This seems quite readable to me and breaks down the data, views and systems distinctly.

An obvious issue is now we’ve lost Editor serialization, but we can afford to use monobehaviour proxy just to access data in our scope:

    public class CharacterProxy : MonoBehaviour
    {
        [SerializeReference][Inject] private CharacterStatsData stats;
        [SerializeReference][Inject] private CharacterInputData input;
        [SerializeReference][Inject] private CharacterMoveData moveData;
        [SerializeReference][Inject] private CharacterAnimatorData animData;
    }

Despite the clean structure, the system is still fully compatible with regular monobehaviours if I want to write prototype code first.

Coming from over a decade of Unity I would not want to have to work in your codebase, Why are you doing all this complex stuff for no good reason? Unity components are not purely view. They are core to rapid application development, remove the need for DI and allows you to watch values you need to monitor in runtime. This is not a MVC or MVVM system. It is a frame dependent component based architecture. It is not a windows console nor a windows form app. Good luck bringing any artists on to work with you when you obfuscate everything they require to do their work.

6 Likes
public Animator animator;
public CapsuleCollider coll;
public Rigidbody rb;
//various stats types
public int idx;
public float statsF;
public bool statsB;
public Vector3 statsV3;
public Quaternion statsRot;
//move
public float moveSpeed;
public Vector3 moveDir;
public bool isGrounded;
public bool jump;
//animator states
public bool[] animStates;
public string[] animStateNames;
public string currAnimStateName;

void Update () {
     //move code via input handling goes here
     //jump handling isGrounded check and jump input handling here
}

public void SetAnimatorState (string _stateName) {
     for (int i = 0; i < animStateNames.Length; i++) {
          if (animStateNames[i] == _stateName) {
               animator.SetBool(_stateName, true);
               animStates[i] = true;
               currAnimStateName = _stateName;
          } else {
               animStates[i] = false;
               animator.SetBool(animStateNames[i], false);
          }
     }
}

This is all you need in one handy component. Know your animator state, control speed and direction, store stats. Drop on 1 or 1000. If you have many store a reference in a manager to them. If wanting to procedurally generate then Instantiate(AnimatorComponentObj, wantedParentTransform). Hire an artist to help you and they will instantly know what is going on. Don’t make it harder than it should be. Don’t throw away for free’s like Update, FixedUpdate and LateUpdate. These are gonna roll regardless and they work with the Unity API. You gotta jackhammer your process in sideways.

1 Like

How comes you have such a big fear of things disappearing unexpectedly?
I find it is not too hard to categorize into permanent game objects, scene-bound game objects and system bound game objects (where e.g. the character is a system, every enemy is a system, the enemy spawners are a system, etc.).
As long as you do not wildly add cross references between those categories and systems, there is not even a need to often have “if X != null”, let alone needing to set up whole mechanics like DI to prevent issues that a solid, classic architecture would not have anyways.

1 Like

Enterprise software development teaches you to pay attention to object lifetime. DI (dependency injection) isn’t just about making it easier to access dependencies. It’s about managing the lifetime of those dependencies too.

1 Like

I’m quite a fan of DI, probably because I have more of a business programming background. I mainly use it for Unit testing. I also find it helps with the “Single Responsibility Principle”. If I’m Unit testing then I really need to use DI, and if I’m using DI then I need a DI container. Personally I find DI on its own kind of useless and it really requires a DI Container.

I still use the non-unity version of Zenject, mainly for services and not gameplay. It might be discontinued but it’s probably fine as it is. It was used in a commercial game (Pokemon go), it’s stable and it’s not like it needs to continually have new features added. I find it a lot easier to maintain and modify but I do lose the ability to create quick prototypes. If I’m making a POC I normally use standard Unity.

Can’t see Unity adding support for it. It only really makes sense in a few special cases or due to personal preference.

1 Like

As mentioned, in unity C# is 2nd class citizen and this sort of thing is normal. What’s more it is possible to reduce the application even further.

This example kinda doesn’t explain why you’d need Di and why does the issue with null checking arise.

Typically when promoting some practice, if the practice has a merit, it is possible to reduce the situation where this practice is needed to something like a paragraph of text. If you cannot do that, that means the merit of your practice is unclear and might not exist.

In your example with ILateTickable you’re trying to apply normal C# coding practice, which is not the right thing to do. C# is second class citizen and unity is using its dialect. Thought it being a dialect is not as extreme as in unreal, for example, where C++ relies heavily on non-standard macros and engine provides garbage collection, while disabling exceptions.

After some experience with unity, the first urge a programmer would feel when seeing your example would be to delete ILateTickable, MonoInstaller, remove Install, CharacterAnimatorHandler, make everything private and non-callable from outside and reduce entire codebase to 5 or 6 flat classes with no inheritance hierarchies. Also note, that LateTick is a bad idea, because that’s Unreal terminology and there’s already LateUpdate, meaning you’re trying to sidestep methods unity offers without a good reason.

Like I said, it would be best to focus on specific small use cases, without vague “on a big codebase”.
For example, in the past someone brought up idea of “initializer method” which is called on instantiated object before it is made active and added to the scene. Something like this makes sense.

2 Likes

The obvious null check is when a multiplayer client disconnects and their instance is destroyed.
Anything referencing the client gameobject of the local instant will throw, so you would need to write recovery logic in your classes if this happened.

There can also be issues with asynchronous scene loading via additive scenes. Unity’s service locator methods don’t work across scenes, so you will need to additively load the scene, place an object in the desired scene, then resolve it as a dependency via FindObjectOfType<>, or set the additively loaded scene to the main scene (this is required if you wanted use culling data from the additively loaded scene, for example). Sync that across multiple clients and you have to rely on coroutines or promises to ensure all objects are in the scene as they need to be.

Although I don’t need to give explicit examples, null is universally known to be an issue in software design: https://hackernoon.com/null-the-billion-dollar-mistake-8t5z32d6

That’s fair. This is from the VContainer documentation:

"In Unity, MonoBehaviour is the entry point for our C# code. On the other hand, MonoBehaviour is also a “View component”.

In modern application design, “separation of domain logic and presentation layer (View component)” is important.

It is against this that MonoBehaviour has many roles (event handling, control flow, domain logic calls, etc.) in addition to its behavior as a View.

One of the purposes of DI is IoC (Inversion of Control). With DI containers, we can make pure C# classes the entry point (not MonoBehaviour). This means that the control flow and other domain logic can be separated from the function of MonoBehaviour as a view component.

View components are dynamically created / destroyed at run time, while all “features” such as control flow and domain logic have a more stable lifespan.

Generally speaking, it’s a good idea to make the View layer stateless and separate it from control flow and data management.

This is the main reason I like DI."

I would add that it also make integration testing easier, which becomes more useful as your project scales.

To be fair, ILateTickable just comes from the framework I’m using. It’s very aware that it’s sitting on top of Unity, I think this is the reason they used it. ILateTickable is a system method, LateUpdate is a view method. ILateTickable runs before LateUpdate as well, so there’s some argument for them being different.

That’s true, it could be smooshed together into a Player class that handles everything (despite violating single responsibility) and in the example I gave I’d agree that would actually be the right way to do it.

I’m going overboard with the DI just to get a feel for it. I’m not sure how I can describe a big codebase in a way that isn’t summarised, it’s literally thousands of classes and a couple hundred scenes at this point.

I’ll be blunt. I consider goals listed by this framework a waste of time.

At the beginning of programming career some people go through the phase, where they want to make things beautiful. Then they spend inordinate amount of time designing “elegant” class hierarchies and reworking them again and again, except that none of this affects the actual functionality of the program. That creates illusion of useful work. You keep rearranging the classes, and behavior of the program does not change.

The description you gave is an example of this very behavior. They’re trying to religiously follow SOLID without questioning if this is even necessary. As far as I know, It isn’t.

MonoBehaviors are not views. Unity framework is not MVC. So, “LateUpdate” is not a “view method”. Additionally mechanisms like ILateTickable are likely to be wasting CPU cycles doing nothing useful. Unity already performs caching for methods like LateUpdate and does not call them if those are not present.

Sure, playing with new approach to learn and see how you can use it is fine, just always keep in mind that you might need to throw it into garbage bin in production, plus it is necessary to spot a “honeymoon phase” if you enter it by accident.

Regarding the situation, no offense intended, but there’s a popular saying: "If you can’t explain it simply, you don’t understand it well enough.”. So, yes, normally I’d expect you to be able to describe the problem you’re facing in concise way. If you cannot simplify the problem, it may be a good idea to analyze it, ponder it and think about it for a while. You may reach some important conclusions. Also, having thousands classes may be an indication of a problem. Meaning it may indicate that the software you’re working on is being overengineering, which can drive up maintenance cost up in the long term.

Anyway, it is simply my opinion. This sort of discussions tend to last many pages, at this moment I don’t have much else to add to it.

5 Likes

What is “the average unity architecture”?

Keep in mind that professionals can’t ganerally share their work source online, so what you see online doesn’t necessarily represent how people do larger projects.

1 Like

Classic Unity architecture would be using Unity in the way it was designed since 2005, which inspired by Scott Bilas’ 2002 GDC presentation on Data-Driven Game Object System.

The usual way this manifests is that everything becomes a monobehaviour attached to a game object so things like your data, logic, events, event dispatcher, your ‘thread’ manager in the way of coroutines, your object lifetime and states (OnDestroy and OnDisable) as well as other meta systems like tags, layers and id via a name.

I’m very thankful to this system as it made coding easy and approachable, and fun! I just wonder if there’s a better way of doing things for certain applications.