using Unity.Netcode; using UnityEngine; /// /// Replicates the visual body state that the disabled-on-remote controller / pose system would /// otherwise drive: the owner publishes its selected pose index, resolved locomotion category /// (idle / walk / run / fall) AND the wall-lean / ground-raise / ceiling mesh offset, so a remote /// avatar shows the same pose, walk/run/fall animation and leans into walls / raises off the ground /// like the owner — not just a sliding, upright idle. /// /// Body position/rotation come from the NetworkTransform; this only carries the small extra state on /// top. Pose index and locomotion change rarely (a pose cycle, a locomotion transition), so those /// NetworkVariables send a delta only when the value actually changes. The mesh offset is the visual /// model's child-local position () — a tiny /// vector the owner eases continuously; the remote eases onto the replicated value so it doesn't /// tick-step (the body itself is already interpolated by the NetworkTransform). /// /// 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); }