Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Feature Request Getting an Animator to drop frames at a distance

Discussion in 'Animation' started by hypnoslave, Apr 30, 2023.

  1. hypnoslave

    hypnoslave

    Joined:
    Sep 8, 2009
    Posts:
    427
    Hey all, does this feature exist in any way? I haven't seen it.

    I've noticed it in several games - when enemies are far away, the animation system tends to drop frames, only updating the animated model every X frames. I actually implemented this in my most recent game, The Last Hero of Nostalgaia, but the only way I found to do it was calling Animator.Update manually, which puts it on the main thread. If there are several enemies around (likely) the removal of multi-threaded processing resulted in basically NO savings, unless I dropped many, many frames, which made sense in some cases so I did it.

    If I could drop frames AND have the animators be processed in parallel that'd be dope.
     
  2. AcidArrow

    AcidArrow

    Joined:
    May 20, 2010
    Posts:
    10,879
    I do not think so, and I also don’t think it has a big chance of being implemented for the animator (or any animator feature, actually), and the new animation stuff are probably a handful of years away from any meaningful release.
     
  3. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    1,992
    Have you tried setting animator.speed to 0 and after few frames speed it up above 1 so it reaches desired frame ?

    I'm totally guessing here on an assumption that there is no point in recalculating pose when speed is 0.
    I don't know what unity is doing internally, it might recalculate even when speed is 0.

    Please post your findings if you decide to test this solution.
     
  4. hypnoslave

    hypnoslave

    Joined:
    Sep 8, 2009
    Posts:
    427
    ah! hah well that's a pretty good idea, buuuuuuttttt that doesn't quite generate the correct effect. That would cause them to appear to be moving in slow motion. If we have a walk cycle, say, where it takes 7 frames to go from one foot extension to the other, that's defines the movement speed of the entity. If we start pausing the animator then the entity will glide. Furthermore, I believe distant entities moving in slow motion would be much more noticeable than having them "animate on 2s" which every animated film is doing these days anyway. When Spider-Man is flipping through the air, he still looks dope because the timing is right, even though we're seeing a lower frame rate because it's a stylistic choice.

    Thanks for sharing the thought though!

    EDIT: I am dumb. I forgot the main reason we're doing this in the first place - which is to stop the animator from taking up processing power at all for a frame or two. if we set the speed to zero, it's still gobbling up resources!
     
  5. tsukimi

    tsukimi

    Joined:
    Dec 10, 2014
    Posts:
    50
    How about stop playing some frame, and then when restart, set play speed high to catch up the original framerate?

    I just wrote a simple DropFrame Component. In the below code, I used
    PlayableGraph.Stop()
    instead of setting speed to zero, that should actually stop the evaluation of Animation.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Playables;
    3.  
    4. [RequireComponent(typeof(Animator))]
    5. public class AutoDropFrame : MonoBehaviour
    6. {
    7.     [SerializeField] private bool doDropFrame;
    8.     [SerializeField, Min(1)] private int targetFrameRate = 16;
    9.  
    10.     private Animator animator;
    11.     private PlayableGraph playableGraph;
    12.  
    13.     private float accumulatedTime = 0f;
    14.     private bool playedThisFrame = false;
    15.  
    16.     void Awake()
    17.     {
    18.         animator = GetComponent<Animator>();
    19.     }
    20.  
    21.     private void OnEnable()
    22.     {
    23.          playableGraph = animator.playableGraph;
    24.          animatorPlayable = playableGraph.GetRootPlayable(0);
    25.     }
    26.  
    27.     void Update()
    28.     {
    29.         if (doDropFrame) {
    30.             DropFrameUpdate();
    31.         }
    32.     }
    33.  
    34.     void DropFrameUpdate()
    35.     {
    36.         // only play if accumulated enough time
    37.         accumulatedTime += Time.deltaTime;
    38.         var timeThreshold = 1f / targetFrameRate;
    39.         if (accumulatedTime < timeThreshold) return;
    40.  
    41.         accumulatedTime -= timeThreshold;
    42.         playableGraph.Play();
    43.         // let animator catch up the accumulated time
    44.         animator.speed = timeThreshold / Time.deltaTime;
    45.         playedThisFrame = true;
    46.     }
    47.  
    48.     private void LateUpdate()
    49.     {
    50.         // animator is played between Update and LateUpdate
    51.         // so we need to stop it in LateUpdate
    52.         if (playedThisFrame) {
    53.             playedThisFrame = false;
    54.             playableGraph.Stop();
    55.         }
    56.     }
    57. }
    You may also want to seperate evaluation to different frames for different enemies, since playing all on same frame would still cause a spike.

    ... However, I also agree that it would be nice to be a official-supported feature, since many big title (like Zelda or Pokemon or Kirby, etc) also have this feature.
     
  6. hypnoslave

    hypnoslave

    Joined:
    Sep 8, 2009
    Posts:
    427
    Hey good idea, and thanks for taking the time to write this!! I thought about doing a "catch up" based on what koirat suggested, but I assumed that setting the speed to 0 would result in the animator to be processing anyway.... So are you sure this is different? I don't know anything about PlayableGraph, actually. wouldn't stopping the animation cause it to be reset? Also, if the state is stopped, does the animator stop using resources? OH HEY. you knooooowwww... the other thing we could do is just DISABLE the animator for a few ticks and then re-enable at increased speed and let it update it's self... that could work actually...

    Oh, and to answer your question, I implemented a simple global variable called "tickCount" - when each enemy starts, they check against that number and increment it to ensure that they're all dropping frames on an offset tick.
     
  7. tsukimi

    tsukimi

    Joined:
    Dec 10, 2014
    Posts:
    50
    You can observe that when stopped, you can change the rotation and position of the bones in inspector, and the model will respond to the transformation, meaning that it is not evaluating (In the mean time, It will stick to the animation pose when setting speed to 0, that is, still evaluating). You can also check the Profiler.
    Animation state won't be reset when the playablegraph is stopped.
    Disabling and enabling animator is indeed also a good solution, I'm just aware of that it may need some initialize cost, I don't know. (also, don't forget to turn the keepAnimatorControllerStateOnDisable on)
    If you're not familiar to PlayableGraph, disable/enable animator should be a better way, considering the maintainability.
    Good to see that you already have a good offset system.
     
  8. hypnoslave

    hypnoslave

    Joined:
    Sep 8, 2009
    Posts:
    427
    Hey that's fantastic.

    I think you solved our problem. Thanks Tsukimi! that's really great.
     
    tsukimi likes this.