-
-
Save wesleywh/1c56d880c0289371ea2dc47661a0cdaf to your computer and use it in GitHub Desktop.
public static void CopyUnityEvents(object sourceObj, string source_UnityEvent, object dest, bool debug = false) | |
{ | |
FieldInfo unityEvent = sourceObj.GetType().GetField(source_UnityEvent, E_Helpers.allBindings); | |
if (unityEvent.FieldType != dest.GetType()) | |
{ | |
if (debug == true) | |
{ | |
Debug.Log("Source Type: " + unityEvent.FieldType); | |
Debug.Log("Dest Type: " + dest.GetType()); | |
Debug.Log("CopyUnityEvents - Source & Dest types don't match, exiting."); | |
} | |
return; | |
} | |
else | |
{ | |
SerializedObject so = new SerializedObject((Object)sourceObj); | |
SerializedProperty persistentCalls = so.FindProperty(source_UnityEvent).FindPropertyRelative("m_PersistentCalls.m_Calls"); | |
for (int i = 0; i < persistentCalls.arraySize; ++i) | |
{ | |
Object target = persistentCalls.GetArrayElementAtIndex(i).FindPropertyRelative("m_Target").objectReferenceValue; | |
string methodName = persistentCalls.GetArrayElementAtIndex(i).FindPropertyRelative("m_MethodName").stringValue; | |
MethodInfo method = null; | |
try | |
{ | |
method = target.GetType().GetMethod(methodName, E_Helpers.allBindings); | |
} | |
catch | |
{ | |
foreach (MethodInfo info in target.GetType().GetMethods(E_Helpers.allBindings).Where(x => x.Name == methodName)) | |
{ | |
ParameterInfo[] _params = info.GetParameters(); | |
if (_params.Length < 2) | |
{ | |
method = info; | |
} | |
} | |
} | |
ParameterInfo[] parameters = method.GetParameters(); | |
switch(parameters[0].ParameterType.Name) | |
{ | |
case nameof(System.Boolean): | |
bool bool_value = persistentCalls.GetArrayElementAtIndex(i).FindPropertyRelative("m_Arguments.m_BoolArgument").boolValue; | |
var bool_execute = System.Delegate.CreateDelegate(typeof(UnityAction<bool>), target, methodName) as UnityAction<bool>; | |
UnityEventTools.AddBoolPersistentListener( | |
dest as UnityEventBase, | |
bool_execute, | |
bool_value | |
); | |
break; | |
case nameof(System.Int32): | |
int int_value = persistentCalls.GetArrayElementAtIndex(i).FindPropertyRelative("m_Arguments.m_IntArgument").intValue; | |
var int_execute = System.Delegate.CreateDelegate(typeof(UnityAction<int>), target, methodName) as UnityAction<int>; | |
UnityEventTools.AddIntPersistentListener( | |
dest as UnityEventBase, | |
int_execute, | |
int_value | |
); | |
break; | |
case nameof(System.Single): | |
float float_value = persistentCalls.GetArrayElementAtIndex(i).FindPropertyRelative("m_Arguments.m_FloatArgument").floatValue; | |
var float_execute = System.Delegate.CreateDelegate(typeof(UnityAction<float>), target, methodName) as UnityAction<float>; | |
UnityEventTools.AddFloatPersistentListener( | |
dest as UnityEventBase, | |
float_execute, | |
float_value | |
); | |
break; | |
case nameof(System.String): | |
string str_value = persistentCalls.GetArrayElementAtIndex(i).FindPropertyRelative("m_Arguments.m_StringArgument").stringValue; | |
var str_execute = System.Delegate.CreateDelegate(typeof(UnityAction<string>), target, methodName) as UnityAction<string>; | |
UnityEventTools.AddStringPersistentListener( | |
dest as UnityEventBase, | |
str_execute, | |
str_value | |
); | |
break; | |
case nameof(System.Object): | |
Object obj_value = persistentCalls.GetArrayElementAtIndex(i).FindPropertyRelative("m_Arguments.m_ObjectArgument").objectReferenceValue; | |
var obj_execute = System.Delegate.CreateDelegate(typeof(UnityAction<Object>), target, methodName) as UnityAction<Object>; | |
UnityEventTools.AddObjectPersistentListener( | |
dest as UnityEventBase, | |
obj_execute, | |
obj_value | |
); | |
break; | |
default: | |
var void_execute = System.Delegate.CreateDelegate(typeof(UnityAction), target, methodName) as UnityAction; | |
UnityEventTools.AddPersistentListener( | |
dest as UnityEvent, | |
void_execute | |
); | |
break; | |
} | |
} | |
} | |
} |
I'm using something similar, but I use UnityEventBase.GetValidMethodInfo
, UnityEventBase.GetPersistentTarget
, and the method name likewise, and I retrieve the value usingSerializedProperty
. I need to figure out how to copy entire property trees. My current method also ensures that existing calls in the destination are not overwritten, exits quickly when possible, can optionally delete calls from the source while preserving dynamic calls, and has a dry run mode! Unfortunately it's proprietary code so I will need to remove the the private bits before I post it. It doesn't understand Object references either.
So far, your method shows that I have to iterate on the property tree by hand and copy the required values; may save the cost of reflection.
This method will also be able to transfer dynamic calls, but I need to see if that is even possible.
If only the PersistentCalls
object was available as a type. 😉
One place to look at is the m_Mode
field I guess.
Admittedly pretty rough but it works.
B()
and I()
are extension methods for surrounding the string with <b>
and <i>
tags.
This script copies the entire property tree representing the Unity Event irrespective of the types of the calls, their modes, or their argument values. It supports undo and redo, and allows dry runs and removing calls from the source. Currently, works on Unity Events on the same object, but it should be easy to use it for Unity Events on multiple objects.
Can only be used in an Editor script, or using #if UNITY_EDITOR
.
/// <returns>A log of all transferred calls, empty if there were none.</returns>
public static
string TransferPersistentCalls(
UnityEngine.Object item, in string fromName, in string toName,
in bool removeOldCalls = false, in bool dryRun = true)
{
var serializedItem = new SerializedObject(item);
const string CallsPropertyPathFormat = "{0}.m_PersistentCalls.m_Calls";
var srcCalls = serializedItem.FindProperty(string.Format(CallsPropertyPathFormat, GetValidFieldName(fromName.Trim())));
var dstCalls = serializedItem.FindProperty(string.Format(CallsPropertyPathFormat, GetValidFieldName(toName.Trim())));
var dstOffset = dstCalls.arraySize;
var log = string.Empty;
for (var srcIndex = 0; srcIndex < srcCalls.arraySize; srcIndex++)
{
#region Log Source Properties
var srcCall = srcCalls.GetArrayElementAtIndex(srcIndex);
var srcTarget = GetCallTarget(srcCall);
var srcMethodName = GetCallMethodName(srcCall);
var srcMode = GetCallMode(srcCall);
var srcCallState = GetCallState(srcCall);
var srcArgs = GetCallArgs(srcCall);
var srcObjectArg = GetCallObjectArg(srcArgs);
var srcObjectArgType = GetCallObjectArgType(srcArgs);
var srcIntArg = GetCallIntArg(srcArgs);
var srcFloatArg = GetCallFloatArg(srcArgs);
var srcStringArg = GetCallStringArg(srcArgs);
var srcBoolArg = GetCallBoolArg(srcArgs);
log += $"At index {srcIndex}:\n".B();
log += $"\t{srcTarget.displayName.B()}: {srcTarget.propertyType.I()} = {srcTarget.objectReferenceValue}\n";
log += $"\t{srcMethodName.displayName.B()}: {srcMethodName.propertyType.I()} = {srcMethodName.stringValue}\n";
log += $"\t{srcMode.displayName.B()}: {srcMode.propertyType.I()} = {srcMode.enumValueIndex}\n";
log += $"\t{srcCallState.displayName.B()}: {srcCallState.propertyType.I()} = {srcCallState.enumValueIndex}\n";
log += $"\t{srcArgs.displayName.B()}: {srcArgs.propertyType.I()} =\n";
log += $"\t\t{srcObjectArg.displayName.B()}: {srcObjectArg.propertyType.I()} = {srcObjectArg.objectReferenceValue}\n";
log += $"\t\t{srcObjectArgType.displayName.B()}: {srcObjectArgType.propertyType.I()} = {srcObjectArgType.stringValue}\n";
log += $"\t\t{srcIntArg.displayName.B()}: {srcIntArg.propertyType.I()} = {srcIntArg.intValue}\n";
log += $"\t\t{srcFloatArg.displayName.B()}: {srcFloatArg.propertyType.I()} = {srcFloatArg.floatValue}\n";
log += $"\t\t{srcStringArg.displayName.B()}: {srcStringArg.propertyType.I()} = {srcStringArg.stringValue}\n";
log += $"\t\t{srcBoolArg.displayName.B()}: {srcBoolArg.propertyType.I()} = {srcBoolArg.boolValue}\n\n";
#endregion
if (!dryRun)
{
SerializedProperty dstCall;
SerializedProperty dstTarget;
SerializedProperty dstMethodName;
SerializedProperty dstMode;
SerializedProperty dstCallState;
SerializedProperty dstArgs;
SerializedProperty dstObjectArg;
SerializedProperty dstObjectArgType;
SerializedProperty dstIntArg;
SerializedProperty dstFloatArg;
SerializedProperty dstStringArg;
SerializedProperty dstBoolArg;
if (dstOffset > 0)
{
#region Check if the Call already Exists in the Destination
dstCall = dstCalls.GetArrayElementAtIndex(srcIndex);
// If we are satisfied that the call is exactly the same, skip ahead.
if (SerializedProperty.DataEquals(srcCall, dstCall))
{
log += $"(Already present in {toName.I()}.)\n\n";
continue;
}
#endregion
}
// Only unique properties beyond this point. Append with care.
#region Copy Properties from Source to Destination
dstCalls.InsertArrayElementAtIndex(dstOffset + srcIndex);
dstCall = dstCalls.GetArrayElementAtIndex(dstOffset + srcIndex);
dstTarget = GetCallTarget(dstCall);
dstMethodName = GetCallMethodName(dstCall);
dstMode = GetCallMode(dstCall);
dstCallState = GetCallState(dstCall);
dstArgs = GetCallArgs(dstCall);
dstObjectArg = GetCallObjectArg(dstArgs);
dstObjectArgType = GetCallObjectArgType(dstArgs);
dstIntArg = GetCallIntArg(dstArgs);
dstFloatArg = GetCallFloatArg(dstArgs);
dstStringArg = GetCallStringArg(dstArgs);
dstBoolArg = GetCallBoolArg(dstArgs);
dstTarget.objectReferenceValue = srcTarget.objectReferenceValue;
dstMethodName.stringValue = srcMethodName.stringValue;
dstMode.enumValueIndex = srcMode.enumValueIndex;
dstCallState.enumValueIndex = srcCallState.enumValueIndex;
dstObjectArg.objectReferenceValue = srcObjectArg.objectReferenceValue;
dstObjectArgType.stringValue = srcObjectArgType.stringValue;
dstIntArg.intValue = srcIntArg.intValue;
dstFloatArg.floatValue = srcFloatArg.floatValue;
dstStringArg.stringValue = srcStringArg.stringValue;
dstBoolArg.boolValue = srcBoolArg.boolValue;
#endregion
}
}
#region Remove Old Calls from Source
if (!dryRun && removeOldCalls)
{
srcCalls.ClearArray();
}
#endregion
serializedItem.ApplyModifiedProperties();
return log;
#region Local Functions
/// <summary></summary>
/// <returns>The original name if it's a regular Unity Event.</returns>
string GetValidFieldName(in string name)
{
var field = item.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
var value = field?.GetValue(item);
if (value is UnityEventBase)
{
return name;
}
else
{
throw new FieldAccessException("Incorrect event name.");
}
}
SerializedProperty GetCallTarget(in SerializedProperty callProperty) => callProperty?.FindPropertyRelative("m_Target");
SerializedProperty GetCallMethodName(in SerializedProperty callProperty) => callProperty?.FindPropertyRelative("m_MethodName");
SerializedProperty GetCallMode(in SerializedProperty callProperty) => callProperty?.FindPropertyRelative("m_Mode");
SerializedProperty GetCallState(in SerializedProperty callProperty) => callProperty?.FindPropertyRelative("m_CallState");
SerializedProperty GetCallArgs(in SerializedProperty callProperty) => callProperty?.FindPropertyRelative("m_Arguments");
SerializedProperty GetCallObjectArg(in SerializedProperty argsProperty) => argsProperty?.FindPropertyRelative("m_ObjectArgument");
SerializedProperty GetCallObjectArgType(in SerializedProperty argsProperty) => argsProperty?.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName");
SerializedProperty GetCallIntArg(in SerializedProperty argsProperty) => argsProperty?.FindPropertyRelative("m_IntArgument");
SerializedProperty GetCallFloatArg(in SerializedProperty argsProperty) => argsProperty?.FindPropertyRelative("m_FloatArgument");
SerializedProperty GetCallStringArg(in SerializedProperty argsProperty) => argsProperty?.FindPropertyRelative("m_StringArgument");
SerializedProperty GetCallBoolArg(in SerializedProperty argsProperty) => argsProperty?.FindPropertyRelative("m_BoolArgument");
#endregion
}
This is good stuff. I'll have to try this out when I get a chance. Thanks for this!
Well I did have another round on the script and encapsulated the SerializedProperty
references into a struct
. This allowed me to turn the local functions into static methods - C# 7.3 doesn't have static local methods.
Okay so here's the version which incorporates all that but does away with removing the old calls because it would remove everything without copying the calls otherwise. It's trivial to delete events anyway.
using Object = UnityEngine.Object;
private struct PersistentCall
{
private SerializedProperty callProperty;
private string propertyPathBase;
private SerializedProperty target;
private SerializedProperty methodName;
private SerializedProperty mode;
private SerializedProperty callState;
private SerializedProperty args;
private SerializedProperty objectArg;
private SerializedProperty objectArgType;
private SerializedProperty intArg;
private SerializedProperty floatArg;
private SerializedProperty stringArg;
private SerializedProperty boolArg;
internal PersistentCall(in SerializedProperty callProperty, in string propertyPathBase)
{
// Read and cache
this.callProperty = callProperty;
this.propertyPathBase = propertyPathBase;
target = callProperty?.FindPropertyRelative("m_Target");
methodName = callProperty?.FindPropertyRelative("m_MethodName");
mode = callProperty?.FindPropertyRelative("m_Mode");
callState = callProperty?.FindPropertyRelative("m_CallState");
args = callProperty?.FindPropertyRelative("m_Arguments");
objectArg = args?.FindPropertyRelative("m_ObjectArgument");
objectArgType = args?.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName");
intArg = args?.FindPropertyRelative("m_IntArgument");
floatArg = args?.FindPropertyRelative("m_FloatArgument");
stringArg = args?.FindPropertyRelative("m_StringArgument");
boolArg = args?.FindPropertyRelative("m_BoolArgument");
}
internal static void MemberwiseClone(in PersistentCall src, in PersistentCall dst)
{
// Write
dst.target.objectReferenceValue = src.target.objectReferenceValue;
dst.methodName.stringValue = src.methodName.stringValue;
dst.mode.enumValueIndex = src.mode.enumValueIndex;
dst.callState.enumValueIndex = src.callState.enumValueIndex;
dst.objectArg.objectReferenceValue = src.objectArg.objectReferenceValue;
dst.objectArgType.stringValue = src.objectArgType.stringValue;
dst.intArg.intValue = src.intArg.intValue;
dst.floatArg.floatValue = src.floatArg.floatValue;
dst.stringArg.stringValue = src.stringArg.stringValue;
dst.boolArg.boolValue = src.boolArg.boolValue;
}
public override string ToString() => $"[{(UnityEventCallState)callState.enumValueIndex}] {target.objectReferenceValue}.{methodName.stringValue}({GetParamSignature(mode.enumValueIndex)})";
private string GetParamSignature(in int enumIndex)
{
switch (enumIndex)
{
case 0: // Event Defined
return $"{GetEventType(this)} (dynamic call)".I();
case 1: // void
return typeof(void).I();
case 2: // Object
return $"{objectArg.objectReferenceValue.GetType()} = {objectArg.objectReferenceValue}".I();
case 3: // int
return $"{typeof(int)} = {intArg.intValue}".I();
case 4: // float
return $"{typeof(float)} = {floatArg.floatValue}".I();
case 5: // string
return $"{typeof(string)} = {stringArg.stringValue}".I();
case 6: // bool
return $"{typeof(bool)} = {boolArg.boolValue}".I();
default:
return string.Empty;
}
Type GetEventType(in PersistentCall self)
{
var @object = self.callProperty.serializedObject.targetObject;
var names = self.propertyPathBase.Split('.');
var result = @object.GetType()
.GetField(names.FirstOrDefault(),
BindingFlags.NonPublic
| BindingFlags.Public
| BindingFlags.Instance)?.GetValue(@object);
while (!(result is UnityEventBase))
{
result = result.GetType()
.GetField(names.LastOrDefault(),
BindingFlags.NonPublic
| BindingFlags.Public
| BindingFlags.Instance)?.GetValue(result);
}
return result?.GetType();
}
}
}
/// <returns>A log of all transferred calls, empty if there were none.</returns>
private static string TransferPersistentCalls(
in Object source, in Object destination, in string srcEventName, in string dstEventName, bool removeOldCalls = false, in bool dryRun = true)
{
const string CallsPropertyPathFormat = "{0}.m_PersistentCalls.m_Calls";
var srcPropertyPath = string.Format(CallsPropertyPathFormat, srcEventName);
var dstPropertyPath = string.Format(CallsPropertyPathFormat, dstEventName);
var src = new SerializedObject(source);
var dst = new SerializedObject(destination);
var srcCalls = src.FindProperty(srcPropertyPath);
var dstCalls = dst.FindProperty(dstPropertyPath);
var dstCallsOriginalCount = dstCalls.arraySize;
var log = string.Empty;
for (var srcIndex = 0; srcIndex < srcCalls.arraySize; srcIndex++)
{
#region Init Source
var srcCallProperty = srcCalls.GetArrayElementAtIndex(srcIndex);
var srcCall = new PersistentCall(srcCallProperty, srcEventName);
var logLine = $"({srcIndex}) {srcCall}\n";
#endregion
if (!dryRun)
{
SerializedProperty dstCallProperty;
#region Check if the Call already Exists in the Destination
if (dstCallsOriginalCount > 0)
{
dstCallProperty = dstCalls.GetArrayElementAtIndex(srcIndex);
// If we are satisfied that the call is exactly the same, skip ahead.
if (SerializedProperty.DataEquals(srcCallProperty, dstCallProperty))
{
log += logLine;
continue;
}
}
#endregion
// Only unique properties beyond this point. Append with care.
#region Copy Properties from Source to Destination
var dstIndex = dstCallsOriginalCount + srcIndex;
dstCalls.InsertArrayElementAtIndex(dstIndex);
dstCallProperty = dstCalls.GetArrayElementAtIndex(dstIndex);
var dstCall = new PersistentCall(dstCallProperty, dstEventName);
PersistentCall.CloneValues(srcCall, dstCall);
#endregion
}
log += logLine.B();
}
log += $"\n(<b>Bold</b> = not already present in {dstEventName.I()}.)\n";
if (!dryRun)
{
if (removeOldCalls && (dstCalls.arraySize > 0))
{
srcCalls.ClearArray();
}
src.ApplyModifiedProperties();
if (source != destination)
{
dst.ApplyModifiedProperties();
}
}
return log;
}
The issue with removing the old calls came about when the source and destination were equal. Hence, this is the modification:
if (!dryRun)
{
if (removeOldCalls && (dstCalls.arraySize > 0))
{
srcCalls.ClearArray();
}
src.ApplyModifiedProperties();
if (source != destination)
{
dst.ApplyModifiedProperties();
}
}
If you want to use this you can use it like the following:
There are some limitations that I can see off hand but this should give people a huge boost to be able to modify this file and do everything they might want with it as well.
You could even modify this to examine each value in the UnityEvent or the method called to see if you wanted to do something in particular with it. Also important to note this is an editor only script and I would put this in your "Editor" directory.
Hopefully this helps someone!