Skip to content

Instantly share code, notes, and snippets.

@wesleywh
Created August 9, 2020 02:59
Show Gist options
  • Save wesleywh/1c56d880c0289371ea2dc47661a0cdaf to your computer and use it in GitHub Desktop.
Save wesleywh/1c56d880c0289371ea2dc47661a0cdaf to your computer and use it in GitHub Desktop.
This will copy UnityEvents from one UnityEvent to Another. It should copy all the settings exactly as is. Tested on Unity2018.4
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;
}
}
}
}
@wesleywh
Copy link
Author

wesleywh commented Oct 4, 2020

This is good stuff. I'll have to try this out when I get a chance. Thanks for this!

@Vivraan
Copy link

Vivraan commented Oct 5, 2020

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.

@Vivraan
Copy link

Vivraan commented Oct 5, 2020

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;
}

@Vivraan
Copy link

Vivraan commented Oct 7, 2020

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();
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment