Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mixers don't stop synchronizing their children while they aren't playing #242

Closed
JohnManna opened this issue Dec 22, 2022 · 8 comments
Closed
Labels
Bug Something isn't working Solved A solution is available here (but may not yet be included in the latest release)

Comments

@JohnManna
Copy link

JohnManna commented Dec 22, 2022

Environment

  • Animancer Version Number: 7.3
  • Animancer Pro or Lite: pro
  • Unity Version: 2022.2.0f1
  • Platform: windows/editor

Description

It seems inactive LinearMixerState nodes are still processed causing a large performance loss with scenes with a high number of active Animancer components. This has been observed by wrapping UpdateAll(_PreUpdatables, info.deltaTime * info.effectiveParentSpeed); with a profile sample and by observing the LinearMixerState nodes within private void UpdateAll(Key.KeyedList<IUpdatable> updatables, float deltaTime). Shouldn't these nodes be removed from _PreUpdatables? Its strange because the playableGraph does remove these nodes when they are inactive.

Reproduction

Spawning 128 characters with this code requires 0.60ms to process the inactive LinearMixerState even though the idle state is playing. Replacing this node with a AnimationClip improves performance bringing the cost down to 0.07ms.

With the mixer
image

Without
image

Graph of time of profiling
image

Code snippets:

  // helpers
  public static T CreateState<T>(this AnimancerComponent animancer, int layer = 0) where T : AnimancerState, new() {
      var state = new T();
      animancer.Layers[layer].AddChild(state);
      return state;
  }
  
  public static void PlayState(this AnimancerComponent animancer, object state, float fadeTime = 0.2f) {
      animancer.Play(animancer.States[state], fadeTime);
  }
  
  // setup
  public AnimationClip idle;
  public MixerTransition2D walk;
  public MixerTransition2D run;
  
  animancer.States.GetOrCreate(0, idle);
  
  LinearMixerState movementMixer = animancer.CreateState<LinearMixerState>();
  movementMixer.Key = 1;
  movementMixer.Initialize(2);
  movementMixer.SetDebugName("movement");
  
  float walkAnimationClipMovementSpeed = ((Motion)walk.Animations[0]).averageSpeed.magnitude;
  float runAnimationClipMovementSpeed = ((Motion)run.Animations[0]).averageSpeed.magnitude;
  
  MixerState<Vector2> walkState = walk.CreateState();
  walkState.SetDebugName($"Walk");
  movementMixer.SetChild(0, walkState, threshold: walkAnimationClipMovementSpeed);
  
  MixerState<Vector2> runState = run.CreateState();
  runState.SetDebugName($"Run");
  movementMixer.SetChild(1, runState, threshold: runAnimationClipMovementSpeed);
  
  // play animations
  
  int animations.runningState = 0;
  int animations.runningState = 1;
  ...
  float movementMagnitude = math.length(characterBody.RelativeVelocity);
  bool moving = movementMagnitude > 0.01f;
  animancer.PlayState(moving ? animations.runningState : animations.idleState, 0.2f);
  
  if (moving) {
      float3 localVelocity = localTransform.InverseTransformDirection(characterBody.RelativeVelocity);
      float2 movement2d = new float2(localVelocity.x, localVelocity.z);
  
      var MovementMixer = animancer.States[animations.runningState] as LinearMixerState;
      var WalkState = MovementMixer.ChildStates[0] as MixerState<Vector2>;
      var RunState = MovementMixer.ChildStates[1] as MixerState<Vector2>;
  
      MovementMixer.Parameter = MathExtensions.MoveTowards(MovementMixer.Parameter, movementMagnitude, 5f * deltaTime);
      WalkState.Parameter = MathExtensions.MoveTowards((float2)WalkState.Parameter, movement2d, 10f * deltaTime);
      RunState.Parameter = MathExtensions.MoveTowards((float2)RunState.Parameter, movement2d, 10f * deltaTime);
  }

@JohnManna JohnManna added the Bug Something isn't working label Dec 22, 2022
@KybernetikGames
Copy link
Owner

Disconnecting weightless playables from their parent usually improves performance (see the Connected vs. Disconnected table here). But for Mixer Synchronization to work it needs to keep all the mixer's children connected and continually modifies their speed to sync their time (because setting the time has other side effects like skipping events).

Having the whole mixer inactive while playing something else shouldn't have any performance cost though (except memory usage obviously). I'll need to look into that.

Also, you might want to check out Animancer v7.4 which has significant improvements to the way you can initialize mixers in code.

@KybernetikGames
Copy link
Owner

Are you sure you aren't modifying the mixer.Parameter while the state is inactive? I haven't looked at the code yet, but that could be causing it to recalculate all the child weights which is particularly expensive for 2D mixers (though I'm hoping I can improve that using the jobs system for Animancer v7.4).

@JohnManna
Copy link
Author

Thanks for the info, I'll check it out.

I've looked through the code and it appears ApplySynchronizeChildren is what is costing so much. I believe this is what you are referring to above.

Are you sure you aren't modifying the mixer.Parameter while the state is inactive? I haven't looked at the code yet, but that could be causing it to recalculate all the child weights which is particularly expensive for 2D mixers (though I'm hoping I can improve that using the jobs system for Animancer v7.4).

Yes, if (moving) protects against that, I just added a breakpoint to verify it.

@JohnManna
Copy link
Author

Disconnecting weightless playables from their parent usually improves performance (see the Connected vs. Disconnected table here). But for Mixer Synchronization to work it needs to keep all the mixer's children connected and continually modifies their speed to sync their time (because setting the time has other side effects like skipping events).

Yeah, I have seen a big improvement with performance with nodes being disconnected. What I don't understand is why is the parent mixer node still running updates when it has been removed from the graph? I understand that its children will still be connected to it but if the parent node is not part of the playable graph, shouldn't updates be skipped?

@JohnManna
Copy link
Author

Adding a simple condition of if (!IsPlaying && !needsMoreUpdates) return; before ApplySynchronizeChildren(ref needsMoreUpdates); in MixerState.Update(out bool needsMoreUpdates) has greatly improved performance. Maybe this has some bad side effects like you mentioned, but for my use case seems ok. In an extreme use case of 128 animators with 10 nested mixers per each, the prepare frame step is now only 0.07ms. Without this change it was 5.50ms.

@KybernetikGames
Copy link
Owner

KybernetikGames commented Dec 23, 2022

Change the if at the start of ApplySynchronizeChildren to:

if (Weight == 0 ||
    !IsPlaying ||
    _SynchronizedChildren == null ||
    _SynchronizedChildren.Count <= 1)
    return;

I only did a quick test but it seems to work fine. The side effects I mentioned were only for having some of the mixer's children disconnected, but if the whole mixer isn't playing then there obviously isn't any point in having the mixer update the speed of its children every frame and the mixer will get added back to the update list when it gets played again.

@JohnManna
Copy link
Author

Fantastic, basically same change I tried. Works great.

@KybernetikGames KybernetikGames added the Solved A solution is available here (but may not yet be included in the latest release) label Dec 23, 2022
@KybernetikGames KybernetikGames changed the title LinearMixerState with MixerState<Vector2> children performance issue Mixers don't stop synchronizing their children while they aren't playing Dec 23, 2022
@KybernetikGames
Copy link
Owner

Animancer v7.4 is now available with this fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Something isn't working Solved A solution is available here (but may not yet be included in the latest release)
Projects
None yet
Development

No branches or pull requests

2 participants