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.