Search Unity

Animancer - Less Animator Controller, More Animator Control

Discussion in 'Assets and Asset Store' started by Kybernetik, Oct 8, 2018.

  1. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    FadeMode.FromStart will create a duplicate state if the previous state is already active so the new state can fade in from the start while the old one fades out from where it was. The Playing and Fading example explains it in more detail.
    Try pausing Unity and stepping through frame by frame to see what the AnimancerComponent Inspector looks like when it's in the wrong pose. That should help narrow down what's actually going on.
    I'm not sure what you mean by that. Can you try to explain it differently?
     
  2. Rusted_Games

    Rusted_Games

    Joined:
    Aug 29, 2010
    Posts:
    135
    Hello @Kybernetik what would be an alternative approach to have a Keyed State without using an enum?
    From the documentation it is clear the concept but I'm wrapping my head to find a solution and avoid having all the states in my character script to be able to register them in the awake, or to have to go back and add an entry to the enum when a new state is created

    Code (CSharp):
    1.  
    2. // Pseudo code
    3.     public KeyStateStruct[] StateKey;
    4.     public readonly StateMachine<int, ActorState>.WithDefault AnimationState = new StateMachine<int, ActorState>.WithDefault();
    5.     private void Awake()
    6.     {
    7.         foreach (var keyValuePair in KeyStateStruc)
    8.         {
    9.             AnimationState.Add(int, AnimState);
    10.         }
    11.     }
    12.  
     
    Last edited: May 16, 2022
  3. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    An int or string key would work, but that's much less safe.

    A ScriptableObject might work better. It would take a bit of extra setup to reference those ScriptableObjects where you add states and where you control the state machine because you'd be using serialized references rather than hard coding keys in the script, but it should give you much better flexibility. And if you need to sync them over a network you could give the ScriptableObject a unique ID and simply have them all add themselves to a dictionary in their OnEnable method.
     
  4. Rusted_Games

    Rusted_Games

    Joined:
    Aug 29, 2010
    Posts:
    135
    Thanks for the suggestion, indeed it is getting complex and I found though there's no "Add" method when using StateMachine<TKey,TState>.WithDefault
     
  5. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    That's because there is no
    StateMachine<TKey,TState>.WithDefault
    class. C# lets you access static members and nested classes through a child class and
    StateMachine<TKey,TState>
    inherits from
    StateMachine<TState>
    so you're actually just referring to the regular
    StateMachine<TState>.WithDefault
    .

    Give the attached file a go. I just wrote it quickly so I haven't tested it, but if it works fine I'll include it in the next version.
     

    Attached Files:

  6. Rusted_Games

    Rusted_Games

    Joined:
    Aug 29, 2010
    Posts:
    135
    It is working just fine, thanks a lot
     
  7. Ghosthowl

    Ghosthowl

    Joined:
    Feb 2, 2014
    Posts:
    228
    The website with the documentation is down for me.
     
  8. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Seems fine to me. Which page were you trying to load? Maybe there's a broken link or something.
     
  9. Ghosthowl

    Ghosthowl

    Joined:
    Feb 2, 2014
    Posts:
    228
    Strange. Seems your site is geo-blocked for me in California, USA. Fails to load due to DNS_PROBE_FINISHED_NXDOMAIN on my home internet, phone internet and hotspot (3 different providers). Using a VPN however allows me to load the site.
     
  10. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
  11. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    Is using anonymous functions / closures in animation events okay? I've got the following code to programmatically loop a section of my jump animation at a slower speed. Its working okay but animancer is giving warnings about setting duplicate identical callbacks. I dont see a great way to get a reference to the recently played jump state without using a closure, so I'm checking to see whether this pattern is okay before I jump through some hoops to avoid an anonymous function. Is the pattern perhaps a performance killer or something?

    Also it made me think about why the animancer event delegate Actions dont get anything useful as a parameter. Couldn't we perhaps get the current state, or even customizable data in the event parameters?

    Code (CSharp):
    1.            
    2. var state = _Animancer.Play(_Jump);
    3.             state.Speed = 1f;
    4.             state.Events.SetCallback("JumpLoopLowerLimit", AnimancerEvent.DummyCallback);
    5.             state.Events.OnEnd = () =>
    6.             {
    7.                 state.Speed = -0.2f;
    8.                 state.Events.AddCallback("JumpLoopLowerLimit", () =>
    9.                 {
    10.                     state.Speed = 0.2f;
    11.                 });
    12.             };
    13.  
     
  12. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Anonymous functions usually allocate some garbage and closures simply increase the amount of garbage so I prefer to avoid them or at least only use them on startup rather than every frame or every time an animation is played.

    You can access the current state during an event via AnimancerEvent.CurrentState to avoid the need for closures. Trying to support a custom event data parameter would effectively just achieve the same thing as closures so I don't think it would be useful.

    The warnings you're getting are because End Events are triggered every frame after the specified time has passed. Or since you're reversing the Speed, they are triggered when the state time is less than the end time. So every frame you're adding the same callback to that event.

    If you're using a transition, you should set up the events on startup rather than repeating the setup (and allocating more garbage) every time you play it. I think this would give you the logic you seem to be going for:
    Code (CSharp):
    1. private void Awake()
    2. {
    3.     _Jump.Speed = 1;// Could just set this in the Inspector.
    4.  
    5.     _Jump.SetCallback("JumpLoopLowerLimit", () =>
    6.     {
    7.         var state = AnimancerEvent.CurrentState;
    8.         if (state.Speed < 0)
    9.             state.Speed = 0.2f;
    10.     });
    11.  
    12.     // Add a second regular event since you don't actually want the behaviour of End Events.
    13.     _Jump.SetCallback("JumpLoopUpperLimit", () =>
    14.     {
    15.         var state = AnimancerEvent.CurrentState;
    16.         if (state.Speed > 0)
    17.             state.Speed = -0.2f;
    18.     });
    19. }
    20.  
    21. // Then just _Animancer.Play(_Jump); without anything else.
     
    Marsunpaisti likes this.
  13. cdr9042

    cdr9042

    Joined:
    Apr 22, 2018
    Posts:
    173
    Hello, I got this warning/error even though I don't use Animator Controllers.
    This is the script that the warning is pointing to

    Code (CSharp):
    1. using Animancer;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class GoalFinishRun : MonoBehaviour
    7. {
    8.     [SerializeField] AnimancerComponent animancer;
    9.     [SerializeField] AnimationClip openAnim;
    10.     [SerializeField] AnimationClip closeAnim;
    11.     [SerializeField] List<GameObject> goalCamera;
    12.  
    13.     private void Start()
    14.     {
    15.         Open();
    16.     }
    17.  
    18.     public void Open()
    19.     {
    20.         animancer.Play(openAnim);
    21.     }
    22.  
    23.     public void ActivateGoalCamera1()
    24.     {
    25.         goalCamera[0].SetActive(true);
    26.     }
    27.  
    28.     public void Close()
    29.     {
    30.         StartCoroutine(CoClose());
    31.     }
    32.  
    33.     IEnumerator CoClose()
    34.     {
    35.         animancer.Play(closeAnim);
    36.         yield return new WaitForSeconds(1f);
    37.         goalCamera[0].SetActive(false);
    38.         goalCamera[1].SetActive(true);
    39.     }
    40. }
    41.  
    I'm using Animancer 7.2, I didn't get this warning in Animancer 7.0. What's happenning?
     
  14. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    You must have one assigned to the Controller field on the Animator component.

    Also, if that wait for one second is meant to wait for the animation to finish, you could use
    yield return new animancer.Play(closeAnim);
    . Or you could use an End Event to avoid needing a coroutine at all:
    Code (CSharp):
    1.     var state = animancer.Play(closeAnim);
    2.     state.Events.OnEnd = () =>
    3.     {
    4.         goalCamera[0].SetActive(false);
    5.         goalCamera[1].SetActive(true);
    6.     };
     
    cdr9042 likes this.
  15. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    Great reply. Thanks! I think the AnimancerEvent.currenrState will also help me avoid closures in the future.
     
  16. cdr9042

    cdr9042

    Joined:
    Apr 22, 2018
    Posts:
    173
    But if I don't assign the animator then the component warn me that "An animator is required in order to play animations.", if I assign the animator then the game throws error.
     
  17. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Animancer requires an Animator component.

    That Animator should not have anything assigned to its Controller field.
     
  18. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    Can you help me understand the ClipTransitionSequence API? It seems to me that its purpose is to pack multiple animations under the hood behind a since ClipTransition interface, but the MaximumDuration only returns the duration of the first clip instead of the total of the sequence.
     
  19. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    I've made some changes to the ClipTransitionSequence class since the last update. Put this in it:
    Code (CSharp):
    1. /// <inheritdoc/>
    2. public override float Length
    3. {
    4.     get
    5.     {
    6.         var length = base.Length;
    7.         for (int i = 0; i < _Others.Length; i++)
    8.             length += _Others[i].Length;
    9.         return length;
    10.     }
    11. }
    12.  
    13. /// <inheritdoc/>
    14. public override float MaximumDuration
    15. {
    16.     get
    17.     {
    18.         var value = base.MaximumDuration;
    19.         for (int i = 0; i < _Others.Length; i++)
    20.             value += _Others[i].MaximumDuration;
    21.         return value;
    22.     }
    23. }
     
  20. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    I've been working on a networked project for a while now and that means I have to use animancer quite a bit "manually", creating states with CreateState() and so on. I've been facing an issue where I constantly get several states out of the same Transition/AnimancerState regardless of wrapping it in !Transition.IsPlaying or !AnimancerState.isPlaying or even !AnimancerComponent.States.TryGet(state), resulting in animancer throwing the error about having too many states for a clip. Can you guide me on how exactly I can call Play() multiple times for the same transition/state, or a function to achieve a similar result, without creating duplicate states if the transition is already playing?

    Basically I'm creating states manually with
    Code (CSharp):
    1.  
    2. protected virtual AnimancerState EnsureTransitionState()
    3.     {
    4.         if (TransitionState == null)
    5.         {
    6.             TransitionState = Transition.CreateStateAndApply(Entity._animancer);
    7.         }
    8.  
    9.         return TransitionState;
    10.     }
    11.  
    And only ever calling .Play() in a block wrapped by !TransitionState.IsPlaying
    Code (CSharp):
    1.  
    2. if (Runner.Stage == SimulationStages.Forward && !TransitionState.IsPlaying)
    3.         {
    4.             Entity._animancer.Play(TransitionState, Transition.FadeDuration, Transition.FadeMode);
    5.         }
    6.  
    I don't even know if I necessarily have to call .Play() or .CreateState(). My desired end result is that when I receive synchronization data from the server I want the current corresponding AnimancerStates normalizedTime etc. to be set to the received values if it exists, or if it doesnt exist, create one and then set its values. The SyncAnimancerState() below can be called multiple times in a row.

    My current code below if it helps illustrate what I'm trying to achieve here
    Code (CSharp):
    1.  
    2. protected virtual AnimancerState EnsureTransitionState()
    3.     {
    4.         if (TransitionState == null)
    5.         {
    6.             TransitionState = Transition.CreateStateAndApply(Entity._animancer);
    7.         }
    8.  
    9.         return TransitionState;
    10.     }
    11.  
    12. protected virtual void SyncAnimancerState(EntityStates stopAtState = 0, int depth = 2)
    13.     {
    14.         // Ensure AnimancerState exists for this transition
    15.         EnsureTransitionState();
    16.         if (!TransitionState.IsPlaying && Object.IsProxy && Runner.Stage == SimulationStages.Forward)
    17.         {
    18.             Entity._animancer.Play(TransitionState, Transition.FadeDuration, Transition.FadeMode);
    19.         }
    20.  
    21.         // Host saves params for others to sync to
    22.         if (TransitionState != null && !Object.IsProxy && Runner.Stage == SimulationStages.Forward)
    23.         {
    24.             AnimancerNormalizedTime = TransitionState.NormalizedTime;
    25.             AnimancerWeight = TransitionState.Weight;
    26.         }
    27.  
    28.         // Everyone except host syncs to hosts params
    29.         if (TransitionState != null && !Object.HasStateAuthority)
    30.         {
    31.             TransitionState.NormalizedTime = AnimancerNormalizedTime;
    32.             TransitionState.Weight = AnimancerWeight;
    33.         }
    34.     }
     
    Last edited: May 31, 2022
  21. MoonBeam91

    MoonBeam91

    Joined:
    Jan 28, 2021
    Posts:
    35
    Bit complex , hope you can understand it.

    I am making a character where you can switch between a dragon and a human. I am using scriptable objects with animancer. The scriptable objects have user defined Boolean values which will trigger respective animations in the scriptable object, when they match the Booleans given by the user.
    Now I have implemented an Enum based interruption manager, where if I press a key 'V' the priority of the animation changes , hence triggering the intended animation.

    But I am facing a problem where I need to press the V twice once to change the priority and the second time to trigger the animation. This only happens when I play the game starting with the human character and not the dragon. If I play ,starting with the dragon , then pressing V once triggers the animation, once I switch back to human and then back to dragon the problem persists. I am using the same input manager for the characters
     
  22. MoonBeam91

    MoonBeam91

    Joined:
    Jan 28, 2021
    Posts:
    35
    ?
     
  23. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    @Marsunpaisti The Playing and Fading example explains why the duplicate states get created. If you don't want them, you can disable the transition's Start Time toggle.

    @MoonBeam91 That sounds like it's probably a problem with your script logic. Try putting some Debug.Logs around the place to narrow down the place where it's actually going wrong.
     
  24. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    I was more puzzled about the fact why IsPlaying would return false when there was an identical state already. But anyways I fixed that problem and realized I had two overlapping problems. I'm syncing the animation states weight, and normalizedtime, and IsPlaying to the clients, and I noticed that the state seems to stop playing if I write values to AnimancerState.NormalizedTime or AnimancerState.Time, what side effects do these two have under the hood? Can I somehow overwrite the Time of a currently playing animation while letting it proceed playing from the new time afterwards?
    I even did a test by just setting
    Code (CSharp):
    1. State.NormalizedTime = State.NormalizedTime
    and even this causes the animation to stop, so clearly there is some side effect internally, right?
     
    Last edited: Jun 1, 2022
  25. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Setting the time will leave it at that value after the next animation update, so if you're setting it every frame it will never advance on its own. I generally find that behaviour unhelpful, but it's build into the Playables API and there's nothing we can do about it. The Mixer Synchronization system gets around it by instead setting the speed of every state every frame based on the current delta time so they will reach the target value in one update.
     
  26. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    I'm calling Animancer.Evaluate() immediately after though. I thought that was supposed to move them forward by X time then?
     
  27. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    It would, except that you've just set the state's time so Evaluate just counts as the next animation update and will leave the time at the value you set.
     
  28. Ghosthowl

    Ghosthowl

    Joined:
    Feb 2, 2014
    Posts:
    228
    Thanks! I ended up figuring it out. It seems your website does not work for me using Comcast/Xfinity or Google (8.8.8.8 / 8.8.4.4) DNS servers. Since my phone is Android, it uses Google as well as my home connection. I was able to bypass this by using Cloudflares DNS servers (1.1.1.1 / 1.0.0.1) instead in the IPv4 settings of my network connection on Windows.
     
  29. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    This was extremely helpful. I'm now experimenting with calling Animancer.Evaluate(0) as a dummy on ticks where I manually modify the time, and it seems to be working.
     
  30. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    Is there some decent way to access whatever blend-related variables Animancer is using under the hood? I managed to properly network the time of any AnimancerStates being played, but since Photon Fusion requires rolling the state back and re-simulating forward a bit, I end up having the weights that are being blended getting "accelerated" and off-sync, due to many calls to Animancer.Evaluate(). Even though I call AnimancerState.SetWeight() to reset it to a weight I was at earlier, and then re-simulate with Evaluate() back to the most recent tick, it seems Animancer loses some internal state that was controlling the blend in the process. Any ideas how I could set any blend-related variables to a certain point in time? I can notice there's a FadeSpeed and TargetWeight, but Animancer seems to stop blending much before the weight reaches 0 if there's calls to SetWeight in between?
     
  31. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    SetWeight doesn't directly do anything to a fade in progress, but if you set it to the fade's target weight then call Evaluate, it would see that the fade is done and end it so it wouldn't automatically start again if you set the weight to something else.

    Have you considered adding the current frame's delta time to the synced state time value? That might let you avoid extra calls to Evaluate (which are bad for performance because they evaluate the whole graph and immediately apply the output to the model).
     
  32. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    Mostly the weight is rolled backwards towards 0. It just seems like the AnimancerState stops approaching the target weight earlier than it is reached for some reason, and I was suspecting that there is some internal timer used to lerp the weight towards 1. The whole Fusion paradigm of client prediction and re-simulation means that once "true" server confirmed data for a previously simulated tick arrives, the client resets all values back to that server-confirmed data and re-simulates back to where they were. Thats why I'm setting the time / weight backwards to begin with, and then calling evaluate several times to simulate it back forward. And I'm guessing thats what perhaps causes animancer to lose track of where it was fading towards because from the perspective of animancer, the time is advancing 'faster'. Or is the fading just internally the equivalent of
    Code (CSharp):
    1. if (weight < targetWeight) weight = Mathf.MoveTowards(weight, targetWeight, fadeSpeed * deltaTime)
    ?
    Perhaps you might know a better approach for "setting" a state to some given moment in time? Is there perhaps a possibility of officially supporting such API functions, to grab out a networkable struct or such, of the data the state contains at any given moment, and a function to re-apply that data at will. In fact, the developers of Photon Fusion have said that if animancer starts supporting networking they would even endorse animancer.
    I believe all that is needed would be the ability to store and re-apply the state (current weight, time, and fading progress etc.) of a given AnimancerState.
     
    Last edited: Jun 3, 2022
  33. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    It's
    if (weight != target)
    since the same code is used for fading out too but yeah, that's pretty much it. That code is in the AnimancerNode class since it's used by both states and layers.

    Making a simple serializable struct to store the current properties of a single state would be easy and I'd be happy to include it in Animancer if it's actually helpful. The only challenge would be coming up with a better name than AnimancerStateState.

    But making a serializable system to roll-back and replay an entire AnimancerComponent for you would be far more complicated than that. I made a GitHub Issue to discuss it further since there are a lot of things to consider which we don't need to clutter up this thread with.
     
  34. Gerark87

    Gerark87

    Joined:
    Feb 2, 2013
    Posts:
    9
    Hello, I updated my unity project from 2020.x to 2021.3.4f1 LTS

    Now I'm having an issue with Animancer Lite. I get a MissingMethodException.

    Here's the full exception log:

    Code (CSharp):
    1. MissingMethodException: Method not found: UnityEngine.Animations.AnimationLayerMixerPlayable UnityEngine.Animations.AnimationLayerMixerPlayable.Create(UnityEngine.Playables.PlayableGraph,int)
    2. Animancer.AnimancerPlayable.OnPlayableCreate (UnityEngine.Playables.Playable playable) (at <c31d644a864a40fdb995cf35388927e0>:0)
    3. UnityEngine.Playables.ScriptPlayable`1:Create(PlayableGraph, AnimancerPlayable, Int32)
    4. Animancer.AnimancerPlayable:Create()
    5. Animancer.AnimancerComponent:InitializePlayable() (at Assets/Plugins/Animancer/AnimancerComponent.cs:311)
    6. Animancer.AnimancerComponent:get_Playable() (at Assets/Plugins/Animancer/AnimancerComponent.cs:77)
    7. Animancer.AnimancerComponent:get_States() (at Assets/Plugins/Animancer/AnimancerComponent.cs:88)
    8. Animancer.NamedAnimancerComponent:Awake() (at Assets/Plugins/Animancer/NamedAnimancerComponent.cs:130)
    I get this exception with the basic examples too. Is the 2021.3.4f1 LTS supported? The latest change log mention only: "Added support for Unity 2021.2 (Beta)" which might be the reason why this is still an issue.
     
  35. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    If you change Unity versions with Animancer Lite, you need to delete it and re-download it in the new version so the Package Manager can give you the correct one for that Unity version. This is because API changes between Unity versions mean that Animancer Lite's DLLs need to be recompiled for a specific Unity version, which doesn't happen with Animancer Pro since Unity compiles all the scripts itself.
     
    Gerark87 likes this.
  36. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    It seems my animancer events are firing for every tick that the state is playing past the Event start time. Because I'm networking stuff I'm never actually re-creating the state, its just being played again from the start. Under the hood, does Animancer normally remove the event from the state once it has been executed, trusting that it will be added again when Play() gets called? And when I'm reusing my existing state it never happens? Do note, that I'm not talking about end events which should behave this way according to documentation, but just regular events

    Basically here I create a state for the transition and only ever re-use it when calling Play
    Code (CSharp):
    1.        
    2. protected virtual AnimancerState EnsureTransitionState()
    3.         {
    4.             if (TransitionState == null)
    5.             {
    6.                 TransitionState = Entity.Animancer.Play(Transition);
    7.                 AddSoundEventCallbacks(TransitionState);
    8.             }
    9.  
    10.             return TransitionState;
    11.         }
    12.  
    13. public void AddSoundEventCallbacks(AnimancerState state)
    14. {
    15.     foreach (AnimationSoundEvent soundEvent in _soundEvents)
    16.         try
    17.         {
    18.             state.Events.SetCallback(soundEvent._animancerEventName, () => PlaySoundEvent(soundEvent));
    19.             Debug.Log("Registered sound event " + soundEvent._animancerEventName + " " + soundEvent._soundType);
    20.         }
    21.         catch (ArgumentException _)
    22.         {
    23.             Debug.LogWarning("No animancer event " + soundEvent._animancerEventName + " found for sound event " + soundEvent._soundType + " in " +
    24.                              ToString());
    25.         }
    26. }
    27.  
    28.  
    I'm playing it like this
    Code (CSharp):
    1.        
    2. public virtual void PlayTransition()
    3.         {
    4.             TransitionState = Entity.Animancer.Play(Transition, Transition.FadeDuration, FadeMode.FixedSpeed);
    5.         }
    6.  
    Currently I have a workaround by just checking whether the events effect was ran for "this play" of the state, but on Animancer level its calling it every frame after the event time passes.
     
  37. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    End Events are triggered every frame after their time has passed, but regular Animancer Events are not as described here. Aside from a looping animation that completes one loop every frame, the only way I can think of that an event would be called every frame is if you're continually resetting the Time property to 0 then calling MoveTime, because that triggers all events and root motion between the old and new time.

    Events are cleared when you Play or Stop an animation as described here. Triggering an event or doesn't remove it, nor does changing the animation's time.

    As mentioned in that second link: when playing a transition, "Accessing [the state's] Events will refer directly to the AnimancerEvent.Sequence owned by the Transition so any modifications you make will be retained when you play it again. This means modifications should generally only be performed once on startup rather than being repeated every time you play it." I don't think that's the source of your problem, just a general recommendation.
     
  38. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    I noticed it was indeed related to my syncing, but the weird thing is that on the host that I was testing on, being the source of truth, the synced variables are pretty much the same as they were originally i.e.

    Code (CSharp):
    1.        
    2. public void ApplyToState(AnimancerState transitionState)
    3.         {
    4.             Debug.Log($"{transitionState} before applying sync T: {transitionState.Time} W: {transitionState.Weight}");
    5.  
    6.             transitionState.IsPlaying = AnimancerIsPlaying;
    7.             transitionState.MoveTime(AnimancerTime, false);
    8.             transitionState.SetWeight(AnimancerWeight);
    9.             transitionState.TargetWeight = AnimancerTargetWeight;
    10.             transitionState.FadeSpeed = AnimancerFadeSpeed;
    11.  
    12.             Debug.Log($"{transitionState} after applying sync T: {transitionState.Time} W: {transitionState.Weight}");
    13.         }
    14.  
    upload_2022-6-9_17-35-14.png

    Apart from small rounding it shouldnt actually move the time so much that it would pass over the event multiple times. Or does MoveTime() make the time wrap around from the end, all the way to the same position, triggering the event every time?
     
  39. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    It wouldn't make sense for it to wrap around like that and doing so would give ridiculous root motion so I doubt that's what's happening.

    Maybe try putting your logs in the MoveTime method itself and actually call GetTime directly on the Playable.
     
  40. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    I actually noticed that its not the MoveTime causing it, even if I just set .Time it'll still repeat. Another interesting thing is, when I log the state time in the event, its actually progressing in time, and getting called even before the designated time for the event has passed. Logically if my code was causing the time to go back and forth over the event it'd be logging values approximately in the area of the event time, right?
    upload_2022-6-9_17-43-30.png upload_2022-6-9_17-45-4.png
     

    Attached Files:

  41. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    If your event was at less than 0.05s and you were resetting Time to 0 every frame then putting it forward with MoveTime, I'd expect to see those logs since MoveTime would be going over the event time every frame. But my understanding is that you're not doing that.

    Are you sure it's not something simple like having another copy of your character in the scene with something not set up correctly?

    Otherwise, if you can't narrow it down any further feel free to send a minimal reproduction project to animancer@kybernetik.com.au and I'll take a look at it.
     
  42. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    I did some non-networked experimenting and narrowed it down to the part where I save the state at the end of the update and re-apply the saved state at the start of the next update (Imitating how my networked system works).
    First it somehow worked even though I used the same struct, then I realized that networked variables get rounded to a precision like 0.001f or so, so I went and imitated this in my test bench. When I added a rounding to 4 decimals of the Time variable when saving the struct, it breaks the event like in my previous posts. Then I did some tests and I realized that if the saved & re-applied time is BEHIND the originally saved time by even 0.0000001f due to rounding, it will for some reason cause the events to re-trigger. If I instead rounded to 4 decimals with Mathf.Ceil it wont break the event.
    Now the question is why moving back by even 0.000001f in time between updates would cause the event to retrigger? It actually seems to behave exactly as if MoveTime "wraps around" if the new time is behind the current time, causing an event retrigger.

    Code (CSharp):
    1.  
    2. private void Update()
    3. {
    4.     if (TransitionState == null)
    5.     {
    6.         Debug.Log("Playing transition");
    7.         TransitionState = _animancer.Play(_transition, _transition.FadeDuration, FadeMode.FixedSpeed);
    8.         TransitionState.Events.AddCallback("SwingSound", () => { Debug.Log("Time when playing test event: " + AnimancerEvent.CurrentState.Time); });
    9.         SavedData = new AnimancerSavedState(TransitionState);
    10.     }
    11.     else
    12.     {
    13.         SavedData.ApplyToState(TransitionState);
    14.         _animancer.Evaluate(0);
    15.         _animancer.Evaluate(0.005f);
    16.         SavedData = new AnimancerSavedState(TransitionState);
    17.     }
    18. }
    19.  
    20. public struct AnimancerSavedState
    21. {
    22.     public bool AnimancerIsPlaying { get; set; }
    23.     public float AnimancerTime { get; set; }
    24.     public float AnimancerWeight { get; set; }
    25.     public float AnimancerTargetWeight { get; set; }
    26.     public float AnimancerFadeSpeed { get; set; }
    27.  
    28.     public AnimancerSavedState(AnimancerState transitionState)
    29.     {
    30.         AnimancerIsPlaying = transitionState.IsPlaying;
    31.         AnimancerTime = transitionState.Time;
    32.         AnimancerTime = Mathf.Round(AnimancerTime * 1000f) / 1000f; // This breaks the event
    33.         //AnimancerTime = 0.00001f + Mathf.Ceil(AnimancerTime * 1000f) / 1000f; // If the stored time never gets rounded backwards the event wont re-trigger
    34.         AnimancerWeight = transitionState.Weight;
    35.         AnimancerTargetWeight = transitionState.TargetWeight;
    36.         AnimancerFadeSpeed = transitionState.FadeSpeed;
    37.     }
    38.  
    39.     public void ApplyToState(AnimancerState transitionState)
    40.     {
    41.         transitionState.IsPlaying = AnimancerIsPlaying;
    42.         transitionState.MoveTime(AnimancerTime, false);
    43.         transitionState.SetWeight(AnimancerWeight);
    44.         transitionState.TargetWeight = AnimancerTargetWeight;
    45.         transitionState.FadeSpeed = AnimancerFadeSpeed;
    46.     }
    47. }
    48.  
     
  43. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    I still haven't been able to replicate the issue on my end. Can you send me a minimal reproduction project?
     
  44. Marsunpaisti

    Marsunpaisti

    Joined:
    May 3, 2018
    Posts:
    22
    The above script should basically be it. In addition to the script I just had a prefab with some random clipTransition with an event, animancer and that script component.
    And in Awake, the _animancer.Playable.DirectorUpdateMode was set to manual if that wasnt apparent from the way its being updated
     
  45. JakartaIlI

    JakartaIlI

    Joined:
    Feb 6, 2015
    Posts:
    30
    I didn't find the answer to the question, so I'll ask here.
    I need to limit the number of frames in the animation when moving. (13 frames per second)
    I used Fine Control script, but with a small update of frames, root motion moves jerkily (teleports) which breaks the Character controller and its physics.
    Is there a way to update the Root Motion every frame, but limit the animation display on the character.
     
  46. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    To get regular root motion you would have to let Animancer play normally. Then you could write a script that uses LateUpdate:
    • On the first LateUpdate, store the rotations of all bones.
    • On each LateUpdate after that, set all the bones back to their stored rotations so you never see them in the newly animated pose.
    • On frame 13, store the rotations again instead of resetting them.
    If you can figure out how to use Stepped Keyframes, that would likely also work. I've never tried it, but this thread seems to have some ideas in it.
     
  47. RiccardoAxed

    RiccardoAxed

    Joined:
    Aug 29, 2017
    Posts:
    119
    Hi, I would just ask if Animancer allows you to add animations clips dinamically, at runtime.

    The typical scenario I've in mind - I could work on soon - would be an AR app where the main character is progressively enriched with new animations, that I would put in assetbundles, download them and assign the new clips to my character at runtime. Is it somehow possibile with Animancer?

    Great asset anyway!

    Riccardo.
     
  48. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Playing animations dynamically at runtime is basically the entire purpose of Animancer. You don't need to do anything to set up animations beforehand and Animancer doesn't know or care how your animations were loaded. If you have a valid AnimationClip (Humanoid or Generic) you can just call animancer.Play(clip).
     
    RiccardoAxed likes this.
  49. RiccardoAxed

    RiccardoAxed

    Joined:
    Aug 29, 2017
    Posts:
    119
    That's great, I thought I somehow had to "add" the animation clip to the Animancer/Animator component in advance to be able to play it, happy to hear the process is completely dynamic instead.

    Thank you for your answer, I'll have a go with Animancer for sure :)
     
  50. alexandergikalo

    alexandergikalo

    Joined:
    Oct 24, 2019
    Posts:
    19
    Hi.
    Does anyone know how to create delay void animation with variable length and play it by _animancer.Play()?
    I need to add growing timeout before start some animations and don't know how to do it at Animancer((
    The same fature at DoTween is "mySequence.AppendInterval(interval)"