Move scripts under Assets/Scripts/ into Components, GameMode, Helper, Network, Enum, Struct and ScriptableObject folders. Pure file moves (plus folder .meta files); no code changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
5.6 KiB
C#
119 lines
5.6 KiB
C#
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// 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 (<see cref="PlayerCharacterController.MeshOffsetTarget"/>) — 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.
|
|
/// </summary>
|
|
[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<int> _poseIndex =
|
|
new(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
|
|
|
|
// Locomotion category stored as a byte (PlayerPoseController.Locomotion has only a handful of values).
|
|
private readonly NetworkVariable<byte> _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<Vector3> _meshOffset =
|
|
new(Vector3.zero, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
|
|
|
|
private PlayerPoseController _pose;
|
|
private PlayerCharacterController _controller;
|
|
private Transform _offsetTarget;
|
|
|
|
void Awake()
|
|
{
|
|
_pose = GetComponent<PlayerPoseController>();
|
|
_controller = GetComponent<PlayerCharacterController>();
|
|
}
|
|
|
|
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);
|
|
}
|