Skip to main content

First-Person Shooter: Animation synchronization

info

This is Elympics First-Person Shooter tutorial: part 13. In this part we’ll explain how to synchronize animations. See: Part 12.

At this moment, each player is represented by a capsule (Capsule Collider and Capsule Mesh), but they should be fully animated, humanoid characters. To achieve this, you’ll need to add appropriate animations, both for the full model of the character visible to other players and the player's hands (a separate model visible only to the player controlling the specific character).

Synchronizing animations with Elympics

In this tutorial, we want to focus only on animation synchronization, so we’ll use a ready-made solution that can be found in the project shared with this guide.

The player character visible to opponents (humanoid model) is placed directly under the empty game object “CharacterModel” container: First-Person Shooter

And it consists of the following components:

First-Person Shooter

The most important component used for full animation synchronization is the Elympics Animator Synchronizer component that also requires the ElympicsBehaviour component to work properly:

First-Person Shooter

This component allows you to select variables and layers in the inspector that will be synchronized. For this reason, to handle the animator and its synchronization, you’ll only need a script that will set the appropriate variable in the animator locally, and Elympics will ensure that the animator is properly synchronized in other clients.

Let's use the PlayerThirdPersonAnimatorMovementController script as an example. It’ll be responsible for setting the animator values related to the player's movement locally:

[RequireComponent(typeof(Animator))]
public class PlayerThirdPersonAnimatorMovementController : MonoBehaviour
{
[SerializeField] private MovementController playerMovementController;
[SerializeField] private DeathController playerDeathController;

private readonly int movementForwardParameterHash = Animator.StringToHash("MovementForward");
private readonly int movementRightParameterHash = Animator.StringToHash("MovementRight");
private readonly int jumpingTriggerParameterHash = Animator.StringToHash("JumpTrigger");
private readonly int deathTriggerParameterHash = Animator.StringToHash("DeathTrigger");
private readonly int resetTriggerParameterHash = Animator.StringToHash("ResetTrigger");
private readonly int isGroundedParameterHash = Animator.StringToHash("IsGrounded");

private Animator thirdPersonAnimator = null;

private void Awake()
{
thirdPersonAnimator = GetComponent<Animator>();
playerMovementController.MovementValuesChanged += ProcessMovementValues;
playerMovementController.PlayerJumped += ProcessJumping;
playerMovementController.IsGroundedStateUpdate += ProcessIsGroundedStateUpdate;
playerDeathController.IsDead.ValueChanged += ProcessDeathState;
}

private void ProcessDeathState(bool lastValue, bool newValue)
{
if (newValue)
{
thirdPersonAnimator.SetTrigger(deathTriggerParameterHash);
}
else
{
thirdPersonAnimator.SetTrigger(resetTriggerParameterHash);
}
}

private void ProcessIsGroundedStateUpdate(bool isGrounded)
{
thirdPersonAnimator.SetBool(isGroundedParameterHash, isGrounded);
}

private void ProcessJumping()
{
thirdPersonAnimator.SetTrigger(jumpingTriggerParameterHash);
}

private void ProcessMovementValues(Vector3 movementDirection)
{
var localMovementDirection = playerMovementController.transform.InverseTransformDirection(movementDirection) * 2.0f;

thirdPersonAnimator.SetFloat(movementForwardParameterHash, localMovementDirection.z);
thirdPersonAnimator.SetFloat(movementRightParameterHash, localMovementDirection.x);
}
}

This script is for local animator support, so it doesn't require any inheritance from ElympicsMonoBehaviour. To operate fully functionally, it needs references to the MovementController and DeathController components that provide information about the player's current status (alive/dead) and the values of actions related to the movement they’re currently performing.

Based on the two components above, animator variables are updated accordingly:

  • Two float type variables responsible for storing information about the current direction and speed with which the player is moving updated based on the MovementValuesChanged event of the MovementController component;
  • One trigger whose method is subscribed to the PlayerJumped event and is responsible for performing the jump animation when the player jumps;
  • One bool variable that updates its state frame by frame depending on whether it’s on the ground or in the air;
  • Two trigger variables executed in the ProcessDeathState method, called when the player's state has changed (alive-dead). These triggers are responsible for triggering the death animation or resetting the player's animation to the idle (respawn) state.

All of these variables are used to handle the part of the animator shown below:

First-Person Shooter

All of these variables are set locally (many of them are based on the updated state, e.g. in the ElympicsUpdate method, so changes will only take place for the player controlled by the client and on the server). However, thanks to the use of the ElympicsAnimatorSynchronizer component, they will be visible to all the players in the game.

The animator of hands visible to the player you’re currently controlling is constructed very similarly to the previously discussed PlayerThirdPersonAnimatorMovementController script. However, it doesn’t have the ElympicsAnimatorSynchronizer component: its animations are visible only to the local player anyway.

Finally, all the animations are synchronized properly:

First-Person Shooter

That's all!

Congratulations! 🎉🎉

You’ve created a fully functional, server-authoritative, multiplayer FPS game!

Feel free to experiment and expand this project to create an FPS game you've always dreamt of!