From 0a1859cab18dee1f5d4a9bf2c49e806663de0e9c Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Sat, 27 Jun 2026 13:20:15 +0200 Subject: [PATCH] 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 --- Assets/Prefabs/Player.prefab | 32 +++++++++++++++ Assets/Scripts/PlayerCharacterController.cs | 43 ++++++++++++++++----- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/Assets/Prefabs/Player.prefab b/Assets/Prefabs/Player.prefab index c7cb008..6d10101 100644 --- a/Assets/Prefabs/Player.prefab +++ b/Assets/Prefabs/Player.prefab @@ -1,5 +1,36 @@ %YAML 1.1 %TAG !u! tag:unity3d.com,2011: +--- !u!1 &3409234855347943236 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3881790915792787113} + m_Layer: 0 + m_Name: FirstPersonTarget + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &3881790915792787113 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3409234855347943236} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1.2, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 6997860362608943113} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &5384024074013944205 GameObject: m_ObjectHideFlags: 0 @@ -36,6 +67,7 @@ Transform: m_Children: - {fileID: 4756699837331941863} - {fileID: 4348940595172967010} + - {fileID: 3881790915792787113} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!143 &1765171223029840803 diff --git a/Assets/Scripts/PlayerCharacterController.cs b/Assets/Scripts/PlayerCharacterController.cs index 4a800eb..fcbfd83 100644 --- a/Assets/Scripts/PlayerCharacterController.cs +++ b/Assets/Scripts/PlayerCharacterController.cs @@ -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;