EDIT: For a complete serializable/inspectable delegate solution, see my uFAction.
I have been working on this lately - To make a delegate survive, you need to either serialize the delegate itself or rebuild it. Serializing a delegate = serializing all its targets and methods, rebuilding a delegate = serializing its targets and methods separately, and creating the delegate via Delegate.CreateDelegate
.
If you want your delegates to target regular System.Objects
(anything but a UnityEngine.Object
) then you could manually serialize/deserialize the delegate itself (no need to rebuild) - But if you’re targeting UnityEngine.Objects
(for ex you need to notify Components, MonoBehaviours, etc) - then you can rebuild the delegate because the target (which is a UnityEngine.Object) can be serialized by Unity’s serialization system.
The reason for this: when you target a System.Object, you can’t recreate the delegate because to recreate it, you need to know its Target
and the Method
and since you’re targeting a System.Object
, the target is not serialized by Unity.
But when you target a UnityEngine.Object, it’s serialized so you could recreate the delegate.
Here’s an example of a SerializedAction
that targets System.Objects
- I’m serializing it normally via BinaryFormatter
- this is tested and the delegate persists between reloads.
using UnityEngine;
using System.Collections.Generic;
using System;
using System.Linq;
using System.IO;
using System.Reflection;
[Serializable]
public class SerializedAction
{
private Action action;
[SerializeField] private string serializedData;
public object[] Targets { get { return Get().GetInvocationList().Select(d => d.Target).ToArray(); } }
public void Invoke()
{
var a = Get();
if (a != null)
a.Invoke();
}
public void Add(Action handler)
{
ChangeAction(() => action = Get() + handler);
}
public void Remove(Action handler)
{
ChangeAction(() => action = Get() - handler);
}
public Action Get()
{
if (action == null)
action = Utils.DeserializeFromString<Action>(serializedData);
return action;
}
private void ChangeAction(Action change)
{
change();
serializedData = Utils.SerializeToString(action);
}
}
In your utils:
/// <summary>
/// Serializes 'value' to a string, using BinaryFormatter
/// </summary>
public static string SerializeToString<T>(T value)
{
using (var stream = new MemoryStream()) {
(new BinaryFormatter()).Serialize(stream, value);
stream.Flush();
return Convert.ToBase64String(stream.ToArray());
}
}
/// <summary>
/// Deserializes an object of type T from the string 'data'
/// </summary>
public static T DeserializeFromString<T>(string data)
{
byte[] bytes = Convert.FromBase64String(data);
using (var stream = new MemoryStream(bytes)) {
return (T)(new BinaryFormatter()).Deserialize(stream);
}
}
Now, with this you can’t target UnityEngine.Objects, because they’re not marked with System.Serializable
to be serialized via BinaryFormatter
.
This means a new system have to exist for targeting UnityEngine.Object
s:
using UnityEngine;
using System.Collections.Generic;
using System;
using System.Linq;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Reflection;
using bf = System.Reflection.BindingFlags;
using Object = UnityEngine.Object;
[Serializable]
public class SerializedUnityAction
{
[Serializable]
private class Entry
{
public Object target;
public List<string> methodsNames = new List<string>();
public Entry() { }
public Entry(Object target, string methodName)
{
this.target = target;
methodsNames.Add(methodName);
}
}
private Action action;
[SerializeField] private List<Entry> entries = new List<Entry>();
public Object[] Targets { get { return entries.Select(e => e.target).ToArray(); } }
public SerializedUnityAction() { }
public SerializedUnityAction(Action handler) { Add(handler); }
public void Invoke()
{
Get().Invoke();
}
private bool IsValidHandler(Action handler)
{
return handler != null && handler.Target is MonoBehaviour;
}
public void Add(Action handler)
{
if (!IsValidHandler(handler)) return;
var target = handler.Target as Object;
if (!Targets.Contains(target)) { // if we're targeting a new Object
entries.Add(new Entry(target, handler.Method.Name));
}
else entries.First(e => e.target == target).methodsNames.Add(handler.Method.Name);
action = Get() + handler;
}
public void Remove(Action handler)
{
if (!IsValidHandler(handler)) return;
var target = handler.Target as Object;
int index = Targets.ToList().IndexOf(target);
if (index == -1) return; // handler target doesn't exist
entries[index].methodsNames.Remove(handler.Method.Name);
if (entries[index].methodsNames.IsEmpty()) // no more handler for that target
entries.RemoveAt(index); // so just remove the entry
action = Get() - handler;
}
private Action Get()
{
if (action == null) {
// re-build the whole invocation list! :)
foreach (var entry in entries) {
var target = entry.target;
if (target == null) continue;
foreach (var method in entry.methodsNames) {
action += Delegate.CreateDelegate(typeof(Action), target, method) as Action;
}
}
}
return action;
}
}
The idea as I mentioned before, is to rebuild the delegate when it’s null (when an assembly reloads happen). Now to rebuild it, you need to have the target and the method names available to you (so target
and methodsNames
MUST serialize and survive) - you might ask, why save the methodsNames
instead of the methodsInfos
directly? well, cause strings serialize, while MethodInfos
don’t - and for this simple delegate, strings are enough - complexity is added as and when needed and not when you think you might need it 