During the implementation of my game using Unity I was faced with the following setup:
- I have a
ScriptableObject
(as an asset) that has a c#event
delegate. - I have a
MonoBehaviour
that has a serialized reference to theScriptableObject
that has the delegate.
I want to "subscribe" the MonoBehaviour
to that ScriptableObject
's event, properly handling the event to avoid memory leaks. Initially I supposed that subscribing to the event on the OnEnable
callback and unsubscribing it on the OnDisable
was enough. However, a memory leak occurs when a developer, by using the Unity Inspector, swaps the value of the serialized reference to the ScriptableObject
during play.
Is there a canonical way to safely subscribe and unsubscribe to c# events in a serialized reference to a ScriptableObject
, given that I want the developers of the game to be able to swap value in the inspector during play?
To illustrate that, I have written a simple code for that scenario:
SubjectSO.cs (The ScriptableObject
with the event)
using UnityEngine;
using System;
[CreateAssetMenu]
public class SubjectSO : ScriptableObject
{
public event Action<string> OnTrigger;
public void Invoke()
{
this.OnTrigger?.Invoke(this.name);
}
}
ObserverMB .cs (The MonoBehaviour
that wants to subscribe to the event in the ScriptableObject
)
using UnityEngine;
public class ObserverMB : MonoBehaviour
{
public SubjectSO subjectSO;
public void OnEnable()
{
if(this.subjectSO != null)
{
this.subjectSO.OnTrigger += this.OnTriggerCallback;
}
}
public void OnDisable()
{
if(this.subjectSO != null)
{
this.subjectSO.OnTrigger -= this.OnTriggerCallback;
}
}
public void OnTriggerCallback(string value)
{
Debug.Log("Callback Received! Value = " + value);
}
}
InvokesSubjectSOEveryUpdate .cs (Auxiliary MonoBehaviour
, for testing)
using UnityEngine;
public class InvokesSubjectSOEveryUpdate : MonoBehaviour
{
public SubjectSO subjectSO;
public void Update()
{
this.subjectSO?.Invoke();
}
}
For testing, I have created two assets of the type SubjectSO
, named:
- SubjectA
- SubjectB
Then, I have created a GameObject
in scene, and attached the following components:
ObserverMB
, referencing SubjectAInvokesSubjectSOEveryUpdate
, referencing SubjectAInvokesSubjectSOEveryUpdate
, referencing SubjectB
When hitting play, the message Callback Received! Value = SubjectA
is printed in the Console every Update, which is expected.
Then, when I use the inspector to change the reference in ObserverMB
from SubjectA to SubjectB, while the game is still playing, the message Callback Received! Value = SubjectA
still keeps being printed.
If I disable and enable ObserverMB
in the inspector, both messages Callback Received! Value = SubjectA
and Callback Received! Value = SubjectB
start being printed every Update.
The initial callback subscription is still in effect, but, as a subscriber, ObserverMB
has lost the reference to that event.
How can I avoid that situation?
I really believe that this seems to be a common use scenario for the use of c# event
delegates and ScriptableObjects
and it seems strange for me that OnEnable
and OnDisable
do not properly handle the serialization case of a developer tweaking the inspector.