Issues combining C# EventWrapper an Unity ScriptableObject/MonoBehaviour

So I have a MonoBehaviour script which has a ScriptableObject property I’m using to store settings data (pretty normal stuff). I’d like the MonoBehaviour to be told when changes are made to the values in the ScritpableObject so that it can make internal updates to some cached state to make it easier for me to try different settings quickly without having to constantly restart my game. I’m trying to get this to work in both edit and playback mode.

I’m pretty new to this so after a bit of digging I’ve come across C# events and specifically EventWrapper. This seems to work pretty well in edit mode, but whenever I go in to play mode I get some strange double notifications.

It’s like there is some sort of odd duplicate or half dead version of my MonoBehaviour and for the life of me I can’t work out what’s going on.

Here’s a minimal reproduction:

[CreateAssetMenu()]
public class SettingsObject : ScriptableObject
{
  [Min(0)]
  public int value = 0;

  public event EventHandler changeNotification;

  void OnValidate()
  {
    changeNotification?.Invoke(this, EventArgs.Empty);
  }
}

public class GameObject : MonoBehaviour
{
  public SettingsObject settings;

  [NonSerialized]
  public SettingsObject oldSettings;

  private void OnValidate()
  {
    Rebind();
  }

  void Rebind()
  {
    if (oldSettings != null)
    {
      Debug.Log(this + "[" + this.GetInstanceID() + " disconnecting from " + oldSettings + "[" + oldSettings.GetInstanceID() + "]");
      oldSettings.changeNotification -= HandleChange;
    }

    if (settings != null)
    {
      Debug.Log(this + "[" + this.GetInstanceID() + " connecting to " + settings + "[" + settings.GetInstanceID() + "]");
      settings.changeNotification -= HandleChange;
      settings.changeNotification += HandleChange;
    }

    oldSettings = settings;
  }

  private void OnDestroy()
  {
    Debug.Log(this + "[" + this.GetInstanceID() + " Destroyed");
    if (oldSettings != null)
    {
      Debug.Log(this + "[" + this.GetInstanceID() + " disconnecting from " + oldSettings + "[" + oldSettings.GetInstanceID() + "]");
      oldSettings.changeNotification -= HandleChange;
    }
  }

  void HandleChange(object s, EventArgs _)
  {
    ScriptableObject so = (ScriptableObject)s;
    Debug.Log(this + "[" + this.GetInstanceID() + "] event from " + s + "[" + so.GetInstanceID() + "]");
  }
}

I create an empty game object and add my GameObject script to it, then I create a couple instances of SettingsObject instances (SettingsA and SettingsB).

This all works as expected in the editor in edit mode, I can rebind the GameObject.settings property as much as I want and I get the notifications I’d expect when I change the appropriate settings object.

But when I hit play weird things happen.

Firstly I see:

GameObject (GameObject)[15430 connecting to SettingsB (SettingsObject)[15448]
GameObject (GameObject)[15430] event from SettingsB (SettingsObject)[15448]
GameObject (GameObject)[15430 connecting to SettingsB (SettingsObject)[15448]

Which I don’t understand. It’s re-making the connections to the settings instance twice, but internal state should prevent that (that’s what the oldSettings guards are for).

Then if I make a change to the bound settings object I see:

null[15430] event from SettingsB (SettingsObject)[15448]
GameObject (GameObject)[15430] event from SettingsB (SettingsObject)[15448]

That’s two notifications for one change, and what’s weirder the first one seems to be going to some sort of null instance (though how I can call GetInstanceID() on null without error I don’t understand).

When I end playback mode I get

GameObject (GameObject)GameObject (GameObject)[15430 Destroyed
[15430 disconnecting from SettingsB (SettingsObject)[15448]
GameObject (GameObject)[15430 connecting to SettingsB (SettingsObject)[15448]

Which confuses me, I didn’t think destroy got called until app exit or scene end.

And from this point on I still get the duplicate notifications even though I’m back in edit mode. pressing play again doesn’t compound the problem though, I’m stuck with two notifications for each change until I restart unity.

There’s obviously something I’m misunderstanding about the lifecycle of game objects that’s throwing me off course, I’d appreciate any pointers, or better solutions for this problem. I’ve dug in to this a bit with lots of log messages and looked through the lifecycle description, but I don’t see what I’m doing wrong.

Any help greatly appreciated.

This is indeed a likely-true diagnosis. You are fooling around right on the twilight edge of where things get torn down and rebuilt in Unity, and this happens anytime you go in and out of play mode, as well as obviously lots of other times.

In particular OnDestroy() can be extra confusing, because instances from a running scene can get destroyed in arbitrary order. I generally desperately try to stay away from OnDestroy(), as you can find rivers of digital ink spilled over people having edge cases with this, such as random stray GameObjects appearing in their (still-unmodified) scene after a PLAY session, or the traditional (but harmless?) editor complaint about:

Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)

Beyond the approach you’re trying now, outputting debug messages to try and identify lifecycle, I don’t have much more to add, except to suggest doing everything as late and sloppy-lazily as possible, such as by setting a boolean when you detect that something must happen, then later checking it in Update() and doing things there. This should get you out of a LOT of the twilight lifecycle areas, as well as also race conditions between Unity callbacks, which can obviously lead to deadlocks.

And then there is also this, my favorite Unity diagram:

https://docs.unity3d.com/Manual/ExecutionOrder.html

Your main problem is the inconsistency between build and editor play-mode behavior. And apparently this confused you so you wrote a bunch of methods to execute at various times so you avoid double-event registry.
I propose another route: tackle the difference between play-mode change and build runs. Here is an example class I’m using (this one is for ScriptableObjects, but you can change it for your need).
The important thing is: OnBegin → subscribe, OnEnd → unsubscribe. End of story, no more worry how enter play-mode handles your events. Obviously you can change those methods too. I chose OnBegin and OnEnd because one of the first programming language I learned was Pascal. :slight_smile:

using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

namespace LurkingNinja.KujiKiri.ManagedObjects
{
    public abstract class ManagedObject : ScriptableObject
    {
        protected abstract void OnBegin();
        protected abstract void OnEnd();
#if UNITY_EDITOR
        protected void Awake() => EditorApplication.playModeStateChanged += OnPlayStateChange;
        protected void OnDestroy() => EditorApplication.playModeStateChanged -= OnPlayStateChange;
        private void OnPlayStateChange(PlayModeStateChange state)
        {
            if (state == PlayModeStateChange.EnteredPlayMode) OnBegin();
            if (state == PlayModeStateChange.ExitingPlayMode) OnEnd();
        }
#else
        protected void Awake() => OnBegin();
        protected void OnDestroy() => OnEnd();
#endif
    }
}
2 Likes

Thanks both. I’ll give these another read tomorrow (late here right now) and try some of the ideas put forward.

I wondered about that but I had hoped to have it work in both edit and play mode. My actual game object has some editor based preview capabilities. Still it wouldn’t be the end of the world to loose them, I’ve not been using it a much as the actual real game object is getting more complex.

Aye, I’d seen that page. To be honest at this point in my Unity learning I found it more confusing than helpful, but I’ll need to give it another good read through.

I’m a little confused. The code snippet here is changing the behaviour between the version compiled for use in the unity editor and the version you’d compile and export? The goal here is to effectively tie my registration/deregistration to the play start/stop action when compiled for use in the editor?

I could see how that would work, again iId loose the use of the events in editor mode, but as I mentioned above I’m not too hung up on preserving the preview code I’ve currently got in place. That UNITY_EDITOR macro is useful though, I’ve no need of these events in an exported build so I can conditional all that out, useful, thanks.

How does that relate to the OnEnable/OnDisable call-backs I’ve saw them in the execution order page earlier and they seem to also be tied to start/stop play mode in the editor?