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 Aug 9, 2020

If you want to use this you can use it like the following:

GameObject source = <You source Object>
GameObject destination = <Where to copy to>
CopyUnityEvents(source.GetComponent<MyOtherComp>, "OnPressActionInput", destination.GetComponent<MyNewComp>().OnPressActionInput);

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!

@Vivraan
Copy link

Vivraan commented Oct 3, 2020

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. 😉

@Vivraan
Copy link

Vivraan commented Oct 3, 2020

One place to look at is the m_Mode field I guess.

@Vivraan
Copy link

Vivraan commented Oct 4, 2020

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
        }

@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