First-Person Shooter: Animation synchronization
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:
And it consists of the following components:
The most important component used for full animation synchronization is the Elympics Animator Synchronizer
component that also requires the ElympicsBehaviour
component to work properly:
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 theMovementController
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:
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:
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!