Search Unity

Animator : Update Physics & Interpolation

Discussion in 'Animation' started by mcapdegelle, Aug 15, 2019.

  1. mcapdegelle

    mcapdegelle

    Joined:
    Nov 7, 2014
    Posts:
    30
    Hey guys, I'm having a hard time with mecanim.

    I'm trying to do a sword combat system. I want it to be precise & deterministic even when the framerate is not stable. I want to do my collision detection & velocity update (based on the root motion data) in the fixed update.

    Now the thing is a can set the animator update mode on "Animate Physics", and it would solve a lot of my problems, but the bones only update at the fixed timestep frequency. Why? It should do a full simulation in the FixedUpdate & Interpolate in the normal Update based on the remaining time right? (Just like rigid body intepolation) I don't want my character to be animated at 50Hz when the framerate is 144Hz, and I can't do slow-mo modifying the TimeScale, it's even worse.

    Sooo right now, I use the animator in the Normal update mode to get smooth animation. I extract root motion & sword position sampling each attack animation clip on startup, so I can use it in the FixedUpdate. It kinda works, but It's not robust : The data will be wrong if I do IK...

    Do you guys have other solutions?
     
  2. Quadropups

    Quadropups

    Joined:
    May 23, 2017
    Posts:
    18
    I'd like to know the answer to this too. This feature would be indeed really useful.
     
  3. IgnisIncendio

    IgnisIncendio

    Joined:
    Aug 16, 2017
    Posts:
    223
  4. Fressbrett

    Fressbrett

    Joined:
    Apr 11, 2018
    Posts:
    97
    I am currently meeting the same problem, and found a similar thread that takes the same concepts a bit further. The idea is to interpolate between FixedUpdates during the Update. It is an ugly solution though.

    I am also thinking about a different solution, which has its own drawbacks, but might just work:
    1. Keep your character animated at the normal update (UpdateMode.Normal)
    2. Move all of your colliders outside of your character hierarchy
    3. Create a second animator on a parent object of your colliders (collider animator) with the same state-setup as your character animator. This new collider animator runs on UpdateMode.AnimatePhysics and contains animations for your colliders, for example a collider at the fist of your character moving forward during a punch
    4. Use a custom script to sync both animators, like this, so that whenever a state is entered in your character animator, it is also entered within your collider animator.
    5. In case the GameObject the character animator sits on was moved by your movement script, you need to make sure to also move the new collider animator (during FixedUpdate!). In case some common parent was moved anyways, you can skip this step.
    This solution is a bit awkward since there is no easy way to preview your character animations and your collider animations at once, only during runtime, making it tedious to set up your collider animations. But once that's done, in theory everything should run as expected - a smooth character and robust physics collisions.


    Edit:
    Out of curiosity I asked chatGPT from OpenAI how one would create a script syncing pairs of states within two animators. After some back and forth I managed to nudge chatGPT to output this, maybe its useful to some:

    Code (CSharp):
    1. /// <summary>
    2. /// The AnimatorStateSynchronizer class is a script that synchronizes the states of two Animator components by setting a trigger parameter on one Animator (the TargetAnimator) when a specific state is entered on the other Animator (the TriggerAnimator). The TargetAnimator has a corresponding state that is set to transition based on the trigger parameter. The states and trigger parameter to be synchronized are specified in an array of TriggerTargetStatePair structs. The OnAnimatorMove function is called when the state of the TriggerAnimator changes, and it checks if the current state matches any of the states in the TriggerTargetStatePairs array. If a match is found, the SetTrigger function is called on the TargetAnimator with the corresponding trigger parameter, causing the TargetAnimator to transition to the corresponding state.
    3. /// </summary>
    4. public class AnimatorStateSynchronizer : MonoBehaviour
    5. {
    6.     public Animator TargetAnimator;
    7.     public Animator TriggerAnimator;
    8.     public TriggerTargetStatePair[] TriggerTargetStatePairs;
    9.  
    10.     [System.Serializable]
    11.     public struct TriggerTargetStatePair
    12.     {
    13.         public string TriggerAnimatorStateName;
    14.         public string TargetAnimatorTriggerParameterName;
    15.         public int TriggerAnimatorStateHash;
    16.         public int TargetAnimatorTriggerParameterHash;
    17.         public bool IsTargetAnimatorTriggerSet;
    18.     }
    19.  
    20.     private void Awake()
    21.     {
    22.         for (int i = 0; i < TriggerTargetStatePairs.Length; i++)
    23.         {
    24.             TriggerTargetStatePair triggerTargetStatePair = TriggerTargetStatePairs[i];
    25.             triggerTargetStatePair.TriggerAnimatorStateHash = Animator.StringToHash(triggerTargetStatePair.TriggerAnimatorStateName);
    26.             triggerTargetStatePair.TargetAnimatorTriggerParameterHash = Animator.StringToHash(triggerTargetStatePair.TargetAnimatorTriggerParameterName);
    27.             triggerTargetStatePair.IsTargetAnimatorTriggerSet = false;
    28.             TriggerTargetStatePairs[i] = triggerTargetStatePair;
    29.         }
    30.     }
    31.  
    32.     private void OnAnimatorMove()
    33.     {
    34.         for (int i = 0; i < TriggerTargetStatePairs.Length; i++)
    35.         {
    36.             TriggerTargetStatePair triggerTargetStatePair = TriggerTargetStatePairs[i];
    37.             if (TriggerAnimator.GetCurrentAnimatorStateInfo(0).shortNameHash == triggerTargetStatePair.TriggerAnimatorStateHash && !triggerTargetStatePair.IsTargetAnimatorTriggerSet)
    38.             {
    39.                 TargetAnimator.SetTrigger(triggerTargetStatePair.TargetAnimatorTriggerParameterHash);
    40.                 triggerTargetStatePair.IsTargetAnimatorTriggerSet = true;
    41.                 TriggerTargetStatePairs[i] = triggerTargetStatePair;
    42.             }
    43.         }
    44.     }
    45. }
    It requires your TargetAnimator (the animator set to Physics Update animating your colliders) to have Triggers that transition into your desired state - to make it easy, you could just transition from AnyState using these triggers into your animations.
     
    Last edited: Dec 17, 2022
  5. Program-Gamer

    Program-Gamer

    Joined:
    Nov 16, 2018
    Posts:
    15
    Bumping this because I've encountered the same problem and I'd like a less hacky solution than what was proposed, as well as something that wasn't generated by an at-best-unreliable language model, thanks.
     
  6. mcapdegelle

    mcapdegelle

    Joined:
    Nov 7, 2014
    Posts:
    30
    It's a long time since I tried to resolve this problem, and I don't have access to the source code anymore. I'll try to explained what I did that I can remember.

    I couldn't find a good way to do this using mecanim, so I switched to the playable graph API (which I liked more in the end). Here is some pseudo code :

    Code (CSharp):
    1. public class PlayableGraphState
    2. {
    3.     // serializes the whole graph storing each node time & weights
    4.     public void Serialize(PlayableGraph graph) { }
    5.  
    6.     // sets each node of the graph to the stored values
    7.     public void Deserialize(PlayableGraph graph) { }
    8.  
    9.     // returns a new state with each time & weights interpolated
    10.     public PlayableGraphState Interpolate(PlayableGraphState target, float t) { }
    11.  
    12.     public void CopyTo(PlayableGraphState state) { }
    13. }
    14.  
    15. public class AnimatedCharacter : MonoBehaviour
    16. {
    17.     private PlayableGraph _graph;
    18.     private PlayableGraphState _prevState;
    19.     private PlayableGraphState _currState;
    20.  
    21.     private void OnEnable()
    22.     {
    23.         _prevState = new PlayableGraphState();
    24.         _currState = new PlayableGraphState();
    25.         _graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
    26.     }
    27.  
    28.     private void FixedUpdate()
    29.     {
    30.         _currState.Deserialize(_graph);
    31.         _graph.evaluate(0);
    32.  
    33.         // pre-anim gameplay updates
    34.  
    35.         _graph.evaluate(Time.fixedDeltaTime);
    36.  
    37.         // post-anim gameplay updates
    38.         // you can use the root motion from the animator
    39.  
    40.         _currState.CopyTo(_prevState);
    41.         _currState.Serialize(_graph);
    42.     }
    43.  
    44.     private void Update()
    45.     {
    46.         var alpha = (Time.time - Time.fixedTime) / Time.fixedDeltaTime;
    47.         var interpolatedState = _prevState.Interpolate(_currState, alpha);
    48.         interpolatedState.Deserialize(_graph);
    49.         _graph.evaluate(0);
    50.     }
    51. }
    The PlayableGraphState class should be properly optimized... It's a lot of work but I didn't found a better way to do it :/
    Hope it helps!
     
    Last edited: Sep 28, 2023
    Program-Gamer likes this.
  7. Program-Gamer

    Program-Gamer

    Joined:
    Nov 16, 2018
    Posts:
    15
    It ended up helping a lot! I figured if I know the structure of my graph in advance, I can make the serialization/deserialization process super efficient by just using a struct as the graph state. Everything else that goes on in the Update and FixedUpdate methods is pretty much exactly as you said, and it works as advertised.

    Here's what I landed on for my GraphState class:

    Code (CSharp):
    1. struct GraphState
    2. {
    3.     public double mixerTime;
    4.  
    5.     public float input0Weight;
    6.  
    7.     public float input1Weight;
    8.  
    9.     public double input0Time;
    10.  
    11.     public double input1Time;
    12.  
    13.  
    14.     public void SerializeFrom(PlayableGraph graph)
    15.     {
    16.         var mixer = graph.GetRootPlayable(0);
    17.         mixerTime = mixer.GetTime();
    18.         input0Weight = mixer.GetInputWeight(0);
    19.         input1Weight = mixer.GetInputWeight(1);
    20.         input0Time = mixer.GetInput(0).GetTime();
    21.         input1Time = mixer.GetInput(1).GetTime();
    22.     }
    23.  
    24.     public readonly void DeserializeTo(PlayableGraph graph)
    25.     {
    26.         var mixer = graph.GetRootPlayable(0);
    27.         mixer.SetTime(mixerTime);
    28.         mixer.SetInputWeight(0, input0Weight);
    29.         mixer.SetInputWeight(1, input1Weight);
    30.         mixer.GetInput(0).SetTime(input0Time);
    31.         mixer.GetInput(1).SetTime(input1Time);
    32.     }
    33.  
    34.     public static GraphState Lerp(GraphState a, GraphState b, float t)
    35.     {
    36.         return new GraphState()
    37.         {
    38.             mixerTime = Mathf.Lerp((float)a.mixerTime, (float)b.mixerTime, t),
    39.             input0Weight = Mathf.Lerp(a.input0Weight, b.input0Weight, t),
    40.             input1Weight = Mathf.Lerp(a.input1Weight, b.input1Weight, t),
    41.             input0Time = Mathf.Lerp((float)a.input0Time, (float)b.input0Time, t),
    42.             input1Time = Mathf.Lerp((float)a.input1Time, (float)b.input1Time, t),
    43.         };
    44.     }
    45. }
    Now, the more complicated part is going to be redoing all my player code to use playables instead of mecanim, but with this test turning out conclusive, I'm confident it'll do the job.

    Thanks a bunch!

    Edit: don't do what I did and cast your playables' time to floats. Just make your own lerp that takes doubles instead of floats.
     
    Last edited: Oct 17, 2023
  8. Wolfos

    Wolfos

    Joined:
    Mar 17, 2011
    Posts:
    951
    With Mecanim, set update mode to "normal" and add this to sync it during the physics timestep:

    Code (CSharp):
    1. public class AnimationSync : MonoBehaviour
    2. {
    3.     [SerializeField] private Animator animator;
    4.  
    5.     private float _lastUpdateTime;
    6.     private float _updateDelta;
    7.  
    8.     private void FixedUpdate()
    9.     {
    10.         var delta = Time.time - _lastUpdateTime;
    11.         animator.Update(delta);
    12.    
    13.         _updateDelta += delta;
    14.         _lastUpdateTime = Time.time;
    15.     }
    16.  
    17.     private void Update()
    18.     {
    19.         // Subtract any previous updates we did during FixedUpdate
    20.         animator.Update(-_updateDelta);
    21.  
    22.         _updateDelta = 0;
    23.         _lastUpdateTime = Time.time;
    24.     }
    25. }
    I tested this at 5FPS and the sword trigger seemed to follow the right path (even though visually, the sword animation skips almost all frames). Meanwhile at 60FPS the animations look silky smooth now.