Sync per-pose index and locomotion state across the network
Add PlayerAnimationSync NetworkBehaviour with owner-authoritative NetworkVariables for pose index and locomotion category (idle/walk/run/fall). Remote avatars switch their PlayerPoseController into network-controlled mode, skipping local input, sprint reset, capsule resize and camera framing, and take pose index plus locomotion externally. Disable pose persistence on network-driven remotes so foreign avatars can't overwrite the local save. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
4fa443f630
commit
7f2f4e5999
5 files changed
+186
-29
No files matched your search
@@ -80,6 +80,18 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
|
||||
private int _currentIndex;
|
||||
|
||||
/// <summary>Locomotion category that selects which clip the active pose plays.</summary>
|
||||
public enum Locomotion { Idle, Walk, Run, Fall }
|
||||
|
||||
// Network drive: a remote avatar's PlayerCharacterController and CharacterController are disabled
|
||||
// (NetworkTransform owns its transform), so its movement-state flags are dead. Instead of reading
|
||||
// local input/physics, such an avatar takes its pose index and locomotion category from the owner
|
||||
// via SetNetworkState (pushed by PlayerAnimationSync). _currentLocomotion is the category resolved
|
||||
// this frame, published by the owner for replication.
|
||||
private bool _networkControlled;
|
||||
private Locomotion _networkLoco;
|
||||
private Locomotion _currentLocomotion;
|
||||
|
||||
// Playables: a 2-input AnimationMixerPlayable that overrides the Animator Controller. Input 1 holds
|
||||
// the current ("to") pose; input 0 holds the outgoing ("from") pose during a crossfade and is empty
|
||||
// once the blend completes. Blending both inputs over a per-frame rest baseline (see below) makes
|
||||
@@ -121,6 +133,26 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
/// <summary>Index of the currently selected pose.</summary>
|
||||
public int CurrentIndex => _currentIndex;
|
||||
|
||||
/// <summary>Locomotion category resolved this frame (which clip the active pose is playing). The owner publishes it for network sync.</summary>
|
||||
public Locomotion CurrentLocomotion => _currentLocomotion;
|
||||
|
||||
/// <summary>True once this avatar is driven by replicated network state instead of local input/physics.</summary>
|
||||
public bool NetworkControlled => _networkControlled;
|
||||
|
||||
/// <summary>
|
||||
/// Switches this avatar to network-driven animation: local input, the sprint-reset, the per-pose
|
||||
/// collision capsule and camera framing are all skipped; pose index and locomotion are supplied by
|
||||
/// <see cref="SetNetworkState"/>. Called on a remote avatar by <see cref="PlayerAnimationSync"/>.
|
||||
/// </summary>
|
||||
public void EnableNetworkDrive() => _networkControlled = true;
|
||||
|
||||
/// <summary>Pushes the replicated pose index and locomotion category onto a network-driven avatar.</summary>
|
||||
public void SetNetworkState(int poseIndex, Locomotion loco)
|
||||
{
|
||||
_currentIndex = WrapIndex(poseIndex);
|
||||
_networkLoco = loco;
|
||||
}
|
||||
|
||||
/// <summary>Eased per-pose y-offset for the first-person eye, applied on top of the camera target by the FirstPersonController.</summary>
|
||||
public float FirstPersonCameraYOffset => _firstPersonCameraOffset;
|
||||
|
||||
@@ -137,40 +169,36 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (CycleKeyPressed())
|
||||
NextPose();
|
||||
// Local control: the cycle key and the sprint reset choose the pose. A network-driven (remote)
|
||||
// avatar skips this — its pose index is pushed in from the owner via SetNetworkState.
|
||||
if (!_networkControlled)
|
||||
{
|
||||
if (CycleKeyPressed())
|
||||
NextPose();
|
||||
|
||||
// Sprinting in any state (ground, air, wall, ceiling) snaps the selection back to the default
|
||||
// pose. Held continuously below, so the reset shows up immediately, not only once back on the floor.
|
||||
if (_player.IsSprinting)
|
||||
_currentIndex = WrapIndex(defaultPoseIndex);
|
||||
// Sprinting in any state (ground, air, wall, ceiling) snaps the selection back to the default
|
||||
// pose. Held continuously, so the reset shows up immediately, not only once back on the floor.
|
||||
if (_player.IsSprinting)
|
||||
_currentIndex = WrapIndex(defaultPoseIndex);
|
||||
}
|
||||
|
||||
PlayerPose pose = ActivePose();
|
||||
|
||||
// Pick the clip for the current locomotion state. Standing still, on a wall or on the ceiling
|
||||
// holds the idle pose; moving on the ground plays the run clip while sprinting or the walk clip
|
||||
// otherwise; falling (airborne and descending past the controller's threshold) plays the fall
|
||||
// clip. All three extra clips are optional — a null one falls back to the idle pose, so an unset
|
||||
// clip keeps the held-pose behaviour. PlayPose crossfades on any change and hands the rig back
|
||||
// Resolve the locomotion category (idle / walk / run / fall) and pick its clip. Locally the
|
||||
// category comes from the controller's movement state; on a remote avatar it is the replicated
|
||||
// value. It is stored so the owner's PlayerAnimationSync can publish it. ClipFor falls back to
|
||||
// the idle clip for an unset optional clip, so a remote avatar (same pose asset) resolves to
|
||||
// exactly the clip the owner shows; PlayPose crossfades on any change and hands the rig back
|
||||
// only when there is no clip at all (empty list / null idle clip).
|
||||
AnimationClip clip = pose ? pose.Clip : null;
|
||||
float speed = pose ? pose.ClipSpeed : 1f;
|
||||
if (pose && _player.IsMovingOnGround)
|
||||
{
|
||||
AnimationClip locomotion = _player.IsSprinting ? pose.RunClip : pose.WalkClip;
|
||||
if (locomotion)
|
||||
{
|
||||
clip = locomotion;
|
||||
speed = _player.IsSprinting ? pose.RunClipSpeed : pose.WalkClipSpeed;
|
||||
}
|
||||
}
|
||||
else if (pose && _player.IsFalling && pose.FallClip)
|
||||
{
|
||||
clip = pose.FallClip;
|
||||
speed = pose.FallClipSpeed;
|
||||
}
|
||||
_currentLocomotion = _networkControlled ? _networkLoco : ResolveLocalLocomotion();
|
||||
AnimationClip clip = ClipFor(pose, _currentLocomotion, out float speed);
|
||||
PlayPose(clip, speed);
|
||||
|
||||
// Collision capsule and camera framing are local-owner concerns: a remote avatar's controller and
|
||||
// CharacterController are disabled and no camera tracks it, so a network-driven avatar stops here.
|
||||
if (_networkControlled)
|
||||
return;
|
||||
|
||||
// CharacterController capsule: each pose carries its own center Y / radius / height so a low pose
|
||||
// (ball, star) can crawl where the default standing capsule is too tall. Eased toward the active
|
||||
// pose's dimensions when the pose changes so the collider resizes smoothly instead of popping; a
|
||||
@@ -235,6 +263,45 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
return poses[Mathf.Clamp(_currentIndex, 0, poses.Count - 1)];
|
||||
}
|
||||
|
||||
/// <summary>Maps the controller's current movement state to a locomotion category for clip selection.</summary>
|
||||
private Locomotion ResolveLocalLocomotion()
|
||||
{
|
||||
if (_player.IsMovingOnGround)
|
||||
return _player.IsSprinting ? Locomotion.Run : Locomotion.Walk;
|
||||
if (_player.IsFalling)
|
||||
return Locomotion.Fall;
|
||||
return Locomotion.Idle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the clip (and its <paramref name="speed"/>) for <paramref name="pose"/> in the given
|
||||
/// locomotion category. The walk/run/fall clips are optional: an unset one falls back to the pose's
|
||||
/// idle clip, so an unauthored category keeps the held-pose behaviour identically on the owner and on
|
||||
/// a network-driven remote avatar. A null pose hands the rig back (no clip).
|
||||
/// </summary>
|
||||
private AnimationClip ClipFor(PlayerPose pose, Locomotion loco, out float speed)
|
||||
{
|
||||
if (pose)
|
||||
{
|
||||
switch (loco)
|
||||
{
|
||||
case Locomotion.Walk when pose.WalkClip:
|
||||
speed = pose.WalkClipSpeed;
|
||||
return pose.WalkClip;
|
||||
case Locomotion.Run when pose.RunClip:
|
||||
speed = pose.RunClipSpeed;
|
||||
return pose.RunClip;
|
||||
case Locomotion.Fall when pose.FallClip:
|
||||
speed = pose.FallClipSpeed;
|
||||
return pose.FallClip;
|
||||
}
|
||||
speed = pose.ClipSpeed;
|
||||
return pose.Clip;
|
||||
}
|
||||
speed = 1f;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eases the CharacterController capsule toward the given target dimensions and writes them through
|
||||
/// the controller. A single normalized blend drives all three values together, so the capsule stays a
|
||||
|
||||
Reference in new issue
Block a user