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. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question Singletons & Service Locators & Dependency Injections, Oh My!

Discussion in 'Scripting' started by Glacier-Games, Mar 17, 2023.

  1. Glacier-Games

    Glacier-Games

    Joined:
    Jan 26, 2013
    Posts:
    12
    I got looped in. Down the forsaken rabbit hole of design patterns. I've read so many forums and articles about the pros and cons, the whys and why nots, the when to's and when not to's... and I'm stuck. If anyone has guidance, please show me the way. I want to preface by saying that I am genuinely curious and looking for guidance, I don't mean to imply that any one thing is useless/bad; I really do want to learn.

    Ever since I've gotten into programming in Unity, I've just always used singletons. They made sense, and made life a million times easier. How else was I supposed to keep hubs for the functionality and variables that needed to be accessed from multiple entry points across my game? I got into game design through modding, and noticed even AAA games use singletons (or so it seems). But now, I've used them so much in my project that I'm scared that I'm making a terrible mistake.

    My first try was DI, and looked at the documentation for existing frameworks and immediately thought to myself that they were overcomplicated. So, I built my own simple attribute-based approach where I called [Inject] before each dependency variable I needed per dependent class. It was a headache to go into all of my scripts and inject the things I needed. But then I thought... why? "Why don't I just use a singleton or call GetComponent/FindObjectOfType/etc? My life was so much easier before..." I see and understand the benefits for normal C# projects, but they feel like overkill for how Unity is structured as a whole.

    Then I remembered hearing about Service Locators, which are apparently frowned upon by DI enthusiasts; but at least they're better than Singletons. So I implemented my own SL framework where I call Services.Get<IService>(). And again, I thought why? Being an indie dev, why would I ever need to unit test. If I want to make a potential change to IService, I'll just make the change to it. Aside from unit testing, I get that I can sort of decouple the dependency from the dependent with Service Locators, but why would I need to do that in an indie game? These all seem like concepts that would be helpful for massive software teams where Bob can accidentally push a destructive mechanic into main, or Joe wants to test a new feature without ruining everyone else's builds, but where do I fit in?

    In the end, I've ended up using a hybrid of a Service Locator for services, and a rudimentary DI implementation to replace most GetComponent calls. Maybe I'm doing it all wrong, and maybe I have no idea what I'm talking about. I'm just scared that I'm digging myself into a hole I can never get out of, but maybe I've already done that by even stepping into this "rabbit-hole" (buh dum tss) (funny drum noise).
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,954
    Steps to success:

    1. understand and clearly express the intended lifecycle (duration) of each of your objects

    (NOTE! You absolutely may not move past step #1 until it is clearly defined. If that lifecycle changes at any point, you MUST return to step #1 above, or you risk muddling what you've done.)

    2. implement the simplest appropriate solution for those lifecycles

    3. return to making a game and stop agonizing about silly CIS-100 stuff.

    That's it. Be sure to make your decisions in this context:

    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

    Here are the patterns I always use, and I never put managers in any scene, EVER:

    ULTRA-simple static solution to a GameManager:

    https://forum.unity.com/threads/i-need-to-save-the-score-when-the-scene-resets.1168766/#post-7488068

    https://gist.github.com/kurtdekker/50faa0d78cd978375b2fe465d55b282b

    OR for a more-complex "lives as a MonoBehaviour or ScriptableObject" solution...

    Simple Singleton (UnitySingleton):

    Some super-simple Singleton examples to take and modify:

    Simple Unity3D Singleton (no predefined data):

    https://gist.github.com/kurtdekker/775bb97614047072f7004d6fb9ccce30

    Unity3D Singleton with a Prefab (or a ScriptableObject) used for predefined data:

    https://gist.github.com/kurtdekker/2f07be6f6a844cf82110fc42a774a625

    These are pure-code solutions, DO NOT put anything into any scene, just access it via .Instance!

    The above solutions can be modified to additively load a scene instead, BUT scenes do not load until end of frame, which means your static factory cannot return the instance that will be in the to-be-loaded scene. This is a minor limitation that is simple to work around.

    If it is a GameManager, when the game is over, make a function in that singleton that Destroys itself so the next time you access it you get a fresh one, something like:

    Code (csharp):
    1. public void DestroyThyself()
    2. {
    3.    Destroy(gameObject);
    4.    Instance = null;    // because destroy doesn't happen until end of frame
    5. }
    There are also lots of Youtube tutorials on the concepts involved in making a suitable GameManager, which obviously depends a lot on what your game might need.

    OR just make a custom ScriptableObject that has the shared fields you want for the duration of many scenes, and drag references to that one ScriptableObject instance into everything that needs it. It scales up to a certain point.

    If you really insist on a barebones C# singleton, here's a highlander (there can only be one):

    https://gist.github.com/kurtdekker/b860fe6734583f8dc70eec475b1e7163

    And finally there's always just a simple "static locator" pattern you can use on MonoBehaviour-derived classes, just to give global access to them during their lifecycle.

    WARNING: this does NOT control their uniqueness.

    WARNING: this does NOT control their lifecycle.

    Code (csharp):
    1. public static MyClass Instance { get; private set; }
    2.  
    3. void OnEnable()
    4. {
    5.   Instance = this;
    6. }
    7. void OnDisable()
    8. {
    9.   Instance = null;     // keep everybody honest when we're not around
    10. }
    Anyone can get at it via
    MyClass.Instance.
    , but only while it exists.
     
    Last edited: Mar 17, 2023
    Ryiah, Glacier-Games and Deleted User like this.
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,384
    Here's my feel... and it's sort of a goldilocks type situation for me:

    Singleton

    Quick and dirty gets the job done. Is it scalable? No. Do you need scalability? That's up to you.

    Having a handful of Singletons IMO is no big deal. If you've got 10's of them though... you're entering trouble territory if the project expects to have longevity.

    But to make a point... Unity is FULL of singletons from Random (heck it's barely even a singleton, it's just a static state machine. At least singletons have object identity.) to EventSystem to so much more.

    DI

    I legitimately can't stand the concept of DI in a component based game development scenario.

    I am of the opinion that a bunch of web developers showed up on game devs door step and said "HEY... why aren't you designing your games like web services!????"

    See, I'm from the enterprise world, and it's not like I'm completely unfamiliar with DI.

    It's just that in the enterprise world you end up writing software that needs to be maintained for years, decades even. It might be managed by extremely large teams where communication can fall apart easily and documentation becomes a huge overhead on development time/cost. And DI offers up various tools, that once all the devs learn to speak its language, can ease a lot of these long term problems.

    And assist test driven development at the same time.

    And hey, there are games out there with massive teams that need to be maintained for years upon years if not decades. But is that your game?

    BUT...

    DI is also, as oft been stated for years on the internet, is a 25 dollar word for a 5 cent idea:
    http://www.jamesshore.com/v2/blog/2006/dependency-injection-demystified

    And here's the thing. Look at your editor screen, where you add components, and configure values on the fly.

    THAT'S DEPEDENCY INJECTION!

    It's not done the same exact way as many of the popular DI frameworks like Zenject or something. But at the end of the day... it's DI. Because DI is just the act of giving an object the references it needs rather than it resolving them itself.

    By upping the DI game to things like Zenject you run into all new problems.

    For example I just did a short contract developing some tools for a game for another company who uses Zenject. And they were very happy by how they leveraged Zenject for a lot of their game.

    I finished up my work and I went to merge it into their project and Zenject became a slight nuisance for a couple hours of that insertion. Since they basically were using Zenject for effectively EVERYTHING in the game from services (which I have a rant about DI hating service locators, but often effectively having a service locator built in), to configurations, to literally everything.

    And you end up with this situation where you don't know which services/configurations/etc are available when. Cause like in Zenject you have what are called "Installers" which is code that initializes the bindings in the "DiContainer" (the service locator people... it's a service locator at the end of the day. A powerful one for certain, but one none the less).

    Thing is you can have installers that run at start of the project, and others that run at start of specific scenes, and unload at other times. And since you can also bind to magical string ids it becomes a rats nest to figure out where data is coming from. I'll look in one script and see that some array of configs objects to get the prefabs for the ground tiles is injected by some string ID "Tiles". And now I'm off on a wild goose chase to figure out where this thing is bound and low and behold it's bound on this specific scene's entry point and so technically it's not available where they expect this new tool to be used.

    And none of this is documented. And even when asked of the team everyone sort of just stares at each other like "uhhhhh???? I'll get back to you on that."

    ...

    Here's what I do though.

    I create a ScriptableObject called "TileConfiguration", fill it with my data, and... drag and drop it on the script in my scene.

    Here's the wonderful part about this...

    1) When I go to that script in that scene, there is a visual editor that shows that it is both configured and WHAT it is configured with it. I can click on it even and it'll ping the configuration in my assets folder.

    2) I know that it's contextually only available in that scene... because it's attached to a script in that scene!

    3) If it's not attached... I know it's NOT attached.

    4) I still get to leverage the power of different configurations in different scenes (which is really all the scene installers are really offering).

    Now of course one might argue "But if I have 10 different scripts that need that 1 configuration... this becomes cumbersome as I have to drag and drop it on every single script in the scene that needs it!"

    And sure...

    But the thing is, in the example I described, it was used in ONE LOCATION.

    They weren't using the DI to leverage the power of the fact you bind once in the installer and inject many at point of need.

    They used DI because... well... they use DI. How else would you do it??? We have a DI framework!

    And this isn't necessarily some major fault. I look at this the same way as I might look at traffic... if you give a driver a big wide straight road, don't be surprised they drive 100mph down that road!

    It's a problem that Singleton also has, and service locator, you give someone a tool that feels so powerful... and people will use it even when it's not necessary.

    And when that tool is full of arcane magical buzz words that most people don't actually fully understand the meaning of... it's hart to not realize your Biz-Baz-Murango-Whoozit is actually .... just a hammer.

    DI is just... data passed to an object at time of construction.

    It's a 25$ word for a 5c concept.

    And that's my biggest beef with DI. It becomes a similar trap as Singletons. Everyone hates on Singletons because raisins. And when trying to devise an answer to the problem they end up just repeating the same fundamental problem over again.

    Singletons are bad because it promotes rigid code that is difficult to test/debug. And as you attempt to scale that out with more and more singletons that problem gets worse and worse.

    But DI has the same problem... if you start smashing everything with the DI hammer... it gets harder and harder to debug. Sure it promotes decoupling which is so much better than the "globals" boogie man. But sometimes that decoupling gets so out of hand you don't know where the data is even coming from! Or if the data is even available!

    Service Locator

    So this is my medium hot porridge, my semi-soft/semi-firm mattress, my goldilocks.

    It has all the same flaws that both DI and Singleton has!

    Just not as bad.

    Unlike Singleton I can use interfaces for my services and at point of initializing my service locator inject my own specific implementation. This facilitates test driven coding as I polymorphically can implement both production and test services.

    It's also great because I can write modular code... because say I have tons of tools that rely on the services. For example I can have an ISceneManager and then a "LoadScene" script that relies on the service ISceneManager. But I can have a "StandardSceneManager", a "AddressableSceneManager", and a "NetworkSceneManager" and now that "LoadScene" script is reusable in multiple projects regardless of what sort of scene management I need to do for that project. I just initialize my service locator with the necessary scene manager for my project, and all the scripts out there that rely on ISceneManager just work.

    I could even implement my own 'Inject' attribute, just like you did, and "inject" my services during awake/construction of the intended objects. Giving me a very rudimentary DI framework where the DIContainer consists primarily of only services.

    And this is where I feel this is better than a DI framework.

    This simplicity of the system forces me to not bend the system beyond its usefulness.

    I'm not shoving every piece of configuration into my DIContainer. I have to take a more reserved look at what I put into it as it requires me to do the boilerplate for initializing the service locator. It doesn't support arbitrary injections like "Tiles" which is just an array of things.

    I don't end up with 100+ bound data contracts.

    I catch myself thinking "Well, I need a configuration for how this room generator is done. How should I accomplish that? Oh... I know... I'll use a ScriptableObject to create an Asset which can be configured on my RoomGenerator script in the prefab!"

    Because the idea of a "configuration" being a "service" is kind of weird. It's not a service, it's a configuration!

    I might even have services that just offer up configurations (as well as manage those things). Because yes, having a global configuration is convenient! Cause guess what guys... when you've "bound" some configuration stuff in your DiContainer... that's a global! You know, the boogie man that makes Singletons oh so bad!

    Globals aren't necessarily bad/dangerous. Using globals when you don't understand what's dangerous about globals is bad. Guns are dangerous... but they sure are helpful when I need to shoot my dinner!

    Anyways, For example:
    upload_2023-3-17_16-59-56.png

    Here are most of the services for a previously released game of mine.

    This service GameObject with various services on it is loaded at start of my game. Basically I just have a "StartupScript" in my boot scene that loads this gameobject up and all the services get registered in the service locator.

    I also have a 'DebugStartup' that does a similar thing, but only does so if the game is in the editor and hasn't already been initialized. This way you don't HAVE to start the game through the boot scene while developing the game and still have it work.

    And if you want to test it... you just have a "Test" service prefab with your test configuration on it.

    And now if I need to check on my configuration I have a nice easy to use editor to do so. The none programmer on my team can easily go check it out and tweak stuff.

    Sure... it's not as powerful as a DI framework... but does it really have to be?

    I used to drive tractor trailer. Haul 80,000lbs of cars across the country. The gear box of trucks have 16-24 gears on them easily.

    Is it powerful?

    Oh hell yeah... I can haul 80,000lb!!!!

    Would I drive it around town?

    NO!

    ...

    TLDR;

    Singletons are a quick and dirty, simple, and powerful way to set things up as anyone with half a programming brain can figure out. Just don't abuse them.

    DI frameworks are an extremely powerful beast that can pull off some really awesome things. It's like a tractor trailer, or a freight train, in its power to accomplish infrastructurally large tasks. But sucks at driving across town.

    Service Locator is equally as dangerous as Singletons and DI... but it brings some of the power of DI to Singletons, with out being the huge wild to control beast that DI frameworks can be.

    Pick your poison for your project... or go with a completely different one. Anyone who tells you any of them are necessarily bad and shouldn't exist what so ever is lying to you.
     
    Last edited: Mar 17, 2023
    ttesla, Briezar, cy_unity_dev and 5 others like this.
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,954
    I love this setup. ScriptableObjects all the way.

    BUT! The only place I see the above completely fail: Addressables / AssetBundles (I'll call it DLC for simplicity)

    To state it explicitly if it isn't obvious:

    - Your base game ships with an instance of that SO.
    - Each DLC asset would have its OWN instance of that SO, frozen in time when you created the DLC.

    When loaded those are two different instances, at least last time I tried.

    What are some slick solutions to this?

    The only thing I can think of is some asset instantiation shim that goes "Oh when you go make one of these, check if one already exists and return that instead," basically at the global class object level that would work based on Unity's initialization framework (eg, custom constructors are not a solution).
     
    Ryiah and lordofduct like this.
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,384
    Yeah, this problem still exists, and is super annoying!

    It actually extends to pretty much everything from prefabs to meshes and all. Your addressable/asset bundle is its own distinct asset from the ones in the main build.

    I have a few ways to deal with this.

    1) any asset that needs perfect object identity gets put into its own assetbundle/addressable group. There is no cross referencing between groups unless done so via an AssetReference.

    2) for assets that don't require perfect object identity don't matter. If it's just a "configuration" object all that really matters is that the configuration matches. And as long as the bundles were built at the same time as the game that will be true.

    3) for assets that HAVE to be directly referenced in more than one bundle/group AND have object identity. Force the object identity.

    For example I have a type we call "ProxyMediator". Basically a "Proxy" in our project is something that can reference a target without being the target itself. A ProxyMediator is a proxy that behaves as an event while also being that reference.

    This came into existence years ago when we wanted ways to send messages between prefabs configurable through the editor. And we could store data in the message (basically ScriptableObject events which are very popular these days).

    But a couple projects ago we started leveraging Addressables and ran into this problem. Actually... if I recall correctly I asked about it here and you and I specifically discussed it and we realized this was the root of the cause. That the SOs in the addressable groups were distinct objects from one another.

    SO how I resolved it was I created a "SerializableGuid" struct (it's just System.Guid that is supported by the unity serializer). And I wrote an editor for it that allows me to set the guid to the asset id in the meta file associated with the asset (which is unique per asset).

    Then... on load of any of these ProxyMediators I create a lookup table that links PM's of the same id together. And I also override its equality comparer so that if you compare them to one another they resolve as equal. So even though they're distinct objects, they behave as the same object.

    https://github.com/lordofduct/space...cepuppy.triggers/Runtime/src/ProxyMediator.cs
     
    Kurt-Dekker likes this.
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,954
    I think you're correct! That does sound familiar now.

    I think if I had to do that again, next time I would just do my classic singleton with databacking pattern.

    And those loaders load by Resources.Load<T>() so they only ever load the correct one.

    I mean truly, what does it even get you to drag the same stupid
    MyContext
    ScriptableObject all over the place? Yeah yeah unit testing, blah blah but you can do that anyway with cleverer selector on load.

    Dragging the same exact SO instance onto 2700 different things in your project is a complete waste of everybody's time. There, I said it.