Replace ceiling Y offset with per-pose CharacterController capsule
Each PlayerPose now carries its own collider center Y, radius and height, eased and routed to the CharacterController via a SetCapsule single-writer. This lets crawl poses (Ball/Star) shrink the capsule to fit under furniture. The ceiling Y offset is removed; poses without authored sizes fall back to the base capsule, so existing poses keep their collision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
13de058819
commit
52f9ace9e0
6 files changed
+144
-59
No files matched your search
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
using UnityEngine.Playables;
|
||||
using UnityEngine.Serialization;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
@@ -22,15 +23,16 @@ using UnityEngine.InputSystem;
|
||||
/// <see cref="AnimationMixerPlayable"/> on an <see cref="AnimationPlayableOutput"/>) that drives the
|
||||
/// Animator directly. Switching clip — between poses, or between idle/walk/run within a pose —
|
||||
/// crossfades the outgoing and incoming clips over <see cref="poseBlendDuration"/> instead of snapping.
|
||||
/// The per-pose ceiling y-offset is eased toward its target and routed through the controller (the
|
||||
/// single writer of the model-offset transform) so it composes with the wall-lean instead of fighting it.
|
||||
/// A separate per-pose camera y-offset (plus that same ceiling offset) is likewise eased and routed to the
|
||||
/// controller's camera target, framing each pose for the ThirdPersonCamera without moving the body.
|
||||
/// Each pose also carries its own CharacterController capsule (center Y / radius / height): the active
|
||||
/// pose's dimensions are eased toward and written through the controller so a low pose (ball, star) can
|
||||
/// crawl under furniture the default standing capsule is too tall for. A per-pose camera y-offset is
|
||||
/// likewise eased and routed to the controller's camera target, framing each pose for the
|
||||
/// ThirdPersonCamera without moving the body.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(PlayerCharacterController))]
|
||||
[DisallowMultipleComponent]
|
||||
// Run after PlayerCharacterController (default order 0) so the ceiling / sprint state polled below is
|
||||
// this frame's, and the ceiling offset resolves without a one-frame seam.
|
||||
// Run after PlayerCharacterController (default order 0) so the sprint / locomotion state polled below is
|
||||
// this frame's, and the per-pose capsule / camera offset resolve without a one-frame seam.
|
||||
[DefaultExecutionOrder(100)]
|
||||
public partial class PlayerPoseController : MonoBehaviour
|
||||
{
|
||||
@@ -56,9 +58,16 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
[Min(0f)]
|
||||
[SerializeField] private float poseBlendDuration = 0.2f;
|
||||
|
||||
[Tooltip("Approach speed (local units/sec) for easing the ceiling y-offset toward its target. 0 = instant.")]
|
||||
[Tooltip("Approach speed (local units/sec) for easing the per-pose camera-target y-offset toward its " +
|
||||
"target while cycling poses. 0 = instant.")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float ceilingOffsetSpeed = 4f;
|
||||
[FormerlySerializedAs("ceilingOffsetSpeed")]
|
||||
[SerializeField] private float cameraOffsetSpeed = 4f;
|
||||
|
||||
[Tooltip("Blend speed (0..1 per second) for resizing the CharacterController capsule toward the active " +
|
||||
"pose's dimensions when the pose changes. 0 = instant snap; higher = snappier resize.")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float capsuleBlendSpeed = 8f;
|
||||
|
||||
[Tooltip("Apply Mecanim foot IK to the played humanoid clips. Off (default) plays each pose exactly as " +
|
||||
"authored. On bends the knees to plant the feet on the IK goal, which makes a straight-knee idle " +
|
||||
@@ -85,8 +94,16 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
private float _blend; // 0..1 weight of the "to" input; 1 = fully on the target pose
|
||||
private bool _blending;
|
||||
|
||||
private float _ceilingOffset; // eased local-up offset currently fed to the controller (mesh + camera)
|
||||
private float _cameraPoseOffset; // eased per-pose camera-target offset; the ceiling offset is added on top when routed to the controller
|
||||
private float _cameraPoseOffset; // eased per-pose camera-target offset routed to the controller's camera target
|
||||
|
||||
// Capsule blend: the CharacterController dimensions are eased toward the active pose's via a single
|
||||
// normalized blend (so the capsule stays a valid shape throughout) and written through the controller.
|
||||
// Snapped on the first frame so the player spawns at its pose size with no resize blend.
|
||||
private bool _capsuleStarted;
|
||||
private float _capFromCenterY, _capFromRadius, _capFromHeight;
|
||||
private float _capToCenterY, _capToRadius, _capToHeight;
|
||||
private float _capCenterY, _capRadius, _capHeight;
|
||||
private float _capBlend;
|
||||
|
||||
// Rest pose snapshot of the whole Animator subtree, taken in Awake before the Animator first
|
||||
// evaluates. An AnimationClipPlayable only writes the bones its curves cover, so a clip that
|
||||
@@ -94,7 +111,7 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
// bone frozen on the previous animation. Restoring this before each pose start makes every pose
|
||||
// define the full body. The controller's model-offset transform (meshOffsetTarget) is explicitly
|
||||
// dropped from this set in CaptureRestPose — with the humanoid rig the Animator sits on that node,
|
||||
// so it is the root of this subtree, and resetting it would wipe the wall-lean / ceiling offset.
|
||||
// so it is the root of this subtree, and resetting it would wipe the wall-lean / ground raise.
|
||||
private Transform[] _restBones;
|
||||
private Vector3[] _restPos;
|
||||
private Quaternion[] _restRot;
|
||||
@@ -144,38 +161,37 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
}
|
||||
PlayPose(clip, speed);
|
||||
|
||||
// Ceiling y-offset: only while stuck to the ceiling. Ease toward the target instead of
|
||||
// snapping, so entering and leaving the ceiling (and switching ceiling poses) glides. Routed
|
||||
// through the controller so it shares the wall-lean's single write of the model transform.
|
||||
float ceilingTarget = (pose && _player.IsOnCeiling) ? pose.CeilingYOffset : 0f;
|
||||
_ceilingOffset = ceilingOffsetSpeed > 0f
|
||||
? Mathf.MoveTowards(_ceilingOffset, ceilingTarget, ceilingOffsetSpeed * Time.deltaTime)
|
||||
: ceilingTarget;
|
||||
_player.SetPoseLocalYOffset(_ceilingOffset);
|
||||
// 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
|
||||
// pose that leaves them unset (height/radius <= 0) keeps the default capsule.
|
||||
bool hasCapsule = pose && pose.ControllerHeight > 0f && pose.ControllerRadius > 0f;
|
||||
float capCenterY = hasCapsule ? pose.ControllerCenterY : _player.BaseCapsuleCenterY;
|
||||
float capRadius = hasCapsule ? pose.ControllerRadius : _player.BaseCapsuleRadius;
|
||||
float capHeight = hasCapsule ? pose.ControllerHeight : _player.BaseCapsuleHeight;
|
||||
ApplyCapsule(capCenterY, capRadius, capHeight);
|
||||
|
||||
// Camera target: a per-pose framing offset (applied in any state) plus the same ceiling offset,
|
||||
// routed to the ThirdPersonCamera's follow target only (never the body). The pose part eases like
|
||||
// the ceiling offset so cycling poses glides the camera; the ceiling part reuses the value above so
|
||||
// camera and model stay in lockstep on the ceiling.
|
||||
// Camera target: a per-pose framing offset (applied in any state), eased so cycling poses glides
|
||||
// the camera, routed to the ThirdPersonCamera's follow target only (never the body).
|
||||
float cameraPoseTarget = pose ? pose.CameraTargetYOffset : 0f;
|
||||
_cameraPoseOffset = ceilingOffsetSpeed > 0f
|
||||
? Mathf.MoveTowards(_cameraPoseOffset, cameraPoseTarget, ceilingOffsetSpeed * Time.deltaTime)
|
||||
_cameraPoseOffset = cameraOffsetSpeed > 0f
|
||||
? Mathf.MoveTowards(_cameraPoseOffset, cameraPoseTarget, cameraOffsetSpeed * Time.deltaTime)
|
||||
: cameraPoseTarget;
|
||||
_player.SetCameraTargetLocalYOffset(_cameraPoseOffset + _ceilingOffset);
|
||||
_player.SetCameraTargetLocalYOffset(_cameraPoseOffset);
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
// Hand the rig back to the Animator Controller and never leak a graph; rebuilt lazily on the
|
||||
// next idle frame. Also clear any ceiling offset we were holding.
|
||||
// next idle frame. Also clear the camera offset and restore the default capsule we were holding.
|
||||
ReleasePose();
|
||||
_ceilingOffset = 0f;
|
||||
_cameraPoseOffset = 0f;
|
||||
if (_player)
|
||||
{
|
||||
_player.SetPoseLocalYOffset(0f);
|
||||
_player.SetCameraTargetLocalYOffset(0f);
|
||||
_player.SetCapsule(_player.BaseCapsuleCenterY, _player.BaseCapsuleRadius, _player.BaseCapsuleHeight);
|
||||
}
|
||||
_capsuleStarted = false; // re-snap to the active pose's capsule if this is re-enabled
|
||||
}
|
||||
|
||||
/// <summary>Advances to the next pose, wrapping at the end of the list.</summary>
|
||||
@@ -200,6 +216,43 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
return poses[Mathf.Clamp(_currentIndex, 0, poses.Count - 1)];
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// valid shape (and its floor contact moves linearly) throughout the resize. The first call snaps, so
|
||||
/// the player spawns at its pose size without a resize blend; <see cref="capsuleBlendSpeed"/> 0 also snaps.
|
||||
/// </summary>
|
||||
private void ApplyCapsule(float centerY, float radius, float height)
|
||||
{
|
||||
if (!_capsuleStarted)
|
||||
{
|
||||
_capsuleStarted = true;
|
||||
_capFromCenterY = _capToCenterY = _capCenterY = centerY;
|
||||
_capFromRadius = _capToRadius = _capRadius = radius;
|
||||
_capFromHeight = _capToHeight = _capHeight = height;
|
||||
_capBlend = 1f;
|
||||
}
|
||||
else if (centerY != _capToCenterY || radius != _capToRadius || height != _capToHeight)
|
||||
{
|
||||
// New target (pose changed): ease from wherever the capsule currently is.
|
||||
_capFromCenterY = _capCenterY;
|
||||
_capFromRadius = _capRadius;
|
||||
_capFromHeight = _capHeight;
|
||||
_capToCenterY = centerY;
|
||||
_capToRadius = radius;
|
||||
_capToHeight = height;
|
||||
_capBlend = 0f;
|
||||
}
|
||||
|
||||
_capBlend = capsuleBlendSpeed > 0f
|
||||
? Mathf.MoveTowards(_capBlend, 1f, capsuleBlendSpeed * Time.deltaTime)
|
||||
: 1f;
|
||||
_capCenterY = Mathf.Lerp(_capFromCenterY, _capToCenterY, _capBlend);
|
||||
_capRadius = Mathf.Lerp(_capFromRadius, _capToRadius, _capBlend);
|
||||
_capHeight = Mathf.Lerp(_capFromHeight, _capToHeight, _capBlend);
|
||||
_player.SetCapsule(_capCenterY, _capRadius, _capHeight);
|
||||
}
|
||||
|
||||
private bool CycleKeyPressed()
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
@@ -354,7 +407,7 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
}
|
||||
|
||||
// Drop the model-offset transform from the reset set so the per-frame ResetToRestPose never
|
||||
// touches it: the wall-lean / ceiling offset (PlayerCharacterController.SetMeshLean) is its sole
|
||||
// touches it: the wall-lean / ground raise (PlayerCharacterController.SetMeshLean) is its sole
|
||||
// writer. With the humanoid rig the Animator shares this node, putting it in the captured subtree;
|
||||
// resetting it each frame is exactly what was wiping the offsets. Nulling the slot makes
|
||||
// ResetToRestPose's null-guard skip it. _player is resolved (and meshOffsetTarget too, since the
|
||||
|
||||
Reference in new issue
Block a user