Camera-relative wall climb up/down keys to fix grab/release twitch

When running backwards toward the camera into a wall, the grab decided on
the camera-relative world direction (S pushes into the wall -> grab) while
the base release decided on the raw S axis (v < 0 -> immediately let go),
causing a per-frame grab/release flicker.

Mirror the existing _wallStrafeSign A/D latch with a new _wallClimbSign:
the camera picks, per climb-key press, which of W/S climbs up (the one
pushing into the wall) via Dot(camForward, wallNormal) and latches it.
All up/down tests now run through the effective climb axis vEff -
wall->ceiling handoff, top-out detach/hop, the alongSurface mapping and
the base release - so grab and release can no longer contradict each
other. Reset on grab and in ExitWallClimb. Facing the wall and grabbing
with W is unchanged (vEff == v).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-27 13:20:15 +02:00
1 parent cfb767f6cb
commit 0a1859cab1
2 files changed
+65 -10

No files matched your search

+33 -10
View File
@@ -238,6 +238,7 @@ public partial class PlayerCharacterController : MonoBehaviour
private float _wallOffset; // signed mesh lean (mouse drag): + = out from the wall, - = into it; moves only the model, not the capsule
private float _wallYaw; // body yaw offset from facing the wall (mouse drag); held until leaving
private int _wallStrafeSign; // latched sideways direction (camera picks it once per strafe); 0 = unlatched
private int _wallClimbSign; // latched up/down direction: whichever W/S key pushes into the wall climbs UP (camera picks it once per climb); 0 = unlatched
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 Vector3 _meshBaseLocalPos; // meshOffsetTarget's authored local position; the lean offset is applied relative to this
@@ -503,6 +504,7 @@ public partial class PlayerCharacterController : MonoBehaviour
_meshOffsetResidual = 0f; // fresh grab: the climb path owns the mesh, drop any leftover ease
_wallYaw = 0f;
_wallStrafeSign = 0;
_wallClimbSign = 0;
_climbLeftGround = false;
}
}
@@ -533,6 +535,26 @@ public partial class PlayerCharacterController : MonoBehaviour
UpdateCeilingClass();
bool onCeiling = _onCeiling;
// Map W/S to UP/DOWN by which key pushes INTO the wall, the same way A/D's _wallStrafeSign
// picks sideways from the camera: whichever vertical key drives the player toward the wall
// climbs up. Facing the wall that's W; with the camera on the wall's side (walking backward
// into it) it's S. Without this remap the grab (decided on the camera-relative world
// direction) and the base-release (decided on raw S) disagree, so the climb flickers on/off.
// Latched once per press and held until released; only walls remap (the ceiling crawl is
// already camera-relative). vEff is the effective climb axis used for every up/down test below.
bool climbing = Mathf.Abs(v) > 1e-4f;
if (!climbing)
_wallClimbSign = 0;
else if (_wallClimbSign == 0 && !onCeiling)
{
Vector3 camFwd = cameraTransform ? cameraTransform.forward : Vector3.zero;
camFwd.y = 0f;
// camFwd aligned with the outward wall normal = camera faces away from the wall, so
// forward (W) moves away from it and S is the into-wall, climb-up key.
_wallClimbSign = (cameraTransform && Vector3.Dot(camFwd, _wallNormal) > 0f) ? -1 : 1;
}
float vEff = _wallClimbSign != 0 ? v * _wallClimbSign : v;
// Jump detaches. Off a wall it pushes straight up; off a ceiling it just lets go and
// drops. Re-grab stays blocked appropriately by UpdateWallClimbState's push-in test.
if (jumpPressed)
@@ -544,7 +566,7 @@ public partial class PlayerCharacterController : MonoBehaviour
// Wall->ceiling hand-off: while climbing up a wall into an overhang, switch to the
// ceiling right above the head instead of trying to mantle over a (non-existent) top.
if (!onCeiling && v > 0f
if (!onCeiling && vEff > 0f
&& ProbeCeiling(out Vector3 ceilNormal, out float ceilHeadGap)
&& ceilHeadGap <= ceilingGrabDistance)
{
@@ -603,7 +625,7 @@ public partial class PlayerCharacterController : MonoBehaviour
{
// Hop over the lip only when actually moving up onto the flattened face (a real
// top-out); if we reached it going down, just step off cleanly with no hop.
if (v > 0f) TopOutDetach();
if (vEff > 0f) TopOutDetach();
else ExitWallClimb();
return;
}
@@ -640,7 +662,7 @@ public partial class PlayerCharacterController : MonoBehaviour
if (_wallStrafeSign < 0)
wallRight = -wallRight;
alongSurface = wallUp * v + wallRight * h;
alongSurface = wallUp * vEff + wallRight * h;
}
if (alongSurface.sqrMagnitude > 1f) alongSurface.Normalize();
bool hasMoveInput = (h * h + v * v) > 1e-4f;
@@ -716,10 +738,10 @@ public partial class PlayerCharacterController : MonoBehaviour
_controller.Move(velocity * dt);
// Reaching walkable ground detaches back to normal grounded movement. The top-out hop
// (carry over the lip) fires ONLY when topping out upward over an edge (v > 0). Arriving
// at the floor by descending, OR pressing down/away (S, v < 0) at the wall's base while
// grounded, just lets go cleanly with no hop -- the latter is the only detach path that
// doesn't require having left the ground first, so a wall grabbed at its base can be
// (carry over the lip) fires ONLY when topping out upward over an edge (vEff > 0). Arriving
// at the floor by descending, OR pressing down/away (the effective down key, vEff < 0) at the
// wall's base while grounded, just lets go cleanly with no hop -- the latter is the only detach
// path that doesn't require having left the ground first, so a wall grabbed at its base can be
// released by pressing away. We test walkable ground under the feet rather than trusting
// isGrounded: the standoff correction can lift the capsule just off the surface, so at a
// slope's top isGrounded stays false and the old check never fired.
@@ -728,12 +750,12 @@ public partial class PlayerCharacterController : MonoBehaviour
{
_climbLeftGround = true;
}
else if (_climbLeftGround && v > 0f)
else if (_climbLeftGround && vEff > 0f)
{
TopOutDetach(); // topped out upward onto walkable ground: hop over the lip (holding W carries on)
TopOutDetach(); // topped out upward onto walkable ground: hop over the lip (holding the up key carries on)
return;
}
else if (_climbLeftGround || v < 0f)
else if (_climbLeftGround || vEff < 0f)
{
ExitWallClimb(); // descended to the floor, or pulling down/away at the base: let go, no hop
return;
@@ -1108,6 +1130,7 @@ public partial class PlayerCharacterController : MonoBehaviour
_wallOffset = 0f;
_wallYaw = 0f;
_wallStrafeSign = 0;
_wallClimbSign = 0;
_offsetDragging = false;
_climbLeftGround = false;
_mantling = false;