UnityEvent Serialization Research
I always forget what is the criteria so UnityEvent could target on the dropdown. By writing this article I could come back and read instead of trial-and-error!
This is something I always forget when designing a new API that I want to be compatible with
UnityEvent target serialization. I would like to end that, by writing this article so I could come back and read instead of blindly trial-and-error again to no avail.
Everyone knows that declaring
public will "expose the variable".
More correct term is that it mark that field for serialization. Serialization means the data could be remembered in the asset or in the scene instead of starting from zero. It is the data you can input to that exposed slot that get saved.
Then advanced Unity users know that
[SerializeField] property is a way to also allows other access modifier not
public to be serialized.
Finally even more advanced Unity user knows that
UnityEvent is equivalent to C#
Action but more awesome because it could be serialized! In other words, you get a nice editor that allows assigning invoke targets, which stays there nicely.
Despite an editor feature this nice available, I still see tons of Unity developers intentionally not using
Button's nice on click event box that appears in the editor and instead assign everything by code even though they are only one-time assignment. A month or so later, it is impossible to trace which button do what because they are all empty in the editor, and who know which code assign or replace the listener at which moment!
Main topics here is that what would be the criteria to make it appear in the dropdown? Surely, these aren't everything that
Camera has to offer.
What does the serialization looks like?
Notice that it "remember" the target with assembly-qualified class name. This has some concerns :
- If you change the class name will everything breaks?
- If you move the class to different
.asmdefwill it also breaks?
- Remembered argument is also assembly qualified full name. If Unity is evil and change assembly name of
UnityEnginethen would it breaks the universe?
- If you refactor target method's name, I think it will breaks?
Argument editor is more awesome than you think
You see the check box above for booleans. It also supports number box and string box. But less known feature (probably) is that it also supports an asset picker!
This is really useful to not have little pieces of audio playing code bits littering all over. Also it is extremely good with prefab workflow. It shows the blue override bar correctly. The prefab variant can add more entries, keep inheriting entries from root prefab, etc. It is critical with the new Unity Localization package which it uses event targeting extensively, all my texts are a variant of one root text prefab! But it is not our interest right now.
The serialization :
You also notice here that it is limited to only 5 types : object, int, float, string, bool. But the object type is so flexible and is my favorite. You should abuse it!
When will things break
Assembly-CSharp over there? I thought I could instantly screw up the serialization simply by moving the
ConnectionTarget to different assembly.
But turns out it is still fine.
Seems like the priority goes to the class name first and assembly is there to tie-break. If I rename the class...
Ok finally it breaks!
But you can still save its life. By doing things in this scene and save it, it will not try to nullify invalid serialization. Therefore when you realized you broken the link as far as the last month, it could still be saved somehow as old serialization is still there.
I could remove the
z in the class name, and the serialization could come back to life.
In a way, this non-GUID serialization is both blessing and a curse. Sometimes it makes you unable to refactor. But at the same time it allows certain flexibility... once in my project, I got into deep trouble and break everything in the project while refactoring!! I fixed the problem by scanning all YAML files and perform string replace of these class names and an entire project is saved. Thank god.
It means that normally
UnityEvent requires an invoker to specify nothing, the argument is fixed by whatever serialized.
Therefore dynamic means that the argument is specified by the invoker instead. Unity supports this by providing you
UnityEvent<T1,T2>, .. and so on. Up to 4 dynamic parameters. (Note that static one only supports 1 argument that you can type in Unity Inspector.)
But the catch is Unity can't serialize generic classes, it must be a solid class known ahead of time. How to make it "solid" is like this. It could be
private just fine too but remember to put on that
[Serializable] (Unlike fields, a
public class is not automatically serializable.)
Conveniently, Unity shows eligible dynamic event target up top. Selecting this no longer show
AudioClip selection box, because invoker will supply it.
You can still ignore the dynamic event and go back to use the static one as well.
public works (??)
This is the first criteria to get it on the dropdown. Supposed we have 3 versions like this :
public works. There is no magic attribute like
[SerializeMethod] to paste to make it work with
internal. This is quite sad for C# freaks who like an extra tidy code.
public one appears...
Can we hack it?
Where's the fun in playing fair, right? What if I just go ahead and edit the serialization like this. If you love serialization by name so much, then get wrecked.
Turns out it works (lmao), but do you want to sacrifice usability in Unity editor just to call
It even lights up in JetBrains Rider!
You can also "hack" in the editor by entering debug mode. To be honest the "debug" editor is kinda nice!
I am designing an Asset Store package however, so it is unacceptable that user must hack like this in order to make my component compatible with
UnityEvent. I have no choice but to play with
Other than that, you decide for yourself is the code hygiene of not using
public and still be able to call it from
UnityEvent important to you or not.
Only 1 static argument allowed
The static event can only call 1 argument methods. You can kinda confirm this by looking at the internal fields of event : the
arguments may have an
s, but it stands for holding 5 different type of arguments in 1 argument.
What if we have mismatched parameters than required
This event has 2 dynamic arguments
float. I want to try targeting the one with only
It does not work. Unity doesn't know how to ignore extra arguments.
What about less argument? Call this method but with the dynamic event with only one
Nope, nothing appears. Just in case, the perfectly matched one of course works.
What about optional arguments?
No! It is not that smart! You better think the optional argument as a real argument.
But this will place heavy restriction to package API design as well, as when you finally get a lot of smart optional arguments and overload resolution planned to work beautifully (e.g. just one
Play method name that works in many ways), then you want to make those compatible with
UnityEvent suddenly there is a wrench throw in.
A way to get out is to make a new entry point just for the
UnityEvent, but you have to invent a new name which may get ugly. (e.g. suspiciously wordy
PlayAudio in addition to
Play, just so
UnityEvent has something to connect to.) I do this in my package because it can't be helped.
Can we hack it?
It has just
AudioClip, but method name is edited to the one that also requires
float (or optional
It does not work. (Entering Play Mode and invoke will not do anything when "Missing", no error also.)
If there is a return value?
In my Asset Store package, I want the play to also return a value. This could be used to stop the already played audio.
Unfortunately, having non
void return value disqualifies everything. Including the
Same as dynamic version :
Can we hack it?
Perform the same hack by directly edit the method name. Note that the argument is perfectly compatible, exactly 1
Whoops, I was caught this time!
Though, we now additionally know that it uses
System.Delegate.CreateDelegate underneath, which is standard C# rather than a Unity-thing. So if we know how that works, then we know what could and could not be hacked. It could be assumed now that Unity engine coded such that it binds to
void return value of the target class, but there is no criteria on access modifier so the hack was successful.
Interestingly CreateDelegate overloads has
firstArgument criteria, so maybe this implies if we hack the 2nd argument it could get through. But I am too tired to try that now.
interface arguments, but subclass OK
If you made your API to be nicely handling
interface of your custom class, then bad news the serializer don't know how to deal with it.
But subclass is OK! This allows the asset picker to pick any subclass from the base class specified in the method's signature. This allows you to use
abstract class as well. In this image,
AuditoPlayableBase is an
In this picture, that
public void is just a scapegoat for
UnityEvent targeting. It is using a
interface so Unity know how to serialize the static argument. Moreover it has
void return and not
AuditoHandle I actually want to use.
The object picker shows all the choices that subclassed from
AuditoPlayableBase that aren't
abstract anymore. This is a really nice developer experience for the user.
This leads to weird class tree design where an
abstract class is there just so Unity could know how to serialize, but the real deal is the
interface I want to use everywhere. The best solution maybe to hide the
abstract class, and
abstract class may essentially do nothing because the
interface had already locked in everything. (lol)
I found about this by mistake. Though it make sense that it should be able to, I didn't think Unity would actually let me to do this lol.
This is given all the previous criteria are passed, so must be returning
void. It can take arguments just fine, like discussed.
Implications in API design
Because the target's name will be forever serialized in the client's code, you must make damn sure you don't refactor the name to something else. This already applies to all your
public code, but breaking people's serialized event target won't trigger compilation error. Scary stuff!
And you have to be more skillfully sidestep everything. Take a look at this :
- The first requirement is that I want the
Playwith complete overload + default argument, so I could type
NonpositionalAudio.Playto use it. It has return value too.
- Then I want to make an instance
Playmethod to service those that wants to use
UnityEvent. First I have to sacrifice return type, then also
interfaceis not usable.
- Lastly there is one more
Playoverload I want to be able to use with events. But if that is named
Play, it will cause infinite recursion to itself instead of resolving to the
staticmethod. Now I have to name it something else.
And this 5
Normally the bottom 2 are enough to handle everything I want, but if I want it to be compatible with events, I need 3 more above with no return value. Also I cannot just use default arguments, I need to explicitly add all the combinations. Of course I cannot use
interface too. Lastly, I can't name the top 3
Play anymore as it would cause similar problem as before. So, I name them
(Other idea includes
PlayFromEvent, to explicitly excuse that there is no return value because it is "for event", but I think that is weirder.)