Why can I not subscribe to an event of a singleton?

I have multiple objects containing holding a script with class ObjBehaviour trying to subscribe to an event of a singleton of a class SpecialMove.

I start my SpecialMove class with

public class SpecialMove : MonoBehaviour
{

    public static SpecialMove instance;

    private void Awake()
    {
        if(instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(gameObject);
        }
    }
    public event Action SpecialStart;
    public event Action SpecialEnd;
}

Further down I insert “SpecialStart?.Invoke()” and do the same for the SpeciaEnd event.

In my class ObjBehaviour at the beginning I write:

   private void OnEnable()
    {
        SpecialMove.instance.SpecialStart += OnSpecialStart;
        SpecialMove.instance.SpecialEnd += OnSpecialEnd;

    }

    private void OnDisable()
    {
        SpecialMove.instance.SpecialStart -= OnSpecialStart;
        SpecialMove.instance.SpecialEnd -= OnSpecialEnd;
    }

and for some reason it throws me a Nullreference error for each object containing the ObjBehaviour class, specifically for the subscription and desubscription of OnSpecialStart.

The weird part is: If I declare a “public SpecialMove spMove” and then insert the SpecialMove script accordingly in the inspector, and subscribe/desubscribe via “spMove.SpecialStart += OnSpecialStart” I do NOT get an error. Also, I have another class (which is also a singleton), lets call it “class C” subscribe/unsubscribe to the events by calling “SpecialMove.instance.SpecialStart += OnSpecialStart”, and in that case I do not get the error.

What could be the reason for this? Other than that the class ObjBehavior is a component of multiple different gameobjects, there is no difference to the event subscription compared to class C, where it does work.

where do you instantiate/define OnSpecialStart/OnSpecialEnd ?
it has to be a method, not just an Action declaration.

It’s a singleton, so make the event declaration static?

1 Like

no need for it to be static. that’s not how you use a singleton.

@orionsyndrome I declare it further down as

private void OnSpecialStart(){
inSpecial = true
}

,
it simply changes the value of a boolean variable.

Are you sure you are invoking Awake only once ?

And one more thing, may I ask what are you trying to achieve with this static MonoBehaviour.
I’m sensing some serious code smell in here.

Im away from my pc for the good part of the day, but will check whether I have declared the methods and Awake function etc correctly later tonight.

Regarding the purpose of the static MonoBehaviour:
The SpecialMove class checks certain button inputs and conditions and starts a “special move” (particle effects, time slowmotion, etc) if all the requirements are met. Im creating the two event actions SpecialStart and SpecialEnd so that other scripts can be notified when the special move is active and how long it lasts.

I am still relatively new to game development so there might be a better solution for this.

Again I will check later tonight if there really are no mistakes in my code, I wrote it very late last night and got frustrated after 2 hours or so of not finding the mistake.

It’s weird and I wouldn’t recommend. It doesn’t have to… but it probably smells.

don’t do these things to MonoBehaviours. treat them as extensions to the underlying system.
don’t inherit them, promote them to singletons, pass them around as static references, expect them to persist and not die etc

you can do all of that to normal C# objects, MonoBehaviours are volatile, because what you see is just a wrapper, a tip of an iceberg.

Ok just got back home and checked the scripts. As far as I know there are no syntax errors or declaration problems.
I declared the two methods as

 private void OnSpecialStart()
    {
        inSpecial = true;
    }
    private void OnSpecialEnd()
    {
        inSpecial = false;
    }

, but again, in another script (class C), the subscription to the event via “SpecialMove.instance.SpecialStart += OnSpecialStart” works without a problem.

Can you elaborate on that? As I said Im still new and would appreciate if someone could tell the correct usage for MonoBehaviours, I might be doing things wrong.

I only promote MonoBehaviours to singeltons, if they have some kind of “managing” role, as in AudioManager, GameManger etc, and put them on empty GameObjects which I call the same.

My reason for having the singleton SpecialMove inherit from MonoBehaviour, is because it needs to be checked every frame (in update), and to my knowledege that is the only possible way.

Also to elaborate a little bit why I don’t manually link the SpecialMove class to the ObjBehaviour class on my gameobjects in the inspector: I am instantiating many game objects that have the ObjBehaviour script as a component, so they need to be able to subscribe (and find) my SpecialMove class in runtime, once they are initiated.

Thanks for all the help, I appreciate it.

One more thing I’d like to add that I found out. I put some Debug.Log()'s in my code like so:

private void OnEnable()
    {
        Debug.Log("A");
        SpecialMove.instance.SpecialStart += OnSpecialStart;
        SpecialMove.instance.SpecialEnd += OnSpecialEnd;
        Debug.Log("B");
    }

When starting the level I have 8 game objects who have the ObjBehaviour class with the above lines on it.
For some reason I get 8 prints of “A”, but only one print of “B”, meaning that the code only made it successfully through OnEnable() once, and failed 7 times.

I really suspect that the problem is somehow related to the many instances of ObjBehaviour I have, but I dont understand why.

Add Debug.Log(“C”); to your SpecialMove.Awake

Sure, here’s another recent thread where I did elaborate a bit. I wrote a proper singleton example, and a phat comment on MonoBehaviours in the end. https://discussions.unity.com/t/778842

You basically only need to decouple your critical path from relying onto MonoBehaviours as if they’re a system foundation. You should mostly treat them only as an entry point, a gateway system to cross-communicate your data. A ghastly apparition. There is no need to interlock design patterns with them, because they can be reserialized or turn dead without warnings or guarantees. You can see in the thread that this person had an issue with a singleton where it couldn’t persist, which violates the basic assumptions for a singleton. Why would you entangle yourself with such unnecessary lack of persistence control…

Now don’t get me wrong, there are ways to establish control, there are ways to sniff the potential changes before they arrive, but it’s also extra work for something that’s just as easily solved if only you stop working on top of them, and let them be.

I’m sure people have varying experiences, and make towers of Babylon with them, but in all truth, you want them to be the simplest of drivers and to easily hook to system events, only to propagate that knowledge further down the line, depending on how complex your codebase turns out to be.

And, to be clear, what you’re doing is not wrong per se, it’s just unreliable. You have almost no control. Maybe I’m overly superstitious, but in ten years I had never encountered a problem with them, simply because I’ve evaded any potential smell by top down design.

Just attach the debugger and step through the code so you can see what it’s doing.

This looks like an order of operations issue. The singleton class isn’t calling Awake early enough to assign itself as the static instance. Try setting the ScriptExectionOrder of the singleton class to be lower than that of the subscribing classes.

1 Like

@orionsyndrome Thank you for the detailed explanation and examples. My take away is to not make MonoBehaviours into “fake” singletons, but rather have a C# script be a true singelton, and then initialize it in a different MonoBehaviour script which is, for example, attached to some Manager Object.

@kru I might be, but then again, other scripts which have the exact same code when it comes to the event subscriptions do not have any problems with the order of operation.

I ditched the whole thing for now since I don’t seem to find a solution to the problem and found a different workaround, that doesnt involve the subscription to SpecialMove.cs’s events, but is (in my opinion) uglier.

I learned a couple of things in this thread though, so thanks for that. Maybe Ill rewrite a couple of my scripts and then try to attack events and subscriptions again.

For observers’ benefit, this is because of the order in which your scripts are executing. By default, all user created monobehaviors will have their awake/start/update/etc methods called in an indeterminate order. This means that scripts will have their methods called unpredictably.

In your case, you have some scripts trying to call SpecialMove.instance.whatever, before the SpecialMove singleton has had its Awake method called. These scripts which are being run before SpecialMove see that SpecialMove.instance is null, and throw a NullReference error.

The other scripts, which you say work fine, are being run after SpecialMove has had its Awake called, so SpecialMove.instance has been set, and everything is fine.

Go to Player Settings->Script Execution Order, drag in SpecialMove, and set its value to something negative so it runs before the other scripts. Your issue will clear up.

1 Like