Smooth free-camera flight and look

FreeCamController: ease movement via dt-correct Vector3.SmoothDamp on a world-space velocity (accel/decel ramp) and lag the look with Mathf.SmoothDampAngle. Both are framerate-independent and collapse to instant at smoothTime 0; smoothed state is snapped on activation (SyncToCurrentView) so entering freecam never jumps to catch up.

Bundles accumulated deferred work: PlayerScaler uniform-scale helper, scale-aware wall/ceiling climb probes and the double-scale skinned-collider bake fix, plus user content authoring (Sit pose + animation, font SDF, scene/prefab edits).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-27 11:49:43 +02:00
1 parent f66ebbf10a
commit 8c89e95014
14 files changed
+12083 -360

No files matched your search

+29 -16
View File
@@ -636,7 +636,7 @@ public partial class PlayerCharacterController : MonoBehaviour
if (!found)
{
// Wall only: losing the center ray while climbing up the face = top edge → mantle.
if (!onCeiling && alongSurface.y > 0f && WallInFront(_controller.height * 0.5f))
if (!onCeiling && alongSurface.y > 0f && WallInFront(WorldHalfHeight))
{
_mantling = true;
_mantleClearedFeet = false;
@@ -695,8 +695,8 @@ public partial class PlayerCharacterController : MonoBehaviour
// longer moves the capsule (only the visual mesh), so it always rests against the wall.
// The controller's depenetration never fights back, so there's no buzz and no quantizing
// deadzone is needed. Exponential follow keeps it framerate-stable; clamp guards spikes.
const float maxPerpSpeed = 8f;
float gapError = _controller.skinWidth - gap;
float maxPerpSpeed = 8f * CapsuleScale;
float gapError = _controller.skinWidth * CapsuleScale - gap;
float follow = 1f - Mathf.Exp(-wallOffsetFollow * dt);
velocity += _wallNormal * Mathf.Clamp(gapError * follow / dt, -maxPerpSpeed, maxPerpSpeed);
}
@@ -877,6 +877,19 @@ public partial class PlayerCharacterController : MonoBehaviour
cameraTarget.localPosition = _cameraTargetBaseLocalPos + Vector3.up * y;
}
// The CharacterController's serialized center/radius/height are authored at the player's base
// (scale-1) size; transform.localScale resizes the ACTUAL collision capsule via lossyScale but
// leaves those serialized numbers untouched. Every climb probe below raycasts in WORLD space, so
// it must use the scaled (world) capsule dimensions and scale its world-distance paddings too --
// otherwise the wall grab range, head-to-ceiling gap and mantle distance are all measured at the
// authored size on a shrunk/grown player (PlayerScaler scales the tunables but not these probes).
// Uniform player scale is enforced by PlayerScaler, so a single lossyScale component suffices.
private float CapsuleScale => transform.lossyScale.y;
private Vector3 WorldCapsuleCenter => transform.TransformPoint(_controller.center);
private float WorldRadius => _controller.radius * CapsuleScale;
private float WorldHalfHeight => _controller.height * 0.5f * CapsuleScale;
private const float ProbeReachSlack = 0.5f; // extra ray length beyond the capsule surface (scale-1 units)
/// <summary>
/// Raycasts from the character center toward the surface (along -_wallNormal) to find the
/// current normal and the gap (surface clearance) to it. The capsule's support distance
@@ -888,10 +901,10 @@ public partial class PlayerCharacterController : MonoBehaviour
normal = _wallNormal;
gap = 0f;
Vector3 origin = transform.position + _controller.center;
Vector3 origin = WorldCapsuleCenter;
Vector3 dir = -_wallNormal;
float extent = CapsuleExtent(dir);
float maxDist = extent + wallMaxOffset + 0.5f;
float maxDist = extent + wallMaxOffset + ProbeReachSlack * CapsuleScale;
int count = Physics.RaycastNonAlloc(
origin, dir, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
@@ -921,8 +934,8 @@ public partial class PlayerCharacterController : MonoBehaviour
/// </summary>
private float CapsuleExtent(Vector3 dir)
{
float radius = _controller.radius;
return radius + Mathf.Abs(dir.y) * (_controller.height * 0.5f - radius);
float radius = WorldRadius;
return radius + Mathf.Abs(dir.y) * (WorldHalfHeight - radius);
}
/// <summary>
@@ -934,9 +947,9 @@ public partial class PlayerCharacterController : MonoBehaviour
normal = Vector3.down;
gap = 0f;
Vector3 origin = transform.position + _controller.center;
float half = _controller.height * 0.5f;
float maxDist = half + wallMaxOffset + 0.5f;
Vector3 origin = WorldCapsuleCenter;
float half = WorldHalfHeight;
float maxDist = half + wallMaxOffset + ProbeReachSlack * CapsuleScale;
int count = Physics.RaycastNonAlloc(
origin, Vector3.up, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
@@ -980,7 +993,7 @@ public partial class PlayerCharacterController : MonoBehaviour
{
// Phase 1: climb straight up until no wall is left in front of the feet.
_controller.Move(Vector3.up * (speed * dt));
if (!WallInFront(_controller.height * 0.5f))
if (!WallInFront(WorldHalfHeight))
_mantleClearedFeet = true;
}
else
@@ -988,7 +1001,7 @@ public partial class PlayerCharacterController : MonoBehaviour
// Phase 2: move forward onto the top surface, then finish.
_controller.Move(forward * (speed * dt));
_mantleOverDist += speed * dt;
if (_mantleOverDist >= _controller.radius * 2f + 0.15f || _controller.isGrounded)
if (_mantleOverDist >= WorldRadius * 2f + 0.15f * CapsuleScale || _controller.isGrounded)
{
_reentryCooldown = TopOutRegrabCooldown;
ExitWallClimb();
@@ -1010,8 +1023,8 @@ public partial class PlayerCharacterController : MonoBehaviour
/// </summary>
private bool WallInFront(float downOffset)
{
Vector3 origin = transform.position + _controller.center - Vector3.up * downOffset;
float maxDist = _controller.radius + wallMaxOffset + 0.5f;
Vector3 origin = WorldCapsuleCenter - Vector3.up * downOffset;
float maxDist = WorldRadius + wallMaxOffset + ProbeReachSlack * CapsuleScale;
int count = Physics.RaycastNonAlloc(
origin, -_wallNormal, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
@@ -1032,8 +1045,8 @@ public partial class PlayerCharacterController : MonoBehaviour
/// </summary>
private bool WalkableGroundBelow()
{
Vector3 origin = transform.position + _controller.center;
float maxDist = _controller.center.y + _controller.stepOffset;
Vector3 origin = WorldCapsuleCenter;
float maxDist = (_controller.center.y + _controller.stepOffset) * CapsuleScale;
int count = Physics.RaycastNonAlloc(
origin, Vector3.down, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);