Add movement accel/decel, camera smoothing, and Running animator hook

- Smooth acceleration/deceleration on ground and air movement
- Interpolated scroll zoom and low-pass smoothed orbit rotation
- Drive Animator "Running" bool while moving on the ground
- Add Paintman animator controllers and animation clips
- Update README and GAME_DESIGN docs for sprint, accel, camera smoothing and animator hookup

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-22 22:35:50 +02:00
1 parent 9e19146e35
commit 4c28d1488d
16 files changed
+1973 -12

No files matched your search

+38 -1
View File
@@ -35,12 +35,23 @@ public class PlayerCharacterController : MonoBehaviour
[Tooltip("Speed multiplier applied to ground movement while the sprint key (Left Shift) is held.")]
public float sprintSpeedMultiplier = 1.8f;
[Tooltip("How quickly ground movement ramps up to the target speed (units/second²). Lower = more gradual start.")]
public float acceleration = 30f;
[Tooltip("How quickly ground movement slows to a stop when input eases off (units/second²). Lower = more glide.")]
public float deceleration = 45f;
[Tooltip("Interpret WASD relative to the camera (off = world X/Z axes).")]
public bool cameraRelative = true;
[Tooltip("Camera used for camera-relative movement. Empty = Camera.main.")]
public Transform cameraTransform;
public Animator animator;
[Tooltip("Animator bool parameter driven true while running on the ground (false in air and while wall/ceiling-climbing).")]
public string runningParameter = "Running";
[Header("Rotation")]
[Tooltip("Smoothly turn the character to face its movement direction.")]
public bool rotateTowardMovement = true;
@@ -124,8 +135,13 @@ public class PlayerCharacterController : MonoBehaviour
/// <summary>True while in wall-climb mode.</summary>
public bool IsWallClimbing => _isWallClimbing;
// 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
private CharacterController _controller;
private int _runningHash;
private float _verticalVelocity;
private Vector3 _horizontalVelocity; // smoothed horizontal move velocity (accel/decel ramp)
// Ground contact normal from the latest Move (for slope sliding).
private Vector3 _groundNormal = Vector3.up;
@@ -158,6 +174,7 @@ public class PlayerCharacterController : MonoBehaviour
void Awake()
{
_controller = GetComponent<CharacterController>();
_runningHash = Animator.StringToHash(runningParameter);
if (!cameraTransform && Camera.main)
cameraTransform = Camera.main.transform;
if (!cameraOrbit)
@@ -233,6 +250,8 @@ public class PlayerCharacterController : MonoBehaviour
_offsetDragging = false;
if (cameraOrbit)
cameraOrbit.LookSuppressed = _offsetDragging;
_horizontalVelocity = Vector3.zero; // drop ground momentum so resuming starts from rest
SetAnimatorRunning(false); // climbing is its own pose, not the ground run
return;
}
@@ -272,7 +291,12 @@ public class PlayerCharacterController : MonoBehaviour
}
float currentSpeed = sprinting ? moveSpeed * sprintSpeedMultiplier : moveSpeed;
Vector3 velocity = moveDir * currentSpeed + slideVelocity + Vector3.up * _verticalVelocity;
// Ease horizontal velocity toward the target so movement accelerates/decelerates
// instead of snapping. Accel while there is input, decel when it eases off.
Vector3 targetHorizontal = moveDir * currentSpeed;
float accelRate = moveDir.sqrMagnitude > 0.0001f ? acceleration : deceleration;
_horizontalVelocity = Vector3.MoveTowards(_horizontalVelocity, targetHorizontal, accelRate * Time.deltaTime);
Vector3 velocity = _horizontalVelocity + slideVelocity + Vector3.up * _verticalVelocity;
// Move fires OnControllerColliderHit, which refills the ground normal;
// fall back to "up" if nothing was hit.
@@ -287,6 +311,12 @@ public class PlayerCharacterController : MonoBehaviour
transform.rotation = Quaternion.RotateTowards(
transform.rotation, target, rotationSpeed * Time.deltaTime);
}
// Run animation: only while grounded and actually moving (airborne/sliding-blocked = idle).
// Real horizontal speed, so pushing into a wall (blocked) reads as idle, not foot-sliding.
Vector3 horizontalVel = _controller.velocity;
horizontalVel.y = 0f;
SetAnimatorRunning(_controller.isGrounded && horizontalVel.sqrMagnitude > RunningSpeedThresholdSqr);
}
/// <summary>
@@ -750,6 +780,13 @@ public class PlayerCharacterController : MonoBehaviour
#endif
}
/// <summary>Drives the animator's ground-run bool (null-safe if no animator is assigned).</summary>
private void SetAnimatorRunning(bool running)
{
if (animator)
animator.SetBool(_runningHash, running);
}
/// <summary>
/// Collects contact normals during Move: wall/ceiling contacts feed the
/// wall-climb detector, the flattest upward contact becomes the ground normal.
+38 -5
View File
@@ -38,6 +38,9 @@ public class ThirdPersonOrbitInput : MonoBehaviour
[Tooltip("Invert vertical look (mouse up tilts the camera down).")]
public bool invertY = false;
[Tooltip("Eases orbit rotation so quick mouse moves ramp in/out (higher = snappier, 0 = raw/instant).")]
public float rotationSmoothing = 20f;
[Header("Zoom")]
[Tooltip("Mouse wheel zooms the camera in and out (changes orbit radius).")]
public bool enableZoom = true;
@@ -45,6 +48,9 @@ public class ThirdPersonOrbitInput : MonoBehaviour
[Tooltip("World units the orbit radius changes per mouse wheel notch.")]
public float zoomSpeed = 0.5f;
[Tooltip("Eases the orbit radius toward the zoom target (higher = snappier, 0 = instant).")]
public float zoomSmoothing = 10f;
[Tooltip("Closest orbit radius (zoom in).")]
public float minRadius = 1.5f;
@@ -56,6 +62,9 @@ public class ThirdPersonOrbitInput : MonoBehaviour
public bool manageCursor = true;
CinemachineOrbitalFollow _orbit;
Vector2 _smoothedLook; // low-pass-filtered look delta (deg/frame) for eased rotation
float _targetRadius; // zoom radius the actual radius eases toward
bool _zoomInit; // _targetRadius seeded from the live radius yet?
/// <summary>Current orbit mode. Setting it also refreshes the cursor state.</summary>
public OrbitLookMode LookMode
@@ -95,23 +104,47 @@ public class ThirdPersonOrbitInput : MonoBehaviour
if (modeWantsLook && !LookSuppressed)
{
Vector2 delta = ReadMouseDelta();
if (delta.sqrMagnitude > 0f)
Vector2 raw = new Vector2(
delta.x * horizontalSensitivity,
delta.y * verticalSensitivity * (invertY ? 1f : -1f));
// Ease the per-frame look toward the raw delta so fast flicks ramp in/out
// instead of snapping; steady motion still converges to full sensitivity.
Vector2 applied = rotationSmoothing > 0f
? (_smoothedLook = Vector2.Lerp(_smoothedLook, raw, 1f - Mathf.Exp(-rotationSmoothing * Time.deltaTime)))
: raw;
if (applied.sqrMagnitude > 0f)
{
ApplyAxisDelta(ref _orbit.HorizontalAxis, delta.x * horizontalSensitivity);
float pitch = delta.y * verticalSensitivity * (invertY ? 1f : -1f);
ApplyAxisDelta(ref _orbit.VerticalAxis, pitch);
ApplyAxisDelta(ref _orbit.HorizontalAxis, applied.x);
ApplyAxisDelta(ref _orbit.VerticalAxis, applied.y);
}
}
else
{
_smoothedLook = Vector2.zero;
}
if (enableZoom)
{
if (!_zoomInit)
{
_targetRadius = _orbit.Radius;
_zoomInit = true;
}
float scroll = ReadScroll();
if (!Mathf.Approximately(scroll, 0f))
{
// One wheel notch = one zoom step (sign only), independent of the
// platform's raw wheel magnitude. Scrolling up zooms in.
_orbit.Radius = Mathf.Clamp(_orbit.Radius - Mathf.Sign(scroll) * zoomSpeed, minRadius, maxRadius);
_targetRadius = Mathf.Clamp(_targetRadius - Mathf.Sign(scroll) * zoomSpeed, minRadius, maxRadius);
}
// Ease the actual radius toward the target (frame-rate independent).
_orbit.Radius = zoomSmoothing > 0f
? Mathf.Lerp(_orbit.Radius, _targetRadius, 1f - Mathf.Exp(-zoomSmoothing * Time.deltaTime))
: _targetRadius;
}
}