diff --git a/Assets/Poses/Pose_Ball.asset b/Assets/Poses/Pose_Ball.asset
index 698b2d6..426028f 100644
--- a/Assets/Poses/Pose_Ball.asset
+++ b/Assets/Poses/Pose_Ball.asset
@@ -18,5 +18,7 @@ MonoBehaviour:
walkClipSpeed: 1
runClip: {fileID: 0}
runClipSpeed: 1
- ceilingYOffset: 0.84
+ controllerCenterY: 0.25
+ controllerRadius: 0.15
+ controllerHeight: 0.5
cameraTargetYOffset: -0.5
diff --git a/Assets/Poses/Pose_Star.asset b/Assets/Poses/Pose_Star.asset
index a405109..d473c34 100644
--- a/Assets/Poses/Pose_Star.asset
+++ b/Assets/Poses/Pose_Star.asset
@@ -18,5 +18,7 @@ MonoBehaviour:
walkClipSpeed: 1
runClip: {fileID: 0}
runClipSpeed: 1
- ceilingYOffset: 1
+ controllerCenterY: 0.12
+ controllerRadius: 0.1
+ controllerHeight: 0.3
cameraTargetYOffset: -0.55
diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity
index c216439..6bf83a8 100644
--- a/Assets/Scenes/SampleScene.unity
+++ b/Assets/Scenes/SampleScene.unity
@@ -4633,7 +4633,7 @@ MonoBehaviour:
wallOffsetReturnSpeed: 2
wallOffsetFollow: 12
wallExitGrace: 0.15
- ceilingGrabDistance: 0.3
+ ceilingGrabDistance: 0.02
wallTopOutBoost: 0.3
cameraOrbit: {fileID: 0}
meshOffsetTarget: {fileID: 0}
@@ -4660,7 +4660,8 @@ MonoBehaviour:
defaultPoseIndex: 0
cycleKey: 17
poseBlendDuration: 0.2
- ceilingOffsetSpeed: 4
+ cameraOffsetSpeed: 4
+ capsuleBlendSpeed: 8
applyFootIK: 0
--- !u!114 &1141039644
MonoBehaviour:
diff --git a/Assets/Scripts/PlayerCharacterController.cs b/Assets/Scripts/PlayerCharacterController.cs
index 42d5aad..7847c45 100644
--- a/Assets/Scripts/PlayerCharacterController.cs
+++ b/Assets/Scripts/PlayerCharacterController.cs
@@ -127,7 +127,7 @@ public partial class PlayerCharacterController : MonoBehaviour
public float wallExitGrace = 0.15f;
[Tooltip("Head-to-ceiling distance (units) at which climbing a wall up into an overhang hands off to the ceiling.")]
- public float ceilingGrabDistance = 0.3f;
+ public float ceilingGrabDistance = 0.02f;
[Tooltip("Upward hop when topping out over a wall/slope edge, so holding W carries you onto the ledge instead of dropping back onto the face and re-grabbing. In meters of jump height; 0 = no hop.")]
public float wallTopOutBoost = 0.3f;
@@ -185,6 +185,16 @@ public partial class PlayerCharacterController : MonoBehaviour
///
public Transform MeshOffsetTarget => meshOffsetTarget;
+ /// Authored CharacterController capsule center Y, captured in Awake. The per-pose capsule
+ /// (driven by ) eases back to this when no pose overrides it.
+ public float BaseCapsuleCenterY { get; private set; }
+
+ /// Authored CharacterController capsule radius, captured in Awake (see ).
+ public float BaseCapsuleRadius { get; private set; }
+
+ /// Authored CharacterController capsule height, captured in Awake (see ).
+ public float BaseCapsuleHeight { get; private set; }
+
// Below this horizontal speed (m/s, squared) the character counts as idle for the run animation.
private const float RunningSpeedThresholdSqr = 0.04f; // (0.2 m/s)^2
@@ -194,7 +204,6 @@ public partial class PlayerCharacterController : MonoBehaviour
private bool _sprinting; // sprint key state this frame (exposed via IsSprinting)
private bool _isIdle; // snapshot: player essentially still this frame (exposed via IsIdle)
private bool _movingOnGround; // snapshot: grounded & moving above the run threshold (exposed via IsMovingOnGround)
- private float _poseYOffset; // local-up model offset from the active pose (ceiling only); folded into SetMeshLean
private float _groundOffset; // signed vertical mesh raise from the ground mouse pose-adjust (+ up / - down); folded into SetMeshLean
// Ground contact normal from the latest Move (for slope sliding).
@@ -234,6 +243,9 @@ public partial class PlayerCharacterController : MonoBehaviour
void Awake()
{
_controller = GetComponent();
+ BaseCapsuleCenterY = _controller.center.y;
+ BaseCapsuleRadius = _controller.radius;
+ BaseCapsuleHeight = _controller.height;
if (!cameraTransform && Camera.main)
cameraTransform = Camera.main.transform;
if (!cameraOrbit)
@@ -720,7 +732,7 @@ public partial class PlayerCharacterController : MonoBehaviour
Vector3 worldOffset = dir * offset;
meshOffsetTarget.localPosition = _meshBaseLocalPos
+ transform.InverseTransformVector(worldOffset)
- + Vector3.up * (_poseYOffset + _groundOffset); // ceiling pose y-offset + ground mouse raise (both 0 unless in use)
+ + Vector3.up * _groundOffset; // ground mouse raise (0 unless in use)
}
///
@@ -773,21 +785,19 @@ public partial class PlayerCharacterController : MonoBehaviour
}
///
- /// Sets a local-up offset applied to the visual model only, composed into the wall-lean's single
- /// write of . Used by to hang a
- /// ceiling pose at the right height; 0 = none. Re-applies immediately so a change shows this frame.
+ /// Resizes the CharacterController capsule (center Y, radius, height) to the active pose's dimensions,
+ /// driven by so a low pose can fit where the default standing capsule
+ /// is too tall. Only the collision capsule changes — the visual model and camera target are untouched.
+ /// Cached so an unchanged value skips the write (a resize re-bakes the capsule internally).
///
- public void SetPoseLocalYOffset(float y)
+ public void SetCapsule(float centerY, float radius, float height)
{
- if (_poseYOffset == y) return;
- _poseYOffset = y;
- if (!meshOffsetTarget) return;
- // While climbing the lean is the live _wallOffset along _wallNormal; otherwise re-apply along
- // whatever residual lean is easing home (both 0 when there's none, restoring the authored pose).
- if (_isWallClimbing)
- SetMeshLean(_wallNormal, _wallOffset);
- else
- SetMeshLean(_meshOffsetExitNormal, _meshOffsetResidual);
+ Vector3 c = _controller.center;
+ if (c.y == centerY && _controller.radius == radius && _controller.height == height) return;
+ c.y = centerY;
+ _controller.center = c;
+ _controller.radius = radius;
+ _controller.height = height;
}
///
diff --git a/Assets/Scripts/PlayerPose.cs b/Assets/Scripts/PlayerPose.cs
index fd1e68d..382c54a 100644
--- a/Assets/Scripts/PlayerPose.cs
+++ b/Assets/Scripts/PlayerPose.cs
@@ -3,8 +3,9 @@ using UnityEngine;
///
/// A selectable player pose: an idle held while the player stands still,
/// plus optional walk and run clips that the pose system plays itself (the Animator stays empty) while
-/// moving on the ground, and a y-offset applied to the visual model only while stuck to the ceiling
-/// (so the pose hangs at the right height). The first ScriptableObject in this project.
+/// moving on the ground, its own CharacterController capsule dimensions (so a low pose like a ball can
+/// crawl under furniture the default standing capsule is too tall for), and a camera-target y-offset that
+/// frames the pose without moving the body. The first ScriptableObject in this project.
///
/// Named PlayerPose (not Pose) to avoid shadowing the built-in UnityEngine.Pose struct, mirroring
/// why the controller is PlayerCharacterController and not just "PlayerController".
@@ -37,14 +38,24 @@ public class PlayerPose : ScriptableObject
[Range(0.1f, 3f)]
[SerializeField] private float runClipSpeed = 1f;
- [Tooltip("Local-up offset added to the visual model ONLY while stuck to the ceiling, so the pose " +
- "hangs at the right height. Ignored on the ground and on walls. Meters.")]
- [SerializeField] private float ceilingYOffset;
+ [Header("Collision")]
+ [Tooltip("CharacterController capsule center Y while this pose is active (meters, local space). Lower " +
+ "this together with Height so a short pose still rests its feet on the floor.")]
+ [SerializeField] private float controllerCenterY = 0.66f;
+
+ [Tooltip("CharacterController capsule radius while this pose is active (meters). Unity clamps it to " +
+ "half the height.")]
+ [Min(0.01f)]
+ [SerializeField] private float controllerRadius = 0.1f;
+
+ [Tooltip("CharacterController capsule height while this pose is active (meters). Lower it (with Center " +
+ "Y) so a ball/star pose can crawl under a table or couch the default capsule is too tall for.")]
+ [Min(0.01f)]
+ [SerializeField] private float controllerHeight = 1.3f;
[Tooltip("Local-up offset applied to the camera target (the ThirdPersonCamera's follow/look target) " +
"while this pose is active, in ANY state. Frames the pose by raising/lowering what the camera " +
- "tracks, without moving the body. The ceiling offset is added on top of this while on the " +
- "ceiling. Meters.")]
+ "tracks, without moving the body. Meters.")]
[SerializeField] private float cameraTargetYOffset;
/// Idle clip held while the player stands still in this pose (may be null).
@@ -65,8 +76,14 @@ public class PlayerPose : ScriptableObject
/// Playback speed multiplier for the run clip (1 = authored speed).
public float RunClipSpeed => runClipSpeed;
- /// Local-up offset applied to the model only while on the ceiling.
- public float CeilingYOffset => ceilingYOffset;
+ /// CharacterController capsule center Y applied while this pose is active.
+ public float ControllerCenterY => controllerCenterY;
+
+ /// CharacterController capsule radius applied while this pose is active.
+ public float ControllerRadius => controllerRadius;
+
+ /// CharacterController capsule height applied while this pose is active.
+ public float ControllerHeight => controllerHeight;
/// Local-up offset applied to the camera target while this pose is active (any state).
public float CameraTargetYOffset => cameraTargetYOffset;
diff --git a/Assets/Scripts/PlayerPoseController.cs b/Assets/Scripts/PlayerPoseController.cs
index 39d166f..0f66ce8 100644
--- a/Assets/Scripts/PlayerPoseController.cs
+++ b/Assets/Scripts/PlayerPoseController.cs
@@ -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;
/// on an ) that drives the
/// Animator directly. Switching clip — between poses, or between idle/walk/run within a pose —
/// crossfades the outgoing and incoming clips over 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.
///
[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
}
/// Advances to the next pose, wrapping at the end of the list.
@@ -200,6 +216,43 @@ public partial class PlayerPoseController : MonoBehaviour
return poses[Mathf.Clamp(_currentIndex, 0, poses.Count - 1)];
}
+ ///
+ /// 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; 0 also snaps.
+ ///
+ 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