Search Unity

AnimationUtility.SetAnimationEvents not working?

Discussion in 'Animation' started by Alismuffin, May 2, 2014.

  1. Alismuffin

    Alismuffin

    Joined:
    Oct 3, 2011
    Posts:
    100
    Hey I have been trying to copy some AnimationEvents from one AnimationClip to another. Everything works as expected - debugging value received from GetAnimationEvents for instance - except for the final part of the procedure; SetAnimationEvents.

    I want the change to be persistent as this is an editor utility script, so using AddEvent on a clip is not ideal.

    Here is the code:

    Code (csharp):
    1.  
    2.         AnimationClip sourceAnimClip = sourceObject as AnimationClip;
    3.  
    4.         AnimationClip targetAnimClip = targetObject as AnimationClip;
    5.  
    6.  
    7.         if (sourceAnimClip  targetAnimClip)
    8.         {
    9.  
    10.             AnimationUtility.SetAnimationEvents(targetAnimClip, null);
    11.            
    12.             AnimationEvent[] sourceAnimEvents = AnimationUtility.GetAnimationEvents(sourceAnimClip);
    13.  
    14.             AnimationUtility.SetAnimationEvents(targetAnimClip, sourceAnimEvents);
    15.         }
    Am I using it incorrectly?
     
  2. charlielee

    charlielee

    Joined:
    Apr 21, 2013
    Posts:
    8
    I got the same Problem....
     
  3. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    Does the destination clip is a clip created by you or it was imported, if it's an imported clip you cannot add event with this function.

    You have two options available for you.
    Duplicate the imported clip by selecting the clip in the project view and press ctrl+d, then you can use AnimationUtility.SetAnimationEvents
    You must be aware that once you duplicate a clip you loose the connection with the original asset, so if the original asset change the duplicated clip won't change.

    The other option is to use SerializedObject, and find the serialized property that define clip's event.
    See attached script
     

    Attached Files:

    Whatever560, Kekaku and Lohaz like this.
  4. charlielee

    charlielee

    Joined:
    Apr 21, 2013
    Posts:
    8
    Thanks your reply, But the Script you attached has bug.
    GetAnimationEvents is not point exact Event length.
    I added 3 event to Target Animation clip, but it show 2 events ....
    Would you check your code ?
     
  5. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    We did test the script before sending it to you and it was working, so please log a bug with your project and we will take a look.
     
  6. coconutfarmer

    coconutfarmer

    Joined:
    Dec 5, 2014
    Posts:
    2
    Hello, thanks for the script. I am using this to add events to a clip from AssetPostprocessor::OnPostprocessModel. I force a reimport of an animation fbx and the events get added (I can see them in the Fbx inspector/animations tab under events and the .meta file has the data as expected).

    Problem:
    When I press play and run the game, the new events are not triggered (even though they are in the clip). However if I make a (dummy) change (say enable LoopTime, then disable it again) in the Animations tab for that fbx and click "Apply", my asset post processor runs again, the events get added and this time when I play the game, the events fire.

    So it seems I need to do something to make the scene update with the changes to the animation clip (which happens if I click the apply button in the fbx import animations tab). Can you please tell how to mimic this 'apply' button behavior from script so my new events will fire in the game?
     
  7. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    in theory AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(yourClip)) should be enough to trigger a reimport and recreate the clip.

    Please log a bug with your project and we will take a look
     
  8. coconutfarmer

    coconutfarmer

    Joined:
    Dec 5, 2014
    Posts:
    2
    Thanks - I just found out that if I reimport an asset twice, it refreshes whatever cache and the events work when I play the game. Maybe this gives you a hint as to what might be causing this.
     
  9. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    Unfortunatelly no, so if you can please log a bug

    Best regards,
     
  10. RDeluxe

    RDeluxe

    Joined:
    Sep 29, 2013
    Posts:
    117
    Hi !

    I tested your script, and I have a weird problem :

    SerializedProperty clipAnimations = serializedObject.FindProperty("m_ClipAnimations");

    returns an empty array (an array nonetheless, just with a size of0) if I have only 1 clip animation in my FBX. The defaultClipAnimations is correct, with an array of 1 element.
    If I add a second one, the script is working correctly (clipAnimations will be a array with 2 elements).

    This behavior does not look logical to me at all.

    Edit : Well, I have no idea what's happening here ... dunno if it's an import problem
     
    Last edited: Dec 3, 2015
  11. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    unfortunatelly this how the model importer behave, as long as you don't change anything from the animation tab the clipAnimations member will stay empty.

    One thing you can do as a workaround is create an AssetPostProcessor script like this
    Code (CSharp):
    1. public class CreateAnimationClip : AssetPostprocessor
    2. {
    3.     void OnPreprocessAnimation()
    4.     {
    5.         var modelImporter = assetImporter as ModelImporter;
    6.         if(modelImporter.clipAnimations.Lenght == 0)
    7.         {
    8.             modelImporter.clipAnimations = modelImporter.defaultClipAnimations;
    9.         }
    10.     }
    11. }
    I should populate clipAnimations on the first import, then you're script should run without any issue
     
    TalentedYao likes this.
  12. antipodean

    antipodean

    Joined:
    Sep 20, 2016
    Posts:
    1
    Note to anyone using this script ... it looks like setting events accepts a time that's scaled by the clip length and must be 0-1. Getting events gets the actual time in seconds of the event. This is why charlielee previously in this thread was getting 2 events instead of three, because one was higher than 1.0.

    PS. Sorry for the thread necromancy, but hopefully this will save some people confusion in the future. Also, super thanks to Mechanim-Dev for creating this!
     
    omg_peter, sl1nk3_ubi and Kolgrima like this.
  13. LevonRavel

    LevonRavel

    Joined:
    Feb 26, 2014
    Posts:
    179
    Hey its really late but antipodean is correct but didnt post a fix so here it is, run the animationclip.length through this code. The result will return 0 - 1: usage is simple var adjustedTime = ClampTime(animationclip.length);

    Code (CSharp):
    1.  
    2.     private float ClampTime(float time)
    3.     {
    4.         if (time < 1) return time;
    5.         var rounded = time + 0.001f;
    6.         return time / rounded;
    7.     }
    8.  
     
    Last edited: Jan 6, 2019
  14. LevonRavel

    LevonRavel

    Joined:
    Feb 26, 2014
    Posts:
    179
    I am having an issue applying my class to the objectReferenceParameter everything else slots in fine without a hitch time interval, notification method, stringParameter all good imports and shows no issues. I just cant assign the object that needs to be notified. In my case its a class any object containing said class it notifies, yes this is possible go to the animation select a clip then drag a class to the objectReferenceParameter. It will notify any object containing said class, anycase heres my code.

    Code (CSharp):
    1.     static void ReImportClip(AnimationClip animClip, AnimationEvent[] events)
    2.     {
    3.         ModelImporter modelImporter = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(animClip)) as ModelImporter;
    4.         SerializedObject serializedObject = new SerializedObject(modelImporter);
    5.         SerializedProperty clipAnimations = serializedObject.FindProperty("m_ClipAnimations");
    6.  
    7.         for (int i = 0; i < clipAnimations.arraySize; i++)
    8.         {
    9.             AnimationClipInfoProperties clipInfoProperties = new AnimationClipInfoProperties(clipAnimations.GetArrayElementAtIndex(i));
    10.             if (clipInfoProperties.name == animClip.name)
    11.             {
    12.                 clipInfoProperties.SetEvents(events);
    13.                 serializedObject.ApplyModifiedProperties();
    14.                 AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(animClip));
    15.                 break;
    16.             }
    17.         }
    18.     }
    19.  
    20.     private void CompileEvent(Motion motion, string type)
    21.     {
    22.         AnimationEvent[] addEvent = new AnimationEvent[1];
    23.         AnimationEvent newEvent = new AnimationEvent();
    24.         newEvent.stringParameter = type;
    25.         newEvent.functionName = "AnimFinished";
    26.         var animHelper = Resources.Load("AnimHelper");
    27.         newEvent.objectReferenceParameter = animHelper; <--assigned here then reset later im a dummy
    28.         var time = ClampTime((motion as AnimationClip).length);
    29.         newEvent.time = time; // 1.55
    30.         newEvent.objectReferenceParameter = new AnimHelper(); <-- This was my issue
    31.         addEvent[0] = newEvent;
    32.         ReImportClip(motion as AnimationClip, addEvent);
    33.     }
    I tried many ways to add my object to CompileEvent Method .. var AnimHelper = Resources.Load("AnimHelper", typeof(AnimHelper)); (AnimHelper) Resouces.Load("AnimHelper"); Resources.Load("AnimHelper", typeof(object)), Resources.Load("AnimHelper") as AnimHelper. With many in between.. How can we assign the objectReferenceParameter with a class in the Editor I dragged a class from the project view to the object ref and it worked no issue on any object. Now just need a hand in class thank you ahead of time. (The above references refer to lines 26 and 27 <-- assignment) -Levon.
     
    Last edited: Jan 6, 2019
  15. Whatever560

    Whatever560

    Joined:
    Jan 5, 2016
    Posts:
    513
    I just modified the file to be able to use it directly in my code, replace the old DoAddEvent function by :


    Code (CSharp):
    1. /// <summary>
    2.     ///
    3.     /// </summary>
    4.     /// <param name="clip"></param>
    5.     public static void DoAddEventToAsset(AnimationClip clip)
    6.     {
    7.         DoAddEvent(clip, clip);
    8.     }
    9.  
    10.     [MenuItem("Mecanim/Copy events")]
    11.     static void DoAddEvent()
    12.     {
    13.         Object[] objs = Selection.GetFiltered(typeof(AnimationClip), SelectionMode.Unfiltered);
    14.         if (objs.Length < 2)
    15.             return;
    16.  
    17.         AnimationClip sourceAnimClip = objs[1] as AnimationClip;
    18.         AnimationClip targetAnimClip = objs[0] as AnimationClip;
    19.         DoAddEvent(targetAnimClip, sourceAnimClip);
    20.     }
    21.  
    22.     private static void DoAddEvent(AnimationClip targetAnimClip, AnimationClip sourceAnimClip)
    23.     {
    24.         if ((targetAnimClip.hideFlags & HideFlags.NotEditable) != 0)
    25.             DoAddEventImportedClip(sourceAnimClip, targetAnimClip);
    26.         else
    27.             DoAddEventClip(sourceAnimClip, targetAnimClip);
    28.     }
     
  16. omg_peter

    omg_peter

    Joined:
    Dec 7, 2020
    Posts:
    11
    Edit: The below still seems unreliable... I will see the .meta have the event added but the UI will not show the added events. I'm thinking there is a race condition between applying the modified properties and importing the asset.

    I was looking for a way to add events programmatically and stumbled on this thread. I had some trouble getting this to work but eventually got what I needed after combining some of the above solutions. The below code strips some of the unnecessary parts out of `AddEvent.cs` and adds code to convert the input events from absolute time to normalized time which is what the "AddClip" code is expecting. Hopefully this helps someone out in the future.

    Code (CSharp):
    1. using System.Linq;
    2. using UnityEditor;
    3. using UnityEngine;
    4.  
    5. public static class AnimationUtilities {
    6.  
    7.     //
    8.     // Public Methods
    9.     //
    10.  
    11.     public static void SetAnimationEvents (AnimationClip animationClip, AnimationEvent[] events) {
    12.         if ((animationClip.hideFlags & HideFlags.NotEditable) != 0) {
    13.             DoAddEventImportedClip(animationClip, events);
    14.         } else {
    15.             AnimationUtility.SetAnimationEvents(animationClip, events);
    16.             EditorUtility.SetDirty(animationClip);
    17.         }
    18.     }
    19.  
    20.     //
    21.     // Private Helpers
    22.     //
    23.  
    24.     // From https://forum.unity.com/threads/animationutility-setanimationevents-not-working.243810/#post-1825309
    25.  
    26.     private static void DoAddEventImportedClip (AnimationClip targetAnimClip, AnimationEvent[] newEvents) {
    27.         string assetPath = AssetDatabase.GetAssetPath(targetAnimClip);
    28.         ModelImporter modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter;
    29.         if (modelImporter == null)
    30.             return;
    31.  
    32.         SerializedObject serializedObject = new SerializedObject(modelImporter);
    33.         SerializedProperty clipAnimations = serializedObject.FindProperty("m_ClipAnimations");
    34.  
    35.         if (!clipAnimations.isArray)
    36.             return;
    37.  
    38.         // The below code is needing normalized time so convert the events to normalized time and then back
    39.         // https://forum.unity.com/threads/animationutility-setanimationevents-not-working.243810/#post-3003674
    40.         float[] originalTimes = newEvents.Select(x => x.time).ToArray();
    41.         foreach (var e in newEvents) {
    42.             e.time /= targetAnimClip.length;
    43.         }
    44.  
    45.         for (int i = 0; i < clipAnimations.arraySize; i++) {
    46.             AnimationClipInfoProperties clipInfoProperties = new AnimationClipInfoProperties(clipAnimations.GetArrayElementAtIndex(i));
    47.             if (clipInfoProperties.name == targetAnimClip.name) {
    48.                 clipInfoProperties.SetEvents(newEvents);
    49.                 _ = serializedObject.ApplyModifiedProperties();
    50.                 AssetDatabase.ImportAsset(assetPath);
    51.                 break;
    52.             }
    53.         }
    54.  
    55.         // Convert back to absolute times since the user may query the data outside this function
    56.         for (int i = 0; i < newEvents.Length; ++i) {
    57.             newEvents[i].time = originalTimes[i];
    58.         }
    59.     }
    60.  
    61.     private class AnimationClipInfoProperties {
    62.         private SerializedProperty m_Property;
    63.  
    64.         private SerializedProperty Get (string property) { return m_Property.FindPropertyRelative(property); }
    65.  
    66.         public AnimationClipInfoProperties (SerializedProperty prop) { m_Property = prop; }
    67.  
    68.         public string name { get { return Get("name").stringValue; } set { Get("name").stringValue = value; } }
    69.  
    70.         public void SetEvent (int index, AnimationEvent animationEvent) {
    71.             SerializedProperty events = Get("events");
    72.  
    73.             if (events != null && events.isArray) {
    74.                 if (index < events.arraySize) {
    75.                     events.GetArrayElementAtIndex(index).FindPropertyRelative("floatParameter").floatValue = animationEvent.floatParameter;
    76.                     events.GetArrayElementAtIndex(index).FindPropertyRelative("functionName").stringValue = animationEvent.functionName;
    77.                     events.GetArrayElementAtIndex(index).FindPropertyRelative("intParameter").intValue = animationEvent.intParameter;
    78.                     events.GetArrayElementAtIndex(index).FindPropertyRelative("objectReferenceParameter").objectReferenceValue = animationEvent.objectReferenceParameter;
    79.                     events.GetArrayElementAtIndex(index).FindPropertyRelative("data").stringValue = animationEvent.stringParameter;
    80.                     events.GetArrayElementAtIndex(index).FindPropertyRelative("time").floatValue = animationEvent.time;
    81.                 } else {
    82.                     Debug.LogWarning("Invalid Event Index");
    83.                 }
    84.             }
    85.         }
    86.  
    87.         public void SetEvents (AnimationEvent[] newEvents) {
    88.             SerializedProperty events = Get("events");
    89.  
    90.             if (events != null && events.isArray) {
    91.                 events.ClearArray();
    92.  
    93.                 foreach (AnimationEvent evt in newEvents) {
    94.                     events.InsertArrayElementAtIndex(events.arraySize);
    95.                     SetEvent(events.arraySize - 1, evt);
    96.                 }
    97.             }
    98.         }
    99.     }
    100. }
    101.  
     
    Last edited: Feb 25, 2022
    magique, h4voc, Grhyll and 1 other person like this.
  17. h4voc

    h4voc

    Joined:
    Mar 2, 2015
    Posts:
    12
    Sorry for bringing this up again, but it works like a charm and is excatly what i needed.