- Home /
How to make Delegates/Events survive assembly reload?
Hi, I'm trying to make usage of C# Events in Unity, however, they don't survive Assembly reloads so they stop working, forcing me to restart the game (in the editor). So I would like to know: is it possible to make Delegates or Events survive assembly reloads?
Thanks in advance!
How are you getting an Assembly reload when the game is playing?
Whenever I change anything in code, be it a simple string or a whole function
So not when the game is running then. You mean when you are editing it. You cannot preserve against that without writing code that will rewire the delegates and events during object initialization.
Yes, sorry, should have specified what kind of restart. So basically I just have to make sure if the events are wired properly, if not, redo them again?
Yes, you will get reload events when the scripts start back up - if they are ExecuteInEdit$$anonymous$$ode then you will get Awake etc.
You can also have static functions run immediately as soon as there is an assembly reload using: [InitializeOnLoad] on a class whose static constructor will be called.
Answer by vexe · Mar 12, 2014 at 12:50 PM
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 :)
I would really like for the people who downvotes to provide reasons/comments as to why they downvoted, and not just hit and run!
Yes sure I won't disappoint nor leave you unsatisfied :) - I will post examples with the first editor-working version I get.
You could actually take these two right now and use them! except with no editor support yet.
[System.Serializable]
public class TestClass1
{
public void Print()
{
Debug.Log("Printin' stuff ONE yo!");
}
}
[System.Serializable]
public class TestClass2
{
public void Print()
{
Debug.Log("Printin' stuff TWO yo!");
}
}
public class Test$$anonymous$$B : $$anonymous$$onoBehaviour
{
public SerializedAction meAction = new SerializedAction();
public TestClass1 test1 = new TestClass1();
public TestClass2 test2 = new TestClass2();
private void OnEnable()
{
meAction.Add(test1.Print);
meAction.Add(test2.Print);
}
private void OnDisable()
{
meAction.Remove(test1.Print);
meAction.Remove(test2.Print);
}
private void OnGUI()
{
if (GUILayout.Button("Invoke")) meAction.Invoke();
}
}
This is an example of the first one. The second (which targets $$anonymous$$Bs) is exactly the same, except the target is an $$anonymous$$B ins$$anonymous$$d of just a regular System.Object
(regular class, in this case TestClass1/2)
$$anonymous$$ake sure you trigger assembly reloads and see if they persist or not :)
Here's a small $$anonymous$$ser - basically I've set it up so that you drag/drop a game object, then you could add targets (from the $$anonymous$$onoBehaviours it has) - in this case my "Test" object have a "ItemPickup" $$anonymous$$B, you could add as many $$anonymous$$Bs as you like, then you could add method entries to notify from that $$anonymous$$B - still in progress.
Apologies for that - this is happening because, the first time you try to add something to the delegate, the 'action' is still null, it haven't been set/subscribed to anything - Now, when Get
sees that, it tries to deserialize the delegate, which haven't been serialized yet!
You might think O$$anonymous$$, an easy solution would be to initialize the action with an empty delegate (`delegate { }`) - but that wouldn't solve it actually, it will work the first time, but after an assembly reload, it will re-initialize it to an empty delegate and so you will lose your subscriptions!
Another way would be to use a hasInit
boolean, but not the best.
At the moment though, just change your Deserialize method to:
private Action Deserialize()
{
try {
using (var fs = File.OpenRead("_SERIALIZED_ACTION_" + guid)) {
var formatter = new BinaryFormatter();
return formatter.Deserialize(fs) as Action;
}
}
catch { return action; }
}
This works - it suppresses the error and returns the action - but I still feel there's something better. I will let you know and always keep you updated.
Another idea I just had in $$anonymous$$d, is to serialize the action in its constructor! hmm, interesting...
[SCRATCH THAT] one can't serialize a null wtf? lol
Answer by stepan-stulov · Aug 03, 2015 at 11:32 AM
Unity has serialisable delegates from the box.
Ok, Unity has (semi-) serialisable delegates from the box. It's not directly a serialisation of delegates, rather of observers, a notifier+observer pattern. Unity's UnityEvent, that's used for Inspector-driven UI event listening (when you hang a game object and its specific method to call on a button's Click event e.g.) is serialised multicast delegates. It's slow (there was an article somewhere comparing native C# delegates and Unity's delegates and it was magnitude 3 difference, but I can't seem to find it). But it works. You can also declare your own events/delegates as either public or [SerilizeField] private fields of type UnityEvent (or one of it's generic variations, up to 4 arguments, but in this case you'd need to extend the generic class, as Unity can't serialise generics) and they become inspector-injectable. These events have API for adding and removing listeners (and obviously invoking) from script. But script-added/removed listeners are non persistent. So happy mouse-programming:)
This feature enable developers to expose events for hooking up listeners to non-programmer folks. I'd say quiet a niche tool for event-driven visual level editing. I can imagine creating a set of actor game objects and a "protocol" of event cross-linking and vuala, a game designer can create little sandbox simulations with no coding help. Correct me if I'm wrong, this is how the GameMaker works in their visual scripting tool.
That having been said, I think you should keep programmer-for-programmer delegates in pure C# and unexposed for inspector.
The biggest problem of these serialised events is that they remain completely unadvertised outside of new UI system by Unity, although they're completely UI agnostic.
Here is a little video, where a guy uses these events (without the UI system):
https://www.youtube.com/watch?v=tZENtGYccGI
public OrcWasCreatedEvent OrcWasCreated; // public class OrcWasCreatedEvent : UnityEvent<Orc>
public UnityEvent AllOrcsWereDestroyed;
// later
OrcWasCreated.Invoke(myOrc);
AllOrcsWereDestroyed.Invoke();
Hi! I used UnityEvents and they do not seem to persist.$$anonymous$$y goal is to be able to call a method from custom editor when game starts. OnEnable and OnInspectorGUI get called only when object is selected. Thus following this answer(http://answers.unity.com/answers/358661/view.html) I ended up with a event solution $$anonymous$$y setup is like this: Bot.cs
public class Bot : $$anonymous$$onoBehaviour
{
public UnityEvent loadEvent;
void Start()
{
loadEvent = new UnityEvent();
Debug.Log("event has in start : " + GetListenerNumber(loadEvent));
loadEvent.Invoke();
}
public static int GetListenerNumber(UnityEventBase unityEvent)
{
var field = typeof(UnityEventBase).GetField("m_Calls", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
var invokeCallList = field.GetValue(unityEvent);
var property = invokeCallList.GetType().GetProperty("Count");
return (int)property.GetValue(invokeCallList);
}
private void OnDrawGizmos()
{
Debug.Log("event has in gizmos for " + name + " : " + GetListenerNumber(loadEvent) + "listeners");
}
}
Ok and the custom editor simpy has OnEnable
[CustomEditor(typeof(Bot))]
public class CustomBot : Editor
{
Bot bot;
private void OnEnable()
{
bot = (Bot)target; // set the bot target
bot.loadEvent.AddListener(LoadEveryThing);
Debug.Log("added listener");
LoadEveryThing();
}
}
First of all in the scene view I click on all bots,thus calling onenable. The gizmo debug shows well. I run the game and suddenly, the events have 0 listeners.
Answer by Bunny83 · Mar 12, 2014 at 11:59 AM
The answer is: it's not possible ;) An "assembly reaload" sounds like some kind of quick update check but in fact the whole scripting environment reloads. This will destroy everything in the managed land. Unity can recover from this by using it's serialization system. Unity serializes the whole scene before the reload, then recreates everything and deserializing the whold scene. Of course only things which can be serialized will "survive" this process.
Unfortunately there is no ingame event / notification when the serializing / deserializing takes place. So you can't react to those actiobs.
What you could do is to backup all delegate assignments with a component + function-name pair stored in serialized variables so in start you might be able to reconstruct the delegate. Multicast delegates are very hard and anonymous delegates impossible to serialize.
If you want this "feature" to work, only use serializable types / variables. However i have to say i don't need that feature ;) It's just there to simplify debugging a bit. However there's always a chance that certain states can't be reverted to it's last state (coroutines for example). So what happens after a "reload" is in most cases kind of random.
This would never happen in the build, so i wouldn't rely on that feature. It's better to provide some debug initialization which allows you to start your game with a well defined state.
Dear Bunny83, in fact now (since the introduction of the new UI system in Unity, which I'm not sure was before or after your reply) it's possible. There indeed is a native in-game event serialisation. Please check my answer on this page. Perhaps not as clean as would be desired, as Unity doesn't have first-class serializable delegates/functions. But in fact the closest from-the-box native feature of Unity to what's asked. Cheers