Wall-climb mesh lean + separate climb/mantle/sprint speeds; reworked rig
PlayerCharacterController: - Wall lean now moves only the visual model + its paint collider (auto-resolved mesh root parenting both renderer and bones), not the CharacterController capsule, so the mesh can sink into the wall (wallMaxInset) as well as lean out (wallMaxOffset); capsule stays flush. - Lean eases back to the authored pose when leaving the wall instead of snapping. - Sprint multiplier now also applies while wall/ceiling climbing. - Separate wallMantleSpeedMultiplier for pulling up over the top edge, independent of the climb speed. Assets: - Re-exported paintman_UV.FBX and added Paintman.blend: rescaled armature to mesh, merged seam duplicate vertices, fresh automatic weights, inner-thigh weight fix (no webbing on leg spread). - Updated anims, animator controller, font SDF and scene. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
cda1373979
commit
d867c617df
10 files changed
+15698
-2821
No files matched your search
@@ -9,10 +9,11 @@ using UnityEngine.InputSystem;
|
||||
///
|
||||
/// 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 up/down leans out from / back to
|
||||
/// the wall; holding the right mouse button and dragging left/right turns the body. Either pauses
|
||||
/// the camera orbit; the lean-out offset eases back to zero while moving, the turn is held until
|
||||
/// leaving the wall.
|
||||
/// faces the wall. Holding the left mouse button and dragging up/down leans the visual mesh out
|
||||
/// of or into the wall (only the model + its paint collider move; the capsule stays put, so the
|
||||
/// lean can sink into the wall); holding the right mouse button and dragging left/right turns the
|
||||
/// body. Either pauses the camera orbit; the lean 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 and pushing into a wall.
|
||||
/// Reaching the top edge mantles the character over onto the surface.
|
||||
@@ -33,7 +34,7 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
[Tooltip("Horizontal movement speed (units/second).")]
|
||||
public float moveSpeed = 4f;
|
||||
|
||||
[Tooltip("Speed multiplier applied to ground movement while the sprint key (Left Shift) is held.")]
|
||||
[Tooltip("Speed multiplier applied to ground movement and wall/ceiling climbing 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.")]
|
||||
@@ -100,8 +101,15 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
[Range(0f, 1f)]
|
||||
public float wallClimbSpeedMultiplier = 0.5f;
|
||||
|
||||
[Tooltip("Farthest the character can lean out from the wall via the mouse-drag offset (units).")]
|
||||
public float wallMaxOffset = 1.2f;
|
||||
[Tooltip("Speed multiplier for the mantle (pulling up over the top edge), independent of the climb speed. Multiplies moveSpeed; 1 = full walk speed, >1 = snappier pull-up.")]
|
||||
[Min(0f)]
|
||||
public float wallMantleSpeedMultiplier = 0.5f;
|
||||
|
||||
[Tooltip("Farthest the mesh can lean out from the wall via the mouse-drag offset (units).")]
|
||||
public float wallMaxOffset = .2f;
|
||||
|
||||
[Tooltip("Farthest the mesh can sink INTO the wall via the mouse-drag offset (units). Only the visual model + its paint collider move; the CharacterController capsule stays put, so this isn't blocked by the wall collider.")]
|
||||
public float wallMaxInset = .2f;
|
||||
|
||||
[Tooltip("Units the lean-out offset changes per pixel of vertical mouse drag (left button held).")]
|
||||
public float wallOffsetSensitivity = 0.01f;
|
||||
@@ -127,6 +135,9 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
[Tooltip("Camera orbit input; paused while dragging the wall-climb offset. Auto-found if empty.")]
|
||||
[SerializeField] private ThirdPersonOrbitInput cameraOrbit;
|
||||
|
||||
[Tooltip("Visual model root moved by the wall-climb lean offset (into/out of the wall) while the capsule stays put. Must parent both the skinned mesh and its bones. Auto-resolved from the SkinnedMeshRenderer if empty.")]
|
||||
[SerializeField] private Transform meshOffsetTarget;
|
||||
|
||||
/// <summary>
|
||||
/// When false, movement/jump input is ignored but gravity and ground contact
|
||||
/// keep running. Toggled by the GameModeManager.
|
||||
@@ -158,11 +169,14 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
private Vector3 _wallNormalAccum; // summed wall normals during the latest Move
|
||||
private bool _wallContactThisFrame;
|
||||
private float _lostContactTimer;
|
||||
private float _wallOffset; // current lean-out distance from the wall (mouse drag)
|
||||
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 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
|
||||
private float _meshOffsetResidual; // leftover mesh lean still easing back to 0 after leaving the wall (no longer climbing)
|
||||
private Vector3 _meshOffsetExitNormal; // wall normal latched at exit; the residual eases along it (live _wallNormal may shift on stray contact)
|
||||
private readonly RaycastHit[] _wallProbe = new RaycastHit[8];
|
||||
|
||||
// Mantle (top-out) state.
|
||||
@@ -181,6 +195,21 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
cameraTransform = Camera.main.transform;
|
||||
if (!cameraOrbit)
|
||||
cameraOrbit = FindFirstObjectByType<ThirdPersonOrbitInput>();
|
||||
|
||||
// Resolve the model root to offset: walk up from the skinned mesh to the topmost node still
|
||||
// under this transform, so it parents BOTH the renderer and its bones. Offsetting only the
|
||||
// renderer would move just the baked paint collider, not the bone-driven visible mesh.
|
||||
if (!meshOffsetTarget)
|
||||
{
|
||||
var smr = GetComponentInChildren<SkinnedMeshRenderer>();
|
||||
if (smr)
|
||||
{
|
||||
Transform t = smr.transform;
|
||||
while (t.parent && t.parent != transform) t = t.parent;
|
||||
meshOffsetTarget = t;
|
||||
}
|
||||
}
|
||||
if (meshOffsetTarget) _meshBaseLocalPos = meshOffsetTarget.localPosition;
|
||||
}
|
||||
|
||||
void Update()
|
||||
@@ -247,7 +276,7 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
// Frozen on the wall while control is off (e.g. paint mode): keep the
|
||||
// grip, no movement or gravity, so switching modes never drops us.
|
||||
if (ControlEnabled)
|
||||
ApplyWallClimbMovement(h, v, moveDir, jumpPressed, Time.deltaTime);
|
||||
ApplyWallClimbMovement(h, v, moveDir, jumpPressed, sprinting, Time.deltaTime);
|
||||
else
|
||||
_offsetDragging = false;
|
||||
if (cameraOrbit)
|
||||
@@ -260,6 +289,9 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
if (cameraOrbit)
|
||||
cameraOrbit.LookSuppressed = false;
|
||||
|
||||
// Glide the leftover wall lean back to the authored pose (set when leaving the wall).
|
||||
EaseMeshOffsetToRest(Time.deltaTime);
|
||||
|
||||
// A ground normal steeper than the slope limit is too steep to stand on
|
||||
// (also covers balancing on a sharp box edge).
|
||||
float slopeAngle = Vector3.Angle(_groundNormal, Vector3.up);
|
||||
@@ -365,6 +397,7 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
_verticalVelocity = 0f;
|
||||
_slideSpeed = 0f;
|
||||
_wallOffset = 0f;
|
||||
_meshOffsetResidual = 0f; // fresh grab: the climb path owns the mesh, drop any leftover ease
|
||||
_wallYaw = 0f;
|
||||
_wallStrafeSign = 0;
|
||||
_climbLeftGround = false;
|
||||
@@ -375,14 +408,15 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
/// Movement while climbing, for both walls and ceilings (distinguished by the surface
|
||||
/// normal). On a WALL: W/S climb straight up/down at a constant speed (camera pitch never
|
||||
/// tilts them), A/D strafe left/right relative to the camera, the body faces the wall, the
|
||||
/// left mouse button + vertical drag leans out via <see cref="_wallOffset"/>, and the right
|
||||
/// left mouse button + vertical drag leans the mesh out of / into the wall via
|
||||
/// <see cref="_wallOffset"/> (capsule stays put), and the right
|
||||
/// mouse button + horizontal drag turns the body via <see cref="_wallYaw"/>. On a
|
||||
/// CEILING: the body stays upright and moves WASD camera-relative in the horizontal plane
|
||||
/// (like on the ground), facing the move direction; the lean-out drag pulls down off the
|
||||
/// ceiling. Surface normal and gap are re-measured each frame; reaching a wall's top edge
|
||||
/// mantles, climbing a wall up into an overhang hands off to the ceiling.
|
||||
/// </summary>
|
||||
private void ApplyWallClimbMovement(float h, float v, Vector3 moveDir, bool jumpPressed, float dt)
|
||||
private void ApplyWallClimbMovement(float h, float v, Vector3 moveDir, bool jumpPressed, bool sprinting, float dt)
|
||||
{
|
||||
_offsetDragging = false;
|
||||
|
||||
@@ -495,10 +529,11 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// Left button + vertical drag sets the lean-out offset (mouse down = away from the
|
||||
// surface; down off a ceiling); right button + horizontal drag turns the body (wall
|
||||
// only). The offset eases back to zero while moving and never detaches, the turn is
|
||||
// held until leaving the wall. Either drag pauses the camera orbit.
|
||||
// Left button + vertical drag sets the lean offset (mouse up = out from the surface, mouse
|
||||
// down = into it; the offset is signed and moves only the visual mesh, so it can sink into
|
||||
// the wall where the capsule can't); right button + horizontal drag turns the body (wall
|
||||
// only). The offset eases back to zero while moving and never detaches, the turn is held
|
||||
// until leaving the wall. Either drag pauses the camera orbit.
|
||||
bool offsetDrag = found && IsLeftMouseHeld();
|
||||
bool yawDrag = found && IsRightMouseHeld();
|
||||
if (offsetDrag || yawDrag)
|
||||
@@ -506,7 +541,7 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
_offsetDragging = true;
|
||||
Vector2 drag = ReadMouseDelta();
|
||||
if (offsetDrag)
|
||||
_wallOffset = Mathf.Clamp(_wallOffset - drag.y * wallOffsetSensitivity, 0f, wallMaxOffset);
|
||||
_wallOffset = Mathf.Clamp(_wallOffset - drag.y * wallOffsetSensitivity, -wallMaxInset, wallMaxOffset);
|
||||
if (yawDrag)
|
||||
_wallYaw += drag.x * wallRotateSensitivity;
|
||||
}
|
||||
@@ -514,19 +549,21 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
{
|
||||
_wallOffset = Mathf.MoveTowards(_wallOffset, 0f, wallOffsetReturnSpeed * dt);
|
||||
}
|
||||
ApplyMeshOffset(_wallOffset);
|
||||
|
||||
float climbSpeed = moveSpeed * wallClimbSpeedMultiplier;
|
||||
if (sprinting) climbSpeed *= sprintSpeedMultiplier;
|
||||
Vector3 velocity = Vector3.zero;
|
||||
if (alongSurface.sqrMagnitude > 1e-6f)
|
||||
velocity += alongSurface.normalized * climbSpeed;
|
||||
if (found)
|
||||
{
|
||||
// Smoothly approach the chosen standoff. Target one skin-width beyond the offset so
|
||||
// the capsule rests just outside the surface (offset 0 = flush): the controller's
|
||||
// depenetration never fights back, so there's no buzz and no quantizing deadzone is
|
||||
// needed. Exponential follow keeps it framerate-stable; clamp guards probe spikes.
|
||||
// Smoothly hold the capsule one skin-width off the surface (flush): the lean offset no
|
||||
// longer moves the capsule (only the visual mesh), so it always rests against the wall.
|
||||
// The controller's depenetration never fights back, so there's no buzz and no quantizing
|
||||
// deadzone is needed. Exponential follow keeps it framerate-stable; clamp guards spikes.
|
||||
const float maxPerpSpeed = 8f;
|
||||
float gapError = (_wallOffset + _controller.skinWidth) - gap;
|
||||
float gapError = _controller.skinWidth - gap;
|
||||
float follow = 1f - Mathf.Exp(-wallOffsetFollow * dt);
|
||||
velocity += _wallNormal * Mathf.Clamp(gapError * follow / dt, -maxPerpSpeed, maxPerpSpeed);
|
||||
}
|
||||
@@ -588,6 +625,38 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offsets only the visual model (mesh + its baked paint collider) along the wall normal by the
|
||||
/// given signed distance (+ = out from the wall, - = into it), relative to the model's authored
|
||||
/// local position. The CharacterController capsule is untouched, so the lean can sink into the
|
||||
/// wall where the capsule would be blocked. Applied in the player's local space so it tracks the
|
||||
/// body yaw; offset 0 restores the authored pose.
|
||||
/// </summary>
|
||||
private void ApplyMeshOffset(float offset) => SetMeshLean(_wallNormal, offset);
|
||||
|
||||
/// <summary>
|
||||
/// Places the visual model at the given signed lean along <paramref name="dir"/> (world space),
|
||||
/// relative to its authored local pose. Local-space conversion tracks the live body yaw.
|
||||
/// </summary>
|
||||
private void SetMeshLean(Vector3 dir, float offset)
|
||||
{
|
||||
if (!meshOffsetTarget) return;
|
||||
Vector3 worldOffset = dir * offset;
|
||||
meshOffsetTarget.localPosition = _meshBaseLocalPos + transform.InverseTransformVector(worldOffset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eases the leftover lean back to the authored pose after leaving the wall, so the model glides
|
||||
/// home instead of snapping. Runs every non-climbing frame at the same rate as the in-climb
|
||||
/// return; the direction is the wall normal latched at exit (a fresh grab clears the residual).
|
||||
/// </summary>
|
||||
private void EaseMeshOffsetToRest(float dt)
|
||||
{
|
||||
if (_meshOffsetResidual == 0f) return;
|
||||
_meshOffsetResidual = Mathf.MoveTowards(_meshOffsetResidual, 0f, wallOffsetReturnSpeed * dt);
|
||||
SetMeshLean(_meshOffsetExitNormal, _meshOffsetResidual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raycasts from the character center toward the surface (along -_wallNormal) to find the
|
||||
/// current normal and the gap (surface clearance) to it. The capsule's support distance
|
||||
@@ -681,7 +750,7 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
private void MantleOverEdge(float dt)
|
||||
{
|
||||
_mantleTimer += dt;
|
||||
float speed = moveSpeed * wallClimbSpeedMultiplier;
|
||||
float speed = moveSpeed * wallMantleSpeedMultiplier;
|
||||
|
||||
Vector3 forward = -_wallNormal;
|
||||
forward.y = 0f;
|
||||
@@ -786,6 +855,10 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
_isWallClimbing = false;
|
||||
_lostContactTimer = 0f;
|
||||
_verticalVelocity = 0f;
|
||||
// Hand the current lean off to the residual so it eases home (EaseMeshOffsetToRest)
|
||||
// instead of snapping; latch the normal since the live one may shift after detaching.
|
||||
_meshOffsetResidual = _wallOffset;
|
||||
_meshOffsetExitNormal = _wallNormal;
|
||||
_wallOffset = 0f;
|
||||
_wallYaw = 0f;
|
||||
_wallStrafeSign = 0;
|
||||
|
||||
Reference in new issue
Block a user