using Unity.Netcode;
using UnityEngine;
///
/// Replicates the visual body state the disabled-on-remote controller / pose system would otherwise
/// drive: the owner publishes its pose index, resolved locomotion category and the wall-lean /
/// ground-raise mesh offset (), so a remote
/// avatar animates like the owner instead of sliding in an upright idle. Body position/rotation come
/// from the NetworkTransform; the remote eases onto the replicated mesh offset so it doesn't tick-step.
/// Runs after the PlayerPoseController (execution order 100) so the owner reads this frame's resolved
/// state; remote avatars apply via OnValueChanged / per-frame easing, independent of frame order.
///
[RequireComponent(typeof(PlayerPoseController))]
[RequireComponent(typeof(PlayerCharacterController))]
[DisallowMultipleComponent]
[DefaultExecutionOrder(200)]
public class PlayerAnimationSync : NetworkBehaviour
{
[Tooltip("How fast a remote avatar eases its mesh lean/raise offset onto the replicated value " +
"(1/second). Higher = tighter follow, lower = smoother. The body itself is interpolated " +
"by the NetworkTransform; this only smooths the small child-local lean offset.")]
[SerializeField] private float meshOffsetFollow = 18f;
private readonly NetworkVariable _poseIndex =
new(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
// Locomotion category stored as a byte (PlayerPoseController.Locomotion has only a handful of values).
private readonly NetworkVariable _locomotion =
new(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
// Visual model's child-local lean/raise offset: wall lean + ground raise + ceiling lean, all folded
// by the controller into MeshOffsetTarget.localPosition. Owner-authoritative like the rest. Already in
// the root's local space, so it stays correct under the synced body rotation (wall/ceiling) as-is.
private readonly NetworkVariable _meshOffset =
new(Vector3.zero, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
private PlayerPoseController _pose;
private PlayerCharacterController _controller;
private Transform _offsetTarget;
void Awake()
{
_pose = GetComponent();
_controller = GetComponent();
}
public override void OnNetworkSpawn()
{
// Resolved after all Awakes ran: the controller resolves MeshOffsetTarget in its own Awake.
_offsetTarget = _controller ? _controller.MeshOffsetTarget : null;
if (IsOwner)
return;
// Remote avatar: stop reading local input/physics and drive the pose from replicated state.
_pose.EnableNetworkDrive();
_poseIndex.OnValueChanged += OnPoseIndexChanged;
_locomotion.OnValueChanged += OnLocomotionChanged;
ApplyRemote(); // spawn-time values are already replicated
// Seed the lean offset to the replicated value so a mid-lean late join doesn't pop from base.
if (_offsetTarget) _offsetTarget.localPosition = _meshOffset.Value;
}
public override void OnNetworkDespawn()
{
_poseIndex.OnValueChanged -= OnPoseIndexChanged;
_locomotion.OnValueChanged -= OnLocomotionChanged;
}
void Update()
{
if (IsOwner)
{
if (!_pose)
return;
// Publish the owner's resolved animation state. Assigning the same value is a no-op for
// replication (the NetworkVariable dirties only on an actual change).
int index = _pose.CurrentIndex;
byte loco = (byte)_pose.CurrentLocomotion;
if (_poseIndex.Value != index) _poseIndex.Value = index;
if (_locomotion.Value != loco) _locomotion.Value = loco;
// Mesh lean/raise offset: the controller owns MeshOffsetTarget.localPosition (sole writer),
// so this frame's value is final by execution order 200.
if (_offsetTarget)
{
Vector3 lp = _offsetTarget.localPosition;
if (_meshOffset.Value != lp) _meshOffset.Value = lp;
}
return;
}
// Remote avatar: ease the visual model onto the replicated lean/raise offset. The controller is
// disabled here, so this is the sole writer of MeshOffsetTarget.localPosition on the remote (the
// pose reset already excludes that node). Easing avoids tick-rate stepping on the small offset.
if (_offsetTarget)
{
float k = 1f - Mathf.Exp(-meshOffsetFollow * Time.deltaTime);
_offsetTarget.localPosition = Vector3.Lerp(_offsetTarget.localPosition, _meshOffset.Value, k);
}
}
private void OnPoseIndexChanged(int previous, int current) => ApplyRemote();
private void OnLocomotionChanged(byte previous, byte current) => ApplyRemote();
private void ApplyRemote() =>
_pose.SetNetworkState(_poseIndex.Value, (PlayerPoseController.Locomotion)_locomotion.Value);
}