Search Unity

Applying Dependency Inversion Principle in unity

Discussion in 'Scripting' started by SprayNpraY, Nov 9, 2021.

  1. SprayNpraY

    SprayNpraY

    Joined:
    Oct 2, 2014
    Posts:
    156
    Hi I've recently learned about the SOLID principles for best code practices and everything seemed to be making sense to me in terms of how to apply it myself until I came across the Dependency Inversion Principle...

    I've already watched several unity and C# specific tutorials on it and I can't find anything that clearly shows how to apply it within unity.

    IMO the tutorials I keep coming across are vague overviews that don't go into detail about how and why it should be done in a specific way within unity.

    Personally if I'm going to learn something as complex as SOLID and applying it, I want to understand if I'm doing it correctly.

    So if anyone knows of any resources that have detailed explanations and examples for how to apply the Dependency Inversion Principle in unity it will be very much appreciated.

    Thanks.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    Whatever you do, don't use Zenject in Unity3D. Our team wasted YEARS on that dead end and it cost us a fortune in wasted engineering time. Complete disaster in a Unity context. You have been warned.

    The main thing to realize is that Unity is the app, not your game. People who try to fire up DI features on constructors and implicit dependencies immediately violate Unity's "everything must be done on the main thread" requirement. Constructors for MonoBehaviours are NEVER called by you and are NOT called on the main thread.
     
    icauroboros and SprayNpraY like this.
  3. SprayNpraY

    SprayNpraY

    Joined:
    Oct 2, 2014
    Posts:
    156
    Thank you very much, you always comment on my questions with very helpful responses...

    I was just looking into Zenject and I was about to import it into a demo project to try it out, and I had a feeling something may have been off with it. I'm very glad I asked for some advice before using it.

    What do you recommend that I do or look into now for applying DI/what does your team use now?
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    I recommend you make games!

    What problem are you trying to solve? Or do you just want to play with it? If you just want to play with DI, go nuts.

    We just got zero benefit from it, at a massive cost of hassle and ongoing penalties.
     
    SprayNpraY likes this.
  5. SprayNpraY

    SprayNpraY

    Joined:
    Oct 2, 2014
    Posts:
    156
    I'm quite inexperienced and I have seen C# devs talking about the importance of applying SOLID so I thought it must also be important with Unity so I watched some unity tutorials on it as well.

    So basically are you saying the D in SOLID isn't that important when it comes to unity game dev?

    I was just wanting to make sure when I start making games properly I'm starting from a foundation of applying good practices and principles.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    I have no position on buzzwords. Buzzwords come and go.

    I suggest you expose yourself to lots of different ways of doing things.

    Do things in lots of different ways.

    Be aware of which ways work best and which don't.

    Pay attention to what is costing you with each approach.

    Learn how to refactor so if you find yourself doing something sub-optimally, you can easily switch to a better path.
     
    Lurking-Ninja likes this.
  7. Premature optimization is root of many evils. This applies to your brain too. Just do things and make things, enjoy the process and learn along the way. You don't get too far if you start to be anxious if you are doing things "the right way".
    First and foremost: "The Right Way" is the way it produces a working game. Everything else is secondary. After that is achieved, you can start to think about how you could do better. (Retrospective) This step is important too. When you identified where were your mistakes, check up on them, what can be done better next time. Try to do it better next time. And the cycle goes on forever.
     
    Kurt-Dekker likes this.
  8. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,070
    SOLID is quite solid, it will stay for a while.
    It is more than 2 decades already.
     
    Edescal likes this.
  9. SprayNpraY

    SprayNpraY

    Joined:
    Oct 2, 2014
    Posts:
    156
    Thank you everyone for your responses I do have some concerns based on the advice given even though as I've mentioned with being inexperienced I'm open to everything I'm about to mention/ask being incorrect.

    I maybe inexperienced but its very experienced Unity and C#/.net developers that I have seen discuss SOLID principles so are you saying SOLID is just a buzz word for something programmers have been doing for a long time or do you mean something else?

    I can see where you're coming from but I've heard many experienced C# and unity C# developers talk about the number of hardships and wasted time they could have saved if they applied SOLID from the beginning.

    And how much time it saves when making changes to code in the future. I maybe inexperienced with C# and Unity but I think doing something effectively is a principle that is worth sticking to with anything in life, not just game dev so I'm personally not convinced I should just do what I find fun/enjoy.

    What if I make a working game but because of how poorly I coded the game when I release an update it's a nightmare to work around or to add new features, which is why I've heard people advise applying SOLID in the first place.

    I'm not anxious or discouraged I personally just want to do it effectively.
     
  10. You won't sell your first games. You may update them for fun. The worst case scenario is that it takes an hour more than it would have taken otherwise.
    Anyway, it's your journey, I don't want to convince you, just show that there is another route which is not that rigid at first, takes you to the same place and you may enjoy the ride more. Applying arbitrary coding principles isn't fun. It's work. At least for me. But I'm coding for too many years now. :D
     
    Kurt-Dekker and SprayNpraY like this.
  11. SprayNpraY

    SprayNpraY

    Joined:
    Oct 2, 2014
    Posts:
    156
    Ok thanks for the advice I'm taking what you've said into consideration.
     
  12. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,628
    My experience has been that if I don't understand how a design pattern works or how to use it, then that usually means that I've just never encountered the problem that it was designed to solve. Once you finally have that problem a few times, though, then it will seem obvious how it applies to your situation.
     
    koirat and Kurt-Dekker like this.
  13. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,912
    It's useful for when you don't want to rewrite a class when you change an implementation.

    Dependency on MainMenuUI script. If MainMenuUI is deleted/renamed, this code won't compile.
    Code (csharp):
    1. public class MainMenuController : MonoBehaviour
    2. {
    3.     // Changing it to a new UI script requires changing this line.
    4.     MainMenuUI view = new MainMenuUI();
    5.  
    6.     void Start()
    7.     {
    8.         // Specific code, unique to MainMenuUI
    9.         view.ButtonPlay.onClick += () => Debug.Log("Starting game");
    10.         view.ButtonPlay.onClick += () => Application.Quit();
    11.     }
    12.  
    13.     public void Play() => Debug.Log("Starting game");
    14.     public void Quit() => Application.Quit();
    15. }
    The following code is not dependent on MainMenuUI. It could be MainMenuUI, MainMenuVoiceControlledUI or MainMenuTouchGestureUI.
    Code (csharp):
    1. public interface IMainMenuUI
    2. {
    3.     event Action OnPlay;
    4.     event Action OnQuit;
    5. }
    6.  
    7. public class MainMenuController : MonoBehaviour
    8. {
    9.     IMainMenuUI view;
    10.  
    11.     // Constructor. Called from outside, so dependency can be passed in.
    12.     public void Init(IMainMenuUI mainMenuUI)
    13.     {
    14.         view = mainMenuUI;
    15.     }
    16.  
    17.     void Start()
    18.     {
    19.         view.OnPlay += Play();
    20.         view.OnQuit += Quit();
    21.     }
    22. }
    Now you can pass in anything that implements IMainMenuUI without having to change the MainMenuController:
    Code (csharp):
    1. mainMenuControllerReference.Init(new MainMenuVoiceControlledUI());
     
    Edescal and SprayNpraY like this.
  14. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,527
    I'm not going to get into the debate on if you should do this, I'm just going to go into how. For me in particular... I'll rely on dependency inversion principal in some scenarios, but not all, because at the end of the day I'm here to make a game now, not later. Things like dependency inversion principal rely heavily on the idea that the software is going to be maintained over long periods of time... think enterprise settings. Games can sometimes have this (especially these days with games that get released and then upgraded and have seasons and what not), but most games traditionally don't really benefit as much from it.

    So...

    Dependency Inversion Principle relies on the idea that you should not write your code to depend directly on a concretion, but rather to depend on an abstraction.

    What this means is create an abstracted layer between the concrete implementation and the code that depends on it.

    Visually...

    Don't do this:


    Do this:


    In a language like C# this is usually facilitated by an interface (but an abstract class could also be used).

    Take for example how collections all implement interfaces like IList and ICollection and etc. This way code that depends on collections can be coded in a manner to rely on these interfaces (abstractions) rather than be forced to use only the predefined concrete Array or List.

    ...

    So lets give a pretty simple example in Unity terms.

    And I'll go to the good old "interaction" scenario.

    A simple example could be something like:
    Code (csharp):
    1.  
    2. public class PlayerOpenDoorScript : MonoBehaviour
    3. {
    4.  
    5.     public float Radius; //distance from self we are allowed to interact
    6.    
    7.     private Collider[] _buffer = new Collider[16];
    8.  
    9.     void Update()
    10.     {
    11.         if (Input.GetButtonDown("Interact"))
    12.         {
    13.             var curpos = this.transform.position;
    14.             int count = Physics.OverlapSphereNonAlloc(curpos, this.Radius, _buffer, Constants.DOOR_LAYER_MASK, QueryTriggerInteraction.Collide);
    15.             float dist = float.PositiveInfinity;
    16.             Door nearest = null;
    17.             for (int i = 0; i < count; i++)
    18.             {
    19.                 var door = _buffer[i].GetComponent<Door>();
    20.                 if (door == null) continue;
    21.                
    22.                 float ds = (door.transform.position - curpos).sqrMagnitude;
    23.                 if (ds < dist)
    24.                 {
    25.                     nearest = door;
    26.                     dist = ds;
    27.                 }
    28.             }
    29.            
    30.             if (nearest != null)
    31.             {
    32.                 nearest.Open();
    33.             }
    34.         }
    35.     }
    36.  
    37. }
    38.  
    39. public class Door
    40. {
    41.  
    42.     public void Open()
    43.     {
    44.         //do whatever the heck we do for opening
    45.     }
    46.  
    47. }
    This code is the simplest. It's a very basic script that allows us to open doors by overlapping with its collider on some door layer.

    Downside is that it only really supports doors. Nothing else. The dependency is pretty darn strict.

    Arguably you could call it "Interactable" and then exploit UnityEvent. But still you're relying on this very explicit concrete type... so not dependency inversion. Also that heads down a completely different abstraction topic about using UnityEvent driven design.

    So lets start expanding it...

    Code (csharp):
    1. public class PlayerInteractController : MonoBehaviour
    2. {
    3.  
    4.     public float Radius; //distance from self we are allowed to interact
    5.    
    6.     private Collider[] _buffer = new Collider[16];
    7.  
    8.     void Update()
    9.     {
    10.         if (Input.GetButtonDown("Interact"))
    11.         {
    12.             var curpos = this.transform.position;
    13.             int count = Physics.OverlapSphereNonAlloc(curpos, this.Radius, _buffer, Constants.INTERACTABLE_LAYER_MASK, QueryTriggerInteraction.Collide);
    14.             float dist = float.PositiveInfinity;
    15.             GameObject nearest = null;
    16.             for (int i = 0; i < count; i++)
    17.             {
    18.                 float ds = (_buffer[i].transform.position - curpos).sqrMagnitude;
    19.                 if (ds < dist)
    20.                 {
    21.                     nearest = _buffer[i].gameObject;
    22.                     dist = ds;
    23.                 }
    24.             }
    25.            
    26.             if (nearest != null)
    27.             {
    28.                 nearest.SendMessage("InteractWith");
    29.             }
    30.         }
    31.     }
    32.  
    33. }
    This code is pretty simple and relies on a very subtle amount of abstraction. That minor amount of abstraction is that we don't actually care what is being interacted with we just send a message saying "InteractWith".

    Now anything that wants to be interacted with could just have a "InteractWith" method on one of its script. And it'll do a thing.

    Downside is that we're relying on this magic string that we just have to know about. But it's a good start.

    This is probably about as simple I could make of a dependency inverted situation in Unity... but it's relying on abstracting to the point where it's really just using dynamics. This could get really hard to debug... and doesn't really hold to other standards practices that are favored in a language like C#.

    So... lets try to be more distinct.

    Code (csharp):
    1. public interface IInteractable
    2. {
    3.    
    4.     float Priority { get; }
    5.     void Interact(PlayerInteractController actor);
    6.    
    7. }
    8.  
    9. public class PlayerInteractController : MonoBehaviour
    10. {
    11.  
    12.     public float Radius; //distance from self we are allowed to interact
    13.    
    14.     private Collider[] _buffer = new Collider[16];
    15.  
    16.     void Update()
    17.     {
    18.         if (Input.GetButtonDown("Interact"))
    19.         {
    20.             var curpos = this.transform.position;
    21.             int count = Physics.OverlapSphereNonAlloc(curpos, this.Radius, _buffer, Constants.INTERACTABLE_LAYER_MASK, QueryTriggerInteraction.Collide);
    22.            
    23.             IInteractable bestmatch = (from c in _buffer.Take(count)
    24.                                        let ic = c.GetComponent<IInteractable>()
    25.                                        where ic != null
    26.                                        orderby (c.transform.position - curpos).sqrMagnitude descending
    27.                                        orderby ic.Priority descending
    28.                                        select ic).FirstOrDefault();
    29.  
    30.             if (bestmatch != null)
    31.             {
    32.                 bestmatch.Interact(this);
    33.             }
    34.         }
    35.     }
    36.  
    37. }
    Now we've done a similar situation but now we've abstracted it into an interface to interact with. This has also allowed us to add some extra properties to the interactable like priority. I've also added the ability to pass along who interacted with it (this could have been done in SendMessage... but in a loser manner).

    So now we've decoupled PlayerInteractController from what its interacting. You could implement IInteractable in many ways. From doors, to chests, to whatever the heck you want. If you want a new thing to interact with... just implement and add it to the scene.

    But we could abstract more. Currently this implies that ONLY the player can interact with things. What if we wanted mobs to interact with things? Mobs don't use the Input system or anything. So what then?

    Code (csharp):
    1. public interface IInteractable
    2. {
    3.    
    4.     float Priority { get; }
    5.     void Interact(IInteractionBehaviour actor);
    6.    
    7. }
    8.  
    9. public interface IInteractionBehaviour
    10. {
    11.    
    12.     void AttemptInteraction();
    13.    
    14. }
    15.  
    16. public class PlayerInteractionBehaviour : MonoBehaviour
    17. {
    18.  
    19.     public float Radius; //distance from self we are allowed to interact
    20.    
    21.     private Collider[] _buffer = new Collider[16];
    22.  
    23.     void Update()
    24.     {
    25.         if (Input.GetButtonDown("Interact"))
    26.         {
    27.             this.AttemptInteraction();
    28.         }
    29.     }
    30.  
    31.     public void AttemptInteraction()
    32.     {
    33.         var curpos = this.transform.position;
    34.         int count = Physics.OverlapSphereNonAlloc(curpos, this.Radius, _buffer, Constants.INTERACTABLE_LAYER_MASK, QueryTriggerInteraction.Collide);
    35.  
    36.         IInteractable bestmatch = (from c in _buffer.Take(count)
    37.                                    let ic = c.GetComponent<IInteractable>()
    38.                                    where ic != null
    39.                                    orderby (c.transform.position - curpos).sqrMagnitude descending
    40.                                    orderby ic.Priority descending
    41.                                    select ic).FirstOrDefault();
    42.  
    43.         if (bestmatch != null)
    44.         {
    45.             bestmatch.Interact(this);
    46.         }
    47.     }
    48.    
    49. }
    50.  
    51. public class MobInteractionBehaviour : MonoBehaviour
    52. {
    53.  
    54.     public void AttemptInteraction()
    55.     {
    56.         IInteractable bestmatch = * WHO KNOWS, MAYBE MOB FINDS TARGETS A DIFFERENT WAY THAN PLAYER??? *;
    57.  
    58.         if (bestmatch != null)
    59.         {
    60.             bestmatch.Interact(this);
    61.         }
    62.     }
    63.    
    64. }
    The abstraction can keep going of course.

    Code (csharp):
    1. public class Entity : MonoBehaviour
    2. {
    3.     //other info that defines an entity
    4. }
    5.  
    6. public interface IInteractable
    7. {
    8.    
    9.     float Priority { get; }
    10.     void Interact(IEntity actor);
    11.    
    12. }
    13.  
    14. public abstract class InteractionBehaviour : MonoBehaviour
    15. {
    16.    
    17.     void AttemptInteraction(IEntity entity);
    18.    
    19. }
    20.  
    21. public class AttemptInteractOnPlayerInput : MonoBehaviour
    22. {
    23.  
    24.     public Entity entity;;
    25.     public InteractionBehaviour interactionBehaviour;
    26.  
    27.     void Update()
    28.     {
    29.         if (Input.GetButtonDown("Interact"))
    30.         {
    31.             interactionBehaviour.AttemptInteraction(entity);
    32.         }
    33.     }
    34.  
    35. }
    36.  
    37. public class MobAI : MonoBehaviour
    38. {
    39.  
    40.     public Entity entity;
    41.     public InteractionBehaviour interactionBehaviour;
    42.    
    43.     void MobAIRoutine()
    44.     {
    45.         //implement AI however you want...
    46.         //reach line where we do interaction
    47.         interactionBehaviour.AttemptInteraction(entity);
    48.     }
    49.  
    50. }
    51.  
    52. public class NearestTargetInteraction : InteractionBehaviour
    53. {
    54.  
    55.     public float Radius; //distance from self we are allowed to interact
    56.    
    57.     private Collider[] _buffer = new Collider[16];
    58.  
    59.     public override void AttemptInteraction(IEntity entity)
    60.     {
    61.         var curpos = this.transform.position;
    62.         int count = Physics.OverlapSphereNonAlloc(curpos, this.Radius, _buffer, Constants.INTERACTABLE_LAYER_MASK, QueryTriggerInteraction.Collide);
    63.  
    64.         IInteractable bestmatch = (from c in _buffer.Take(count)
    65.                                    let ic = c.GetComponent<IInteractable>()
    66.                                    where ic != null
    67.                                    orderby (c.transform.position - curpos).sqrMagnitude descending
    68.                                    orderby ic.Priority descending
    69.                                    select ic).FirstOrDefault();
    70.  
    71.         if (bestmatch != null)
    72.         {
    73.             bestmatch.Interact(entity);
    74.         }
    75.     }
    76.    
    77. }
    You can abstract to your hearts content.

    ...

    But here in lies the issue.

    When do you stop abstracting?

    What's the final layer?

    Are we forever engineering a solution and never making a game????
     
  15. SprayNpraY

    SprayNpraY

    Joined:
    Oct 2, 2014
    Posts:
    156
    Thank you I've found your example very useful.

    Thank you so much for the detailed explanation and example it makes much more sense to me now why and how it can be used in the context of unity game dev.
    I guess knowing how much to add abstractions and new layers will come through experience and good planning?

    Do you know of anywhere such as a book or videos that goes through what you have shown in your example code in more detail and broken down, it would just make it easier for me personally to grasp and for it to sink in.
     
  16. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,527
    Experience is key.

    For me it comes down to planning the game design first.

    Let's take this interactable design I went through. Say I'm making an adventure game. I know how many interactors I'd likely have.

    1) the player for certain
    2) maybe mobs?
    2.5) maybe maybe NPCs, and really NPCs would likely be using the same logic as mobs?

    The idea of new interactors getting added is unlikely. So in the end I have 2 concrete "interactor" scripts here.

    As for interactables though... I have no idea. I can think of several off the top of my head. But I could perceive my design having many many more coming up later on.

    So... I probably wouldn't abstract the interactor, but I would abstract the interactable. And I would definitely do the Entity thing as I do that in all my projects.

    Mainly because odds are I'll only ever have 2 interactor scripts. No need to spend any effort writing all whole bunch of abstraction around that. Why do it? In case I add another interactor in the future? A whole 3? It's doubtful any dependencies issues are going to arise there.

    But the interactables on the other hand... I could have many of them. I can't foresee how many I might end up having. So yeah... lets keep that abstracted.

    I've been at programming for a very long time at this point. Heck I've been in Unity for 11+ years, let alone the 20+ years I've been programming (it's hard to say when I really started... since before 2006 I was more a "script kiddy" dicking around in javascript/vba/etc).

    What I mean is that any sources I've used myself came from a time period that predates video tutorials, and the books are so out of date that they likely aren't useful. And none of it is specific to Unity, I built my Unity knowledge by just working with it.

    So... sorry, I don't have any resources like that on hand.

    Personally... I think the forums here are the best resource.

    It's how I learned most of my skills. Been a forum rat since I can remember. I like to use people's questions as an opportunity to challenge myself. As well as to read other people's solutions as a way to familiarize myself with new concepts.
     
    DrZwieback and SprayNpraY like this.