diff --git a/Assets/Scripts/NetworkPlayerSetup.cs b/Assets/Scripts/NetworkPlayerSetup.cs index 4f28e39..b358f3e 100644 --- a/Assets/Scripts/NetworkPlayerSetup.cs +++ b/Assets/Scripts/NetworkPlayerSetup.cs @@ -28,5 +28,16 @@ public class NetworkPlayerSetup : NetworkBehaviour if (controller) controller.enabled = false; var cc = GetComponent(); if (cc) cc.enabled = false; + + // Owner cleanup: the concealment meter is a per-frame skinned-mesh bake + voxelization + + // physics overlap whose result only feeds the LOCAL player (HUD / gameplay). A remote avatar's + // concealment is computed on its own owner's machine, so running it here is pure wasted CPU. + var concealment = GetComponentInChildren(true); + if (concealment) concealment.enabled = false; + + // NOTE: SkinnedColliderBaker deliberately stays ENABLED on remotes. MeshPainter's stamp replay + // (ApplyNetworkStamp -> DrawBrushGpu) rasterizes the baker's live, per-frame BakedMesh to map a + // 3D brush placement into UV space; freezing the bake would mis-map replicated paint onto any + // non-rest pose. Its bake is the necessary cost of correct remote paint, not wasted work. } } diff --git a/Assets/Scripts/PlayerAnimationSync.cs b/Assets/Scripts/PlayerAnimationSync.cs index 494d0fb..5dbb55c 100644 --- a/Assets/Scripts/PlayerAnimationSync.cs +++ b/Assets/Scripts/PlayerAnimationSync.cs @@ -2,23 +2,33 @@ using Unity.Netcode; using UnityEngine; /// -/// Replicates the pose system's animation state across the network. The owner publishes its selected -/// pose index and resolved locomotion category (idle / walk / run / fall); remote avatars switch their -/// to network-driven mode and play the matching clip, so a remote -/// player shows the same pose and walk/run/fall animation the owner sees — not just a sliding idle. +/// 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. /// -/// Position/rotation come from the NetworkTransform; this only carries the small animation state. Both -/// values are owner-authoritative and change rarely (a pose cycle, a locomotion transition), so the -/// NetworkVariables send a delta only when the value actually changes. +/// 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, independent of frame order. +/// 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); @@ -26,12 +36,27 @@ public class PlayerAnimationSync : NetworkBehaviour private readonly NetworkVariable _locomotion = new(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); - private PlayerPoseController _pose; + // 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); - void Awake() => _pose = GetComponent(); + 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; @@ -40,6 +65,9 @@ public class PlayerAnimationSync : NetworkBehaviour _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() @@ -50,16 +78,36 @@ public class PlayerAnimationSync : NetworkBehaviour void Update() { - if (!IsOwner || !_pose) - return; + 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), but guard anyway to keep - // intent clear. - int index = _pose.CurrentIndex; - byte loco = (byte)_pose.CurrentLocomotion; - if (_poseIndex.Value != index) _poseIndex.Value = index; - if (_locomotion.Value != loco) _locomotion.Value = loco; + // 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();