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
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)
*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
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");
}
}
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).
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.
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.
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.
1. Synchronize the properties of every
AnimancerState
separately2. Synchronize a whole array of states (
AnimancerLayer.States
) as-isGoing 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.
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.
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.