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:
1 parent
9e19146e35
commit
4c28d1488d
16 files changed
+1973
-12
No files matched your search
@@ -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,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user