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
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice
  4. Dismiss Notice

Replacing singletons with static events/actions

Discussion in 'Scripting' started by kailin89, Jun 20, 2020.

  1. kailin89

    kailin89

    Joined:
    Dec 16, 2019
    Posts:
    13
    I've set myself a goal of making a project without the use of any singletons. One solution I've started using is that instead of one script getting a reference to another through a singleton, it just registers for the events that it needs through static events and actions. So an example would be our PlayerHealth component, which a bunch of other interested observers (UI, sound etc) would register for - would look something like this:

    Code (CSharp):
    1. public class HealthManager : MonoBehaviour
    2. {
    3. private int health;
    4. public static Action<int> HealthChangedAction;
    5.  
    6. public void ModifyHealth(int amount)
    7. {
    8. health += amount;
    9. HealthChangedAction?.Invoke(health);
    10. }
    11. }
    And the health bar UI for example would register for it like so:

    Code (CSharp):
    1. public class HealthBar : MonoBehaviour
    2. {
    3.  
    4. void OnEnable()
    5. {
    6. HealthManager.HealthChangedEvent += HandleHealthChange
    7. }
    8.  
    9. void OnDisable()
    10. {
    11. HealthManager.HealthChangedEvent -= HandleHealthChange
    12. }
    13.  
    14. void HandleHealthChange(int health)
    15. {
    16. //change health display
    17. }
    18.  
    19. }
    It all works good, and everything is nicely decoupled, can easily remove/add components without breaking any parts of the code. But... have I just replaced one alleged turd (the singleton) with another differently shaped one?

    What do you lot think? I got the idea for this from this tutorial -
     
  2. WarmedxMints

    WarmedxMints

    Joined:
    Feb 6, 2017
    Posts:
    1,035
    I prefer to use scriptable objects myself.

    For example, I have a player manager scriptable object which is in my resources folder. The player loads it from resources on start and reports its current health and any changes. Then the UI also loads the scriptable object from resources and listens to any health changes which I usually do via a unity event on the scriptable object.

    What I like about this is I can stick my player into a scene and it doesn't care if there is no UI and the same goes for the UI.

    Here's a good unite talk on the structure I like to use
     
    kailin89 likes this.
  3. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Not really. It may appear like that at this stage of development, but there's still a coupling of the listening component to the event source.
    For simple cases, you can go with that solution and don't bother anymore. If you need to extend the system later on, you will run into trouble for sure.

    Generally speaking: yes. There's surely the one or other situation in which you could quickly implement a small feature for which it doesn't matter. However, you shouldn't integrate such things into complex parts of your code, because it'll cause problems later.

    Take that health component which - apparently - tracks health for an object,other objects in your scene. Suppose you want to re-use it for other objects in your scene. The event would be raised for all listeners, and in order to distinguish the one source from another, your listener would need to decide whether or not the event source (the "sender") is the one it wanted to listen to.

    You can just get around both implementations and their issues by injecting an instance of the health component. That wouldn't require a singleton (actually it could be one, but the listener wouldn't need to know, as resolving the instance is no longer its task), nor a static event. As an alternative, you can set the instance using a simple setter and an orchestrating component which handles the setup, i.e. which has the sole responsibility of organizing/wiring components.

    The serialized field is probably still the easiest solution for your problem, though.
     
    kailin89 and lordofduct like this.
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    Question?

    Why did you forego using singletons? What was your reason?

    Does static's fix that reason? Does it accomplish avoiding the thing that made you want to leave singletons?

    ...

    The biggest problem with Singletons out there is that it is a global single state, and it's hard to extend a global single state without refactoring. Well... statics are ALSO a global single state, so you've replaced one for the same thing.

    Another reason, as was mentioned, is the coupling of code. Which statics have the same problem.

    Honestly a singleton is like a static with an added benefit... that it has object identity and can be passed by reference (you can consume the 'instance' static field). Statics don't have this. So really your static states are a more gimped version of the singletons you're replacing.

    ...

    Thing is sometimes you just need some easy to access global point of entry for something.

    I personally like a "service provider" approach.

    Basically you'll have a single static/singleton object somewhere. This could just be the 'ServiceProvider' itself, or maybe a 'GameManager' which has a 'ServiceProvider' as its member.

    Then on start of the game you register your various necessary globals to this 'ServiceProvider'. Nice thing here though is you could register them as an 'interface' which decouples dependencies. It means if you want to swap out the type of services, you can, and all code that accesses the 'ServiceProvider' won't care about the difference.

    For example, here's where I register my services:
    Code (csharp):
    1.  
    2.             //################
    3.             // Initialize Services - this should always happen first, any given service shouldn't rely on accessing other services on creation
    4.             //########
    5.             {
    6.                 //create services
    7.                 var sceneManager = Services.Create<ISceneManager, SPSceneManager>(true);
    8.                 sceneManager.SceneLoaded += this.OnSceneLoaded;
    9.  
    10.                 var inputManager = Services.Create<IInputManager, GameInputManager>(true);
    11.                 inputManager.Add(MAIN_INPUT, InputFactory.CreateInputDevice(MAIN_INPUT));
    12.  
    13.                 Services.Create<ICameraManager, CameraManager>(true);
    14.                 Services.Create<ScenarioPersistentData>(true);
    15.                 Services.Create<com.spacepuppy.Audio.IAudioManager, com.spacepuppy.Audio.AudioManager>(true);
    16.  
    Notice how they're implemented as:
    ISceneManager
    IInputManager
    ICameraManager

    When I access them, they're accessed via said interface. Like here is me getting my "input device":
    Code (csharp):
    1.  
    2.         protected void Update()
    3.         {
    4.             if (_entity.GetHealthStatus() == HealthStatus.Dead)
    5.                 return;
    6.  
    7.             if (_idleActionAnim != null && !_idleActionAnim.IsPlaying) _idleActionAnim = null;
    8.          
    9.             var input = Services.Get<IInputManager>().GetDevice<MansionInputDevice>(Game.MAIN_INPUT);
    10.             if (input == null) return;
    11.  
    12.             if (_entity.Type == IEntity.EntityType.UndeadPlayer)
    13.             {
    14.                 if (Game.Scenario.Paused)
    15.                 {
    16.                     Game.Scenario.GameStateStack.Purge(GameState.Paused);
    17.                 }
    18.             }
    19.             else if (!Game.Scenario.SceneStalled && input.GetButtonState(MansionInputs.Pause) == ButtonState.Down && !Game.Scenario.Paused)
    20.             {
    21.                 var pauseMenu = Services.Get<IPauseDisplay>();
    22.                 if (pauseMenu != null) pauseMenu.Show();
    23.             }
    24.             else if (!_entity.Stalled && input.GetButtonState(MansionInputs.Inventory) == ButtonState.Down)
    25.             {
    26.                 var inventory = Services.Get<IInventoryDisplay>();
    27.                 if (inventory != null) inventory.Show(InventoryDisplayMode.Standard);
    28.             }
    29.  
    Since I access it via 'IInputManager'... it means I could have any input manager in the service that implements that interface.

    So if say I was doing a unit test, I could initialize with a custom "UnitTestInputManager" that returned states needed for the unit test for testing (say I wanted to test a determinate input sequence without having to pull out the gamepad and do it manually).

    Or lets say I released for a platform that didn't support normal input devices. I needed write a whole new input system that did touch screen... or VR... or whatever. I could just implement that special "***InputManager" and pass it in at startup. And all my other code continues working. They just have to return an adequately implement "MansionInputDevice" (which just holds the current states) that return the appropriate values for the same calls to things like 'GetButtonState'. Sure the method is called 'GetButtonState', but it's underlying state could easily just be determined in the new ****InputManager via the touchscreen, or a VR gesture, or whatever.
     
    Last edited: Jun 20, 2020
    bobbaluba, mr_president and kailin89 like this.
  5. kailin89

    kailin89

    Joined:
    Dec 16, 2019
    Posts:
    13
    Thanks for your resonses, I am feeling like it was not the best decsion to use statics instead of singletons - just wanted to try something different but it seems essentially the same approach.

    That's an intersting method @lordofduct -

    So if applying to my original example, I would have a ServiceProvider class hold a reference to something that implements an IHealth interface, then have other interested parties register with the IHealth component via the service provider?

    How is your ServiceProvider creating and storing the instances - I guess Services.Create<> will create the instance, then does it store each service by type in a dicationary or something? Should all the services also need to implement some kind of IService interface so that they can be created?
     
  6. kailin89

    kailin89

    Joined:
    Dec 16, 2019
    Posts:
    13
    One reason for using statics was that I have many enemies you can shoot, and I wanted them all to call an action when shot. So for example :

    Code (CSharp):
    1. public class Enemy : MonoBehaviour {
    2.  
    3. public int DamageDone {get; private set;}
    4.  
    5. public static Action <Enemy> WasShotEvent;
    6.  
    7. public virtual void WasShot() {
    8. WasShotEvent?.Invoke(this);
    9. }
    10.  
    11. }
    then I could inherit from Enemy and everything registered for WasShotEvent would get alerted whenever an enemy was shot, and would handle it depending on the enemy passed in (using its DamageDone variable for instance).