diff --git a/Assets/Scripts/PlayerCharacterController.cs b/Assets/Scripts/PlayerCharacterController.cs index e697844..80f1d1b 100644 --- a/Assets/Scripts/PlayerCharacterController.cs +++ b/Assets/Scripts/PlayerCharacterController.cs @@ -7,13 +7,13 @@ using UnityEngine.InputSystem; /// CharacterController-based player: camera-relative WASD movement, manual gravity /// and jumping, sliding off too-steep slopes/edges, and a wall-climb mode. /// -/// Wall-climb (entered on wall/ceiling contact): W/S climb straight up/down the wall at a -/// constant speed, A/D strafe left/right relative to the camera, and the body always faces -/// the wall. Holding the left mouse button and dragging sets the wall pose (vertical = lean +/// Wall-climb (entered by pushing the move input into a wall): W/S climb straight up/down the +/// wall at a constant speed, A/D strafe left/right relative to the camera, and the body always +/// faces the wall. Holding the left mouse button and dragging sets the wall pose (vertical = lean /// out from / back to the wall, horizontal = turn the body) and pauses the camera orbit; the /// lean-out offset eases back to zero while moving, the turn is held until leaving the wall. /// On leaving, the body eases back to its movement direction. Jumping detaches and works -/// normally; re-grab is blocked while rising and resumes once falling near a wall. +/// normally; re-grab is blocked while rising and resumes once falling and pushing into a wall. /// Reaching the top edge mantles the character over onto the surface. /// /// Named PlayerCharacterController to avoid a clash with Unity.VisualScripting's @@ -65,13 +65,17 @@ public class PlayerCharacterController : MonoBehaviour public float slideControl = 0.3f; [Header("Wall Climb")] - [Tooltip("Enter wall-climb mode on wall/ceiling contact: stick to the surface, move at reduced speed, ignore gravity and jumping.")] + [Tooltip("Enter wall-climb mode by pushing into a wall: stick to the surface, move at reduced speed, ignore gravity and jumping.")] public bool enableWallClimb = true; [Tooltip("Minimum surface angle from 'up' (degrees) that counts as a climbable wall/ceiling rather than a slope.")] [Range(50f, 180f)] public float wallMinAngle = 80f; + [Tooltip("How directly the move input must point into a wall to grab it (0 = any inward nudge, 1 = head-on only). Stops mere contact (e.g. standing at a wall's base) from grabbing.")] + [Range(0f, 1f)] + public float wallGrabMinPush = 0.3f; + [Tooltip("Speed multiplier for climbing up and strafing while wall-climbing (0.5 = half speed).")] [Range(0f, 1f)] public float wallClimbSpeedMultiplier = 0.5f; @@ -120,6 +124,7 @@ public class PlayerCharacterController : MonoBehaviour private float _wallOffset; // current lean-out distance from the wall (mouse drag) private float _wallYaw; // body yaw offset from facing the wall (mouse drag); held until leaving private bool _offsetDragging; // true while the offset/rotation is being set (pauses camera orbit) + private bool _climbLeftGround; // true once the capsule has left the ground while climbing (enables ground-detach) private readonly RaycastHit[] _wallProbe = new RaycastHit[8]; // Mantle (top-out) state. @@ -185,7 +190,7 @@ public class PlayerCharacterController : MonoBehaviour moveDir = inputDir; } - UpdateWallClimbState(); + UpdateWallClimbState(moveDir); // Latch the wall normal for this frame, then reset the per-frame contact // accumulators (OnControllerColliderHit refills them during Move). @@ -260,10 +265,11 @@ public class PlayerCharacterController : MonoBehaviour } /// - /// Enters wall-climb mode on any wall/ceiling contact. Leaving while climbing - /// is handled in . + /// Enters wall-climb mode only when the move input actively pushes into a wall + /// (not on mere contact). Leaving while climbing is handled in + /// . /// - private void UpdateWallClimbState() + private void UpdateWallClimbState(Vector3 moveDir) { if (_reentryCooldown > 0f) _reentryCooldown -= Time.deltaTime; @@ -275,11 +281,22 @@ public class PlayerCharacterController : MonoBehaviour return; } + // Grab only when the move input points into the contacted wall, not on mere + // contact: standing at a wall's base or sliding down onto the ground keeps + // contact without pushing in, so it won't (re-)grab there. + bool pushingIntoWall = false; + if (_wallContactThisFrame && moveDir.sqrMagnitude > 1e-4f + && _wallNormalAccum.sqrMagnitude > 1e-6f) + { + Vector3 wallNormal = _wallNormalAccum.normalized; + pushingIntoWall = Vector3.Dot(moveDir.normalized, wallNormal) <= -wallGrabMinPush; + } + // Re-grab only while falling/standing (not while rising from a jump) and // only under player control; once climbing we stay attached across control // toggles (e.g. paint mode) so switching modes never drops us off the wall. // The cooldown prevents instantly re-grabbing the edge we just mantled over. - if (!_isWallClimbing && ControlEnabled && _wallContactThisFrame + if (!_isWallClimbing && ControlEnabled && pushingIntoWall && _reentryCooldown <= 0f && _verticalVelocity <= 0f) { _isWallClimbing = true; @@ -288,6 +305,7 @@ public class PlayerCharacterController : MonoBehaviour _slideSpeed = 0f; _wallOffset = 0f; _wallYaw = 0f; + _climbLeftGround = false; } } @@ -330,11 +348,17 @@ public class PlayerCharacterController : MonoBehaviour // W/S climb straight up/down the wall at a constant speed (camera pitch never // tilts or slows them); the camera only sets left/right through A/D. Vector3 wallUp = Vector3.ProjectOnPlane(Vector3.up, _wallNormal); - Vector3 wallRight = cameraTransform - ? Vector3.ProjectOnPlane(cameraTransform.right, _wallNormal) - : Vector3.zero; if (wallUp.sqrMagnitude > 1e-6f) wallUp.Normalize(); + + // Strafe along the wall's own horizontal tangent, not the live camera-right vector: + // feeding camera-right in here coupled movement to the orbit camera's per-frame + // re-aim, and that feedback loop jittered sideways motion. The camera now only picks + // which way is "right" (sign), so A/D stay camera-relative without the wobble. + Vector3 wallRight = Vector3.Cross(_wallNormal, wallUp); if (wallRight.sqrMagnitude > 1e-6f) wallRight.Normalize(); + if (cameraTransform && Vector3.Dot(cameraTransform.right, wallRight) < 0f) + wallRight = -wallRight; + Vector3 alongWall = wallUp * v + wallRight * h; if (alongWall.sqrMagnitude > 1f) alongWall.Normalize(); bool hasMoveInput = (h * h + v * v) > 1e-4f; @@ -382,14 +406,29 @@ public class PlayerCharacterController : MonoBehaviour velocity += alongWall.normalized * climbSpeed; if (found) { - // Drive the gap toward the desired offset; the closed loop also keeps light - // contact when flush (offset 0). Clamp guards against probe spikes. + // Hold the chosen standoff (offset, 0 = flush) without fighting the controller's + // depenetration. A deadzone wider than the skin-width settling distance ignores + // sub-threshold gap noise that otherwise buzzed the body back and forth along the + // wall normal. Clamp guards against probe spikes. const float maxPerpSpeed = 8f; - velocity += _wallNormal * Mathf.Clamp((_wallOffset - gap) / dt, -maxPerpSpeed, maxPerpSpeed); + float gapError = _wallOffset - gap; + if (Mathf.Abs(gapError) > _controller.skinWidth + 0.02f) + velocity += _wallNormal * Mathf.Clamp(gapError / dt, -maxPerpSpeed, maxPerpSpeed); } _controller.Move(velocity * dt); + // Reaching the ground detaches and drops back to normal grounded movement. Gated on + // having left the ground first, so grabbing a wall while standing at its base (still + // grounded) doesn't instantly detach before any climbing has happened. + if (!_controller.isGrounded) + _climbLeftGround = true; + else if (_climbLeftGround) + { + ExitWallClimb(); + return; + } + // Always face the wall (forward = into the wall), turned by the held mouse-drag yaw. // This holds the whole climb; on leaving, the ground rotation eases the body back to // its movement direction (RotateTowards, so it interpolates rather than snapping). @@ -513,6 +552,7 @@ public class PlayerCharacterController : MonoBehaviour _wallOffset = 0f; _wallYaw = 0f; _offsetDragging = false; + _climbLeftGround = false; _mantling = false; _mantleClearedFeet = false; _mantleOverDist = 0f;