This document is about: FUSION 2
SWITCH TO

Animation

Overview

Animation gives players crucial gameplay feedback, such as movement and actions induced by player input.

This document covers choosing the right approach for animations when building multiplayer games with Fusion. The examples will focus on character animation; however, the concepts for synchronizing animations over network presented in this document are applicable for any other animated objects as well.

For a practical animation sample please refer to the Fusion Animations technical sample.

Animation Accuracy

Before starting with animations in Fusion, it is important to establish the baseline accuracy required and targeted by the animation system.

Generally speaking, there are two types of animation approaches based on their accuracy:

  • Render accurate animations
  • Tick accurate animations
Most games use render accurate animations. Tick accurate animations are only needed in rare use cases (explained below) when animations affect gameplay.

Render Accurate Animations

Render accurate animations are executed outside the simulation (from Render or MonoBehaviour.Update calls) or just in the FUN Forward* stage. The start of the animation is usually only loosely synchronized and character poses are NOT rewound to previous states during resimulations. Since full animation state (current playing animation, animation time, animation weight,...) is not part of the networked data it is not guaranteed that characters will be in exactly same pose on all clients at specified time.

Using a render accurate approach means calculations depending on limb positions can be quite off. The degree of inaccuracy depends largely on the type of animation. Static animations such as Idle or Aim may be accurate enough, while running or rapid actions might be significantly worse. It is recommended to not use transforms in the animated hierarchy for simulation calculations (e.g. laser ray calculation from palm of the hand). However such usage is rarely necessary. Check Do I need tick accurate animations? section for more information.

✅ Pros:

  • Ability to use common animation solutions (Animator, Animancer)
  • No need to change studio animation pipeline
  • 3rd party animation add-ons out of the box

❌ Cons:

  • Transforms in the animated hierarchy cannot be used for accurate simulation calculations
  • Inaccurate position of hitboxes that are dependent on the animations - although this can be mitigated with some solutions (check Animations and Lag Compensation)
Note: When something happens differently during resimulation it is expected that animation will self-correct over time. Imagine a jump animation was started based on a jump input command, but after a few ticks the server state reconciles without the jump action (e.g. the player actually got frozen by the opponent before the jump). The animation should just be canceled immediately and a blend to the correct current state should be applied.

*FUN Forward = FixedUpdateNetwork method call with Forward simulation stage (Runner.IsForward == true)

Tick Accurate Animations

Tick accurate animation means that the animation is in the exactly same state on all clients and the server at a given tick. In other words, the complete animation state is part of the networked data and the animation can be rewound and played again (or rather stepped through) during resimulations. Having characters in the same pose on all clients is especially important for lag compensated colliders on character limbs. For example, hitting another player's leg in the middle of a run animation on a client, will produce the exact same hit on the server.

✅ Pros:

  • Characters are at any given time in the same pose on all clients and the server making all hit calculations precise
  • Gives the ability to build upon precise limb positioning for other calculations - e.g. laser ray originating from the palm of a hand, or hurt box calculations for melee attacks

❌ Cons:

  • Difficult to pull off, as it requires a custom animation solution (the Unity Animator does not support tick accurate animations)
  • 3rd party animation add-ons might not work (due to their dependency on the Animator, inability to handle resimulations, etc...)
  • The animation solution might be more code-centric, resulting in worse developer experience when setting animations up compared to other solutions like Animator Controllers
Advanced Explanation: Tick accurate animations refers to an approach providing enough data to reconstruct a character pose for any given tick and apply this data so the character will appear in that exact same pose for the given tick on all clients.
When the most recent tick data arrives from the server and resimulation begins on the client side, code resimulates the animation for the local player (= Input Authority) based on the new data up until it reaches the local predicted tick again. For proxies no special handling is necessary during resimulations since they are animating in remote time (= based on already verified data from the server); therefore a proxy's animation will be identical in resimulation.
For correct lag compensation it is enough to evaluate proxy animations once in the FUN Forward* call before the HitboxManager saves positions of the hitboxes. During resimulations the character pose of proxies is not actually changed and already saved hitbox positions are used for correct lag compensated casts.

*FUN Forward = FixedUpdateNetwork method call with Forward simulation stage (Runner.IsForward == true)

Do I need tick accurate animations?

Building a tick accurate animation solution is complex and time-consuming; consider whether it is absolutely necessary for the project and if the additional workload is justified. In many cases tick accurate animations are unnecessary and projects can be successfully executed with render accurate animations without players noticing the difference. Deciding which approach to choose is very game specific.

Generally speaking a tick accurate approach is required if the project falls within one of the two following cases:

  • The need for 100% accurate hits to hitboxes placed on animated parts of a character
  • There are calculations using transform positions that are heavily influenced by animations and cannot be easily replaced by manual calculation - e.g. to drive a hurt box that is connected to a fist during a melee attack

Precise calculations based on character position or direction can also be achieved with a render accurate approach. It just means the calculations cannot be based on transforms which are influenced by the animated transform hierarchy (= bones). The calculated position in the game state and their visual representation can differ as explained in the examples below.

Example 1: In FPS games it is a common approach to do shooting calculation from the camera - not from the actual barrel of the gun that is influenced by animations. Check how the visuals of projectiles are interpolated to their actual path in the Fusion Projectiles. However, if the calculations have to actually be done from the barrel of the gun and the gun position and rotation are influenced by player animations, a tick accurate approach is required.

Example 2: When performing a melee attack, the hurt boxes can follow a predefined scripted path during attack. This path then can be executed during resimulations independently on the character animation. On the other hand if the hurt box has to follow a complicated path of the character's fist as it is designed in the animation, a tick accurate animations are necessary.

Render accurate animation solutions

Below is a list of few recommended render accurate animation solutions.

NOTE: In the following paragraphs, the Unity Mecanim Animator will simply be referred to as Animator.

Animator/Animancer + Existing network state

Render accurate approach

Usually an already existing network state can be used to control the animations. Animations are set based on networked data in the Render method. This solution can be easily recommended for most games as it is reasonably easy to implement, does not waste network resources and can be made more precise based on the game needs.

C#

public override void Render()
{
    _animator.SetFloat("Speed", _kcc.Data.RealSpeed);
}

When it is needed, additional data for animations can be easily synchronized over standard networked properties.

C#

[Networked]
private NetworkButtons _lastButtonsInput { get; set; }
[Networked]
private int _jumpCount { get; set; }
private int _lastVisibleJump;

public override void Spawned()
{
    _lastVisibleJump = _jumpCount;
}

public override void FixedUpdateNetwork()
{
    var input = GetInput<PlayerInput>();
    if (input.HasValue == false)
        return;

    if (input.Value.Buttons.WasPressed(_lastButtonsInput, EInputButtons.Jump) == true)
    {
        DoJump();
        _jumpCount++;
    }

    _lastButtonsInput = input.Value.Buttons;
}

public override void Render()
{
    if (_jumpCount > _lastVisibleJump)
    {
        _animator.SetTrigger("Jump");
        // Play jump sound/particle effect
    }

    _lastVisibleJump = _jumpCount;
}

Similar functionality that uses ChangeDetector instead:

C#

[Networked]
private NetworkButtons _lastButtonsInput { get; set; }
[Networked]
private int _jumpCount { get; set; }

private ChangeDetector _changes;

public override void Spawned()
{
    _changes = GetChangeDetector(ChangeDetector.Source.SnapshotFrom);
}

public override void FixedUpdateNetwork()
{
    var input = GetInput<PlayerInput>();
    if (input.HasValue == false)
        return;

    if (input.Value.Buttons.WasPressed(_lastButtonsInput, EInputButtons.Jump) == true)
    {
        //DoJump();
        _jumpCount++;
    }

    _lastButtonsInput = input.Value.Buttons;
}

public override void Render()
{
    foreach (string propertyName in _changes.DetectChanges(this, out var previousBuffer, out var currentBuffer))
    {
        switch (propertyName)
        {
            case nameof(_jumpCount):
                var reader = GetPropertyReader<int>(nameof(_jumpCount));
                var values = reader.Read(previousBuffer, currentBuffer);

                if (values.Item2 > values.Item1)
                {
                    _animator.SetTrigger("Jump");
                    // Play jump sound/particle effect
                }

                break;
        }
    }
}

It is possible to use a simpler version of the DetectChanges block (see below). However, such usage is slightly error-prone since it is possible that jump will not happen on the server (e.g. the player actually got frozen by the opponent before the jump) which would result in triggering the Jump for the local player twice (once when the value is increased during local prediction and once when the value is returned to previous value after receiving data from the server).

C#

foreach (string propertyName in _changes.DetectChanges(this, out var previousBuffer, out var currentBuffer))
{
    switch (propertyName)
    {
        case nameof(_jumpCount):
            _animator.SetTrigger("Jump");
    }
}
Note: Higher animation precision can be achieved by using Interpolation and synchronizing animation state (current playing animation and animation time) when needed. See more in the Animation and Lag Compensation and Tips for render accurate state synchronization sections.

For a practical example please refer to Fusion Animations technical sample (Example 1-3).

Animator + NetworkMecanimAnimator

Render accurate approach

Use the built-in NetworkMecanimAnimator component that comes with Fusion to synchronize Animator properties. After assigning an Animator to the NetworkMecanimAnimator component, just set Animator properties and NetworkMecanimAnimator will synchronize them automatically to all clients. The exceptions are triggers which need to be set also through the NetworkMecanimAnimator component (check Network Mecanim Animator page in the manual for more information).

NetworkMecanimAnimator
NetworkMecanimAnimator

For better animation behavior when a proxy object suddenly appears (e.g. after late join or when proxy object enters Area of Interest) enable synchronization of StateRoot (first layer in the animator controller) and StateLayers (all other layers), this will ensure the correct state is playing at the correct time.

NOTE: Enabling this option will significantly increase data traffic.

NetworkMecanimAnimator
StateRoot and StateLayers sychronization options
Note: The use of triggers is NOT recommended (although supported) in complex animation setups due to their unpredictable consume behavior in some situations (especially when using Sub-State Machines) that could get worse when transferring trigger action over the network due to slightly different timing. For example use a HasJumped bool that will be active for some time instead of a Jump trigger.

For a practical example please refer to Fusion Animations technical sample (Example 4).

Animator/Animancer + FSM synchronization

Render accurate approach

Play animations from FSM (Finite State Machine) states - e.g. jump state, attack state, locomotion state. The current state is synchronized over the network, additional data needed for the animations can be stored in standard networked properties.

C#

public class JumpState : PlayerState
{
    protected override void OnEnterState()
    {
        DoJump();
    }

    protected override void OnEnterStateRender()
    {
        Animator.SetTrigger("Jump");
    }
}

Breaking animation control into states helps manage complex animation setups and allows easy control over other visual effects together with animations (e.g. play jump sound and jump VFX when entering jump state). When FSM is properly synchronized with support for resimulations it can be used for other simulation logic as well - states can be actually AI states or player behavior states.

Note: This approach needs a custom networked FSM implementation. A NetworkFSM implementation by Photon is currently in preview as part of the Fusion Animations sample.

For a practical example please refer to Fusion Animations technical sample (Example 5).

Tick accurate animation solutions

Be aware that the standard Unity Animator has limited options on how to control animation states, which prevents it from being used for tick accurate animations. However, Unity's lower level Playables API does allow for such functionality. So if the project will be targeting tick accurate animations, the implementation will most likely need to be based on Playables.

Animancer + Tick accurate wrapper around animancer states

Tick accurate approach

Since Animancer is based on Playables, the AnimancerState properties can be synchronized and Animancer can be stepped manually from FixedUpdateNetwork to achieve tick accurate animations.

This approach is recommended only when "tick accurate enough" is acceptable for your project. Since Animancer is doing some magic in the background (like creation of weightless states during certain fades), achieving 100% tick accuracy with all the Animancer features can be time-consuming and going with a custom solution based on Playables right away might be a preferable long-term option.

Note: There are two ways this can be approached:
1. Synchronize the properties of every AnimancerState separately
2. Synchronize a whole array of states (AnimancerLayer.States) as-is
Going with the solution 2 might end up easier and more bullet proof depending on the required overall complexity of the animation solution for the project.

Custom solution on top of Playables API

Tick accurate approach

The Playables API is outside the scope of this manual. The general idea is to use custom data structures representing animation states to synchronize the necessary data over the network and to construct PlayableGraph. Then evaluate PlayableGraph according to the project's needs in standard Fusion methods.

Check Fusion BR sample for battle tested code-driven solution or refined and simplified version in Fusion Animations technical sample (Example 6).

Legacy Unity Animation + Tick accurate wrapper around AnimationStates

Tick accurate approach

Use custom data structure to synchronize data about playing AnimationStates. Use Animation.Sample to achieve correct character pose for the tick.

This is NOT a recommended solution unless the project's animation pipeline is already using the Legacy Animation system.

Animations and Lag Compensation

Lag Compensation ensures hits that were registered on one client will be correctly recognized on the server as well.

If hitboxes are influenced or driven by animations, it is critical to ensure characters are in the correct pose before the state of hitboxes is captured. Be aware the hitbox data is captured automatically by HitboxManager in the FUN Forward call. For resimulations the already saved positions of hitboxes are used for correct raycast calculations, thus animations on proxy characters do not need to go back in time. This is exactly why even the render accurate approach can result in precise-enough lag compensated hits if executed correctly.

Note: HitboxManager has high script execution order (2000) so all NetworkBehaviours will be executed before HitboxManager by default and no special action is necessary.

The server assumes clients render proxy characters in remote timeframe so this timing is used on server for lag compensated casts. Therefore, animations on clients need to respect the remote timeframe of proxies as well. In other words the proxies on clients need to be animated based on interpolated data in order to have accurate lag compensation.

Having correct animation timings is better and more precisely achieved with tick accurate animations since the interpolated animation states data can be used for proxies in every FUN Forward and Render to reconstruct their character pose at given tick or render time. Proxy characters in this case just display interpolated data from the server, they do not really "play" animations themselves.

On the other hand render accurate animations run fully on the proxy characters. What matters is the time when the parameter is set to the Animator or the animation clip is started. The simple approach is to ignore total accuracy and act based on the latest network data (see the example below). However, for more precise animations it is required to set animation parameters or start playing clips based on the interpolated data.

It is usually also necessary to synchronize playing animation (animation state and state time) either periodically or in special occasions (like character entering client's area of interest) - see more in the Tips for render accurate state synchronization section.

Render accurate animations based on latest networked data (_speed, _jumpCount):

C#

[Networked]
private float _speed { get; set; }
[Networked]
private int _jumpCount { get; set; }

private int _lastVisibleJump;

public override void Spawned()
{
    _lastVisibleJump = _jumpCount;
}

public override void FixedUpdateNetwork()
{
    // _speed and _jumpCount is changing here
}

public override void Render()
{
    _animator.SetFloat("Speed", _speed);

    if (_lastVisibleJump < _jumpCount)
    {
        _animator.SetTrigger("Jump");
    }

    _lastVisibleJump = _jumpCount;
}

Render accurate animations based on interpolated data. The use of interpolators makes animations more accurate at the expense of slightly increased complexity:

C#

[Networked]
private float _speed { get; set; }
[Networked]
private int _jumpCount { get; set; }

public override void FixedUpdateNetwork()
{
    // _speed and _jumpCount is changing here
}

public override void Render()
{
    var interpolator = new NetworkBehaviourBufferInterpolator(this);

    _animator.SetFloat("Speed", interpolator.Float(nameof(_speed)));

    int interpolatedJumpCount = interpolator.Int(nameof(_jumpCount));

    if (_lastVisibleJump < interpolatedJumpCount)
    {
        _animator.SetTrigger("Jump");
    }

    _lastVisibleJump = interpolatedJumpCount;
}

Tips

Tips for render accurate state synchronization

The render accurate approach does not focus on animation state synchronization, but rather on synchronizing changes and applying those changes at the correct time. However, to increase the precision of the render accurate approach it is better to implement at least some sort of state synchronization. This means to synchronize either periodically or at certain events the current playing state and especially the current state time of the character animation.

For example, it is known that the proxy character is running and what its speed is. However, when these values are set in the Animator for the first time (after joining the game, when proxy characters enter a local player's area of interest etc.) it will play the locomotion loop from start even though the remote player was running already for 20 seconds. This will lead to different animation timing between clients and thus imprecise animation synchronization. This issue will usually fix itself when the remote player performs another action like Jump but until the player performs such action, animation timing can be quite off.

In case of Unity Animator Animator.GetCurrentAnimatorStateInfo() can be used to get the current state's full path hash and normalized time, synchronize them as standard networked properties and apply them on proxy characters by calling Animator.Play(int stateNameHash, int layer, float normalizedTime). The sync should however only happen when the animator is out of transition to prevent any unwanted visual glitches.

Note: This is exactly what NetworkMecanimAnimator does when a StateRoot synchronization is enabled. It synchronizes the currently playing animation state on the first layer as well as its time; it does so continuously except when the animator is transitioning between states. Synchronizing normalized time continuously is not ideal as the value changes constantly and consumes network resources.

Tips for tick accurate state synchronization

Whether the animation states will be driven based on Playables, AnimancerStates or Unity's AnimationStates, be sure to synchronize only the data that is needed for the correct recalculation and ideally do not synchronize value changes over time. For example, it is unnecessary to send Time and Weight changes with every tick - but it is crucial to synchronize a state's StartTick, FadeSpeed and TargetWeight to calculate the current Weight and Time values on every client.

Back to top