Fusion Animation Synchronization
Overview
- By theory, animation should not sync over the network, animation should be handled locally if possible. by locally means each client play animation in response to the network object’s state. thus, the ones that should sync over the network are the ‘states’ or the animation name.
‘Sync’ Animator Over Network
-
One of many solutions to achieve this is, by sync the clip name. then let each client play an animation by that clip name.
public class Player : NetworkBehaviour { [Networked] public string PlayerName { get; set; } [Networked] public string ClipName { get; set; } ///some code/// }
Lining up some animation clips inside animator.

Then let player play correct animation, after server sync it of course.
private void PlayAnimation(string targetClipName) { if (LauncherExtension.IsServer) { if (_unitController.PlayerControlled) LauncherExtension.Launcher.GetPlayer(_controller.PlayerRef).ClipName = targetClipName; } if (_unitController.PlayerControlled) _animator.CrossFade(LauncherExtension.Launcher.GetPlayer(_controller.PlayerRef).ClipName, 0f, 0); else _animator.CrossFade(targetClipName, 0f, 0); }
Adding up State Machine
-
For example repos, a state machine was implemented, hence there is a tiny bit of modification on the code above.
private void PlayAnimation(State currentState) { if (LauncherExtension.IsServer) { if (_unitController.PlayerControlled) LauncherExtension.Launcher.GetPlayer(_controller.PlayerRef).ClipName = currentState?.ClipName; } if (_unitController.PlayerControlled) _animator.CrossFade(LauncherExtension.Launcher.GetPlayer(_controller.PlayerRef).ClipName, 0f, 0); else _animator.CrossFade(currentState?.ClipName, 0f, 0); }
public class StateMachine : MonoBehaviour { [SerializeField] private StateSO _initialState; [SerializeField] private TransitionTableSO _transitionSO; [SerializeField] private StateMachineAnimator _stateMachineAnimator; private Transition _transition; private State _currentState; private State _targetState; public State CurrentState => _currentState; public void Initialize() { _currentState = _initialState.GetState(this); _transition = _transitionSO.Initialize(this, _currentState); } public void NetworkUpdater() { if (_transition.TryUpdateState(out _targetState)) DoTransition(_targetState); _stateMachineAnimator?.Updater(_currentState); _currentState?.OnUpdate(); } private void DoTransition(State targetState) { _currentState?.OnExit(); _currentState = targetState; _currentState?.OnEnter(); } }
-
Below quick clip of network animation handling.
Handling World Object Animation
-
Handling world object or interactable animation shouldn’t differ with character animation. handle it locally if is possible. however, sometimes we have to sync interactable animation, some example case would be when opening a treasure chest, player should have a ‘state’ where player is interacting with interactable, entering that state would require player to standing near interactable object and pressing some buttons. when that all condition met, player will start ‘interacting’ with that object, thus we can play some animation for it. for this example a same sync technique in syncing character are used.
-
Let create simple interface for interactable object.
public interface IInteractableObject { string ClipName { get; } void Interact(); }
-
Then create class for handle interactable animation, note, it recommended to separate animation handling and handling network object in general.
public class ObjectAnimator : NetworkBehaviour, IInteractableObject { [SerializeField] private Animator _animator; [SerializeField] private string _clipName; [Networked(OnChanged = nameof(OnClipNameChanged))] public string ClipName { get; set; } public void Interact() { PlayAnimation(_clipName); } private static void OnClipNameChanged(Changed<ObjectAnimator> changed) { changed.Behaviour.OnChangeClipName(); } private void OnChangeClipName() { _animator.CrossFade(ClipName, 0f, 0); } private void PlayAnimation(string clipName) { if (Runner.IsServer) ClipName = clipName; _animator.CrossFade(ClipName, 0f, 0); } }
In snippet above, one of available solution is to use OnChanged callback who trigger each time ClipName changed it value.
-
Below some clip for interactable object animation sync.
-
Some thing to consider, from snippet above, the interactable are indeed part of NetworkBehaviour and that might increased network bandwidth for sending that object update state over network so kindly culling that kind of object when it become irrelevant.
Mecanim-Based Animation
- Sometimes playing animation by directly calling its hash or name isn’t the right way, or for some cases, isn’t enough. In that case, consider using Unity’s mecanim-based animation system.
- Mecanim handles the transition between animations by fulfilling specific conditions specified in transition.
- Photon Fusion is capable to support unity mecanim by utilizing the NetworkMecanimAnimator component.
- NetworkMecanimAnimator works by synchronizing each client mecanim transition values.
- Below some steps order to use NetworkMecanimAnimator :
-
Set up the Mecanim normally in Animator tab, add animation, parameter, transition between animation. for more detailed tutorial regarding unity mecanim, please refer to Unity Official Documentation.

-
Add NetworkMecanimAnimator component in gameobjects who going to play animations.

-
Set Animator field with already-existing animator component.

-
To set value of parameter may be achieved by using Animator.SetFloat() if parameter has float as type data, and many more as written in Unity Documentation.
[SerializeField] private Animator _animator; [SerializeField] private NetworkMecanimAnimator _mecanimAnimator; public void Start() { //calling like this, _animator.SetFloat("Move", 1f); //or like this, _mecanimAnimator?.Animator.SetFloat("Move", 1f); //both are legit. //..rest of the code. }
-
As for handling parameter with Trigger type data, must set its value by calling NetworkMecanimAnimator.SetTrigger(), since trigger are transient and it is possible for the backing bool to reset to false before NetworkMecanimAnimator captures the values of the Animator component.
-
Some insight when using NetworkMecanimAnimator.SetTrigger(), when this test was conducted, NetworkMecanimAnimator.SetTrigger() only trigger on remote and server, leaving local untouched, thus forced to refrain from using NetworkMecanimAnimator.SetTrigger() until matter resolved.
-
Since NetworkMecanimAnimator.SetTrigger() was out of picture, an alternative is proposed, resulting in bool used as trigger substitute. below some snippet for implementation, note : implementation done in Generic State Machine, and set it up inside condition class, kindly check the repository when its available.
public class Button : NetworkBehaviour { [SerializeField] NetworkMecanimAnimator _networkAnimator; protected NetworkButtons Pressed; protected NetworkButtons ButtonPrev; TickTimer _jumpDone; public override void FixedUpdateNetwork() { NetworkButtons _button; if (GetInput(out NetworkInputData input) && Runner.IsForward) { _button = input.buttons; Pressed = input.buttons.GetPressed(ButtonPrev); ButtonPrev = _button; if (Pressed.IsSet(KeycodeButton.Jump)) _jumpDone = TickTimer.CreateFromSeconds(Runner, 0.2f); } if (Object.InputAuthority) _networkAnimator?.Animator.SetBool("Jump Bool", !_jumpDone.ExpiredOrNotRunning(Runner)); } }
public class ButtonPressCondition : Condition { ///..some code... public override bool Statement() { Pressed = Controller.Inputs.Buttons.GetPressed(ButtonPrev); if (StateAnimator.UseMecanim) { if (!TriggerButtonTimer.Expired(LauncherExtension.Launcher.Runner) && TriggerButtonTimer.IsRunning) { return false; } else if (Pressed.IsSet(PressConditionSO.InputButton)) { if (TriggerButtonTimer.ExpiredOrNotRunning(LauncherExtension.Launcher.Runner)) { TriggerButtonTimer = TickTimer.CreateFromSeconds(LauncherExtension.Launcher.Runner, PressConditionSO.ActiveDuration); } } //check if has input authority if (UnitController.PlayerControlled) //check if not resimulation and only forward ticks. if (UnitController.Runner.IsForward) NetworkAnimator?.Animator.SetBool(PressConditionSO.AnimationParameter, Pressed.IsSet(PressConditionSO.InputButton)); } return Pressed.IsSet(PressConditionSO.InputButton); } ///..some code... }
-
The Trigger Alternative make use of TickTimer function in fusion, a helper type for timers. TickTimer works as a sort of delay, preventing bool parameter not changing its value till TickTimer expires.
-
Below quick footage for mecanim animation syncing :
-
Activating Hitbox Through Mecanim Animation
-
In some cases, instead of becoming an aesthetic point, animation may play a huge role in the game loop, like when reaching some frames in animation will trigger animation events will activate hitbox, casting rays, and many others. And since fusion is capable of “Syncing” mecanim animation, safe to say it is able to trigger animation events in some frames.
-
Add animation events via Animation tab (Ctrl+6 for default shortcut), or if it animation inside a imported object (fbx file), add via Animation in Import Settings.

via Animation tab.

via Import Settings,

Set it in Events sections.
-
Regarding Hitbox, fusion has a built-in hitbox class that works best with fusion’s lag compensation. consider utilizing it for hitbox-related cases for best result.
-
As for how set up hitbox, please refer to [Hitbox](Lag Compensated Hitbox & Raycasts a671c7d1ca5041369432d2e46292e348.md) page.
-
When Attack button is activated, attack animation will play then at some frame it will trigger attack hitbox to active and .deactivate it again after end of animation.
[demo_attack_raw.mp4](Sync Animation Handling 7dd9051499c349f4bd068ca11119b092/demo_attack_raw.mp4)
-
As for guarding, it has same mechanic like attacking. it will hide body hitbox when animation reach certain frame.
[demo_guard_raw.mp4](Sync Animation Handling 7dd9051499c349f4bd068ca11119b092/demo_guard_raw.mp4)
-
In order for the server to ‘recognize’ that client hit connects fusion OverlapBox was used instead of Raycasts, as its works best in this kind of situation. please refer to [Hitbox](Lag Compensated Hitbox & Raycasts a671c7d1ca5041369432d2e46292e348.md) page for detailed information.
public void CheckAttack() { if (_controller.PlayerControlled) { _hitbox.SetHitboxActive(_attackHitbox, true); //perform overlapbox check then saved it into list of LagCompensatedHit. _hit = LauncherExtension.Launcher.Runner.LagCompensation.OverlapBox(transform.position + _attackHitbox.Offset, _attackHitbox.BoxExtents * 2f, Quaternion.identity, _controller.PlayerRef, _hits); for (int i = _hit - 1; i >= 0; i--) { //only hit non-self. if (_hits[i].Hitbox.Root.Object.InputAuthority != _controller.PlayerRef) { _tempHitboxActivator = GetHitboxActivator(_hits[i].Hitbox.Root.transform); //register hit if target hitbox is body and not already in hitted state. if (IsHitTargetBody(_tempHitboxActivator, _hits[i].Hitbox)) { _tempHitboxActivator.SetReceivedHit(1); _hitbox.SetHitboxActive(_attackHitbox, false); break; } } } _hitbox.SetHitboxActive(_attackHitbox, false); } }
-
Below is server perspective when hit triggered, or not.
[Hit connects.](Sync Animation Handling 7dd9051499c349f4bd068ca11119b092/demo_attack_server_perspective_.mp4)
Hit connects.
[Evaded.](Sync Animation Handling 7dd9051499c349f4bd068ca11119b092/demo_guard_server_persective.mp4)
Evaded.
-
And below is the full perspective of both client and server.
[demo_full_.mp4](Sync Animation Handling 7dd9051499c349f4bd068ca11119b092/demo_full_.mp4)
No Comments