Add ceiling rotation drag and refine ground/wall pose offset controls
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
080b2a1c39
commit
f652cdeca0
6 files changed
+407
-203
No files matched your search
@@ -23,6 +23,11 @@ using UnityEngine.InputSystem;
|
||||
/// horizontal plane camera-relative (like on the ground). Jump (or reaching a ceiling edge)
|
||||
/// lets go and drops.
|
||||
///
|
||||
/// On the ground the same mouse scheme fine-tunes a pose without moving the capsule: holding the
|
||||
/// left mouse button and dragging up/down raises/lowers the visual mesh, holding the right mouse
|
||||
/// button and dragging left/right turns the body. The raise eases back while walking and holds while
|
||||
/// standing; the turn holds until movement re-aims the body. Both pause the camera orbit while held.
|
||||
///
|
||||
/// Named PlayerCharacterController to avoid a clash with Unity.VisualScripting's
|
||||
/// example "PlayerController".
|
||||
/// </summary>
|
||||
@@ -32,10 +37,10 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
{
|
||||
[Header("Movement")]
|
||||
[Tooltip("Horizontal movement speed (units/second).")]
|
||||
public float moveSpeed = 4f;
|
||||
public float moveSpeed = 2f;
|
||||
|
||||
[Tooltip("Speed multiplier applied to ground movement and wall/ceiling climbing while the sprint key (Left Shift) is held.")]
|
||||
public float sprintSpeedMultiplier = 1.8f;
|
||||
public float sprintSpeedMultiplier = 3.2f;
|
||||
|
||||
[Tooltip("How quickly ground movement ramps up to the target speed (units/second²). Lower = more gradual start.")]
|
||||
public float acceleration = 30f;
|
||||
@@ -49,22 +54,19 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
[Tooltip("Camera used for camera-relative movement. Empty = Camera.main.")]
|
||||
public Transform cameraTransform;
|
||||
|
||||
[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;
|
||||
|
||||
[Tooltip("Turn speed in degrees per second.")]
|
||||
public float rotationSpeed = 720f;
|
||||
public float rotationSpeed = 540f;
|
||||
|
||||
[Header("Jump & Gravity")]
|
||||
[Tooltip("Peak jump height in units.")]
|
||||
public float jumpHeight = 1.2f;
|
||||
|
||||
[Tooltip("Gravity in units per second squared (negative = downward).")]
|
||||
public float gravity = -19.62f;
|
||||
public float gravity = -14.715f;
|
||||
|
||||
[Tooltip("Small downward velocity kept while grounded for reliable ground contact.")]
|
||||
public float groundedStickVelocity = -2f;
|
||||
@@ -89,7 +91,7 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
|
||||
[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;
|
||||
public float wallMinAngle = 60f;
|
||||
|
||||
[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)]
|
||||
@@ -101,16 +103,16 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
|
||||
[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;
|
||||
public float wallMantleSpeedMultiplier = 1.5f;
|
||||
|
||||
[Tooltip("Farthest the mesh can lean out from the wall via the mouse-drag offset (units).")]
|
||||
public float wallMaxOffset = .2f;
|
||||
public float wallMaxOffset = 0.1f;
|
||||
|
||||
[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;
|
||||
public float wallMaxInset = 0.2f;
|
||||
|
||||
[Tooltip("Units the lean-out offset changes per pixel of vertical mouse drag (left button held).")]
|
||||
public float wallOffsetSensitivity = 0.01f;
|
||||
public float wallOffsetSensitivity = 0.001f;
|
||||
|
||||
[Tooltip("Degrees the body turns per pixel of horizontal mouse drag (left button held) while climbing.")]
|
||||
public float wallRotateSensitivity = 0.2f;
|
||||
@@ -128,7 +130,7 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
public float ceilingGrabDistance = 0.3f;
|
||||
|
||||
[Tooltip("Upward hop when topping out over a wall/slope edge, so holding W carries you onto the ledge instead of dropping back onto the face and re-grabbing. In meters of jump height; 0 = no hop.")]
|
||||
public float wallTopOutBoost = 1.2f;
|
||||
public float wallTopOutBoost = 0.3f;
|
||||
|
||||
[Tooltip("Camera orbit input; paused while dragging the wall-climb offset. Auto-found if empty.")]
|
||||
[SerializeField] private ThirdPersonOrbitInput cameraOrbit;
|
||||
@@ -172,17 +174,25 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
/// </summary>
|
||||
public bool AnimatorOverridden { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The visual-model transform moved by the wall-lean / ceiling offset; its localPosition is owned
|
||||
/// solely by <see cref="SetMeshLean"/>. Exposed so <see cref="PlayerPoseController"/> can exclude it
|
||||
/// from its per-frame rest reset — with the humanoid rig the Animator sits on this same node, so it
|
||||
/// lands in the reset subtree and would otherwise be snapped back to base every frame, wiping the offset.
|
||||
/// </summary>
|
||||
public Transform MeshOffsetTarget => meshOffsetTarget;
|
||||
|
||||
// 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)
|
||||
private bool _sprinting; // sprint key state this frame (exposed via IsSprinting)
|
||||
private bool _isIdle; // snapshot: player essentially still this frame (exposed via IsIdle)
|
||||
private bool _movingOnGround; // snapshot: grounded & moving above the run threshold (exposed via IsMovingOnGround)
|
||||
private float _poseYOffset; // local-up model offset from the active pose (ceiling only); folded into SetMeshLean
|
||||
private float _groundOffset; // signed vertical mesh raise from the ground mouse pose-adjust (+ up / - down); folded into SetMeshLean
|
||||
|
||||
// Ground contact normal from the latest Move (for slope sliding).
|
||||
private Vector3 _groundNormal = Vector3.up;
|
||||
@@ -219,7 +229,6 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
void Awake()
|
||||
{
|
||||
_controller = GetComponent<CharacterController>();
|
||||
_runningHash = Animator.StringToHash(runningParameter);
|
||||
if (!cameraTransform && Camera.main)
|
||||
cameraTransform = Camera.main.transform;
|
||||
if (!cameraOrbit)
|
||||
@@ -318,10 +327,20 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// Ground pose-adjust (mouse): left button + vertical drag raises/lowers the mesh, right
|
||||
// button + horizontal drag turns the body -- the wall scheme mirrored to the ground. Either
|
||||
// pauses the camera orbit so the same drag isn't also spent orbiting. Gated to player control
|
||||
// so it never fights painting (paint mode owns the left button).
|
||||
bool groundYawDrag = false;
|
||||
if (ControlEnabled)
|
||||
groundYawDrag = HandleGroundPoseAdjust(h, v, Time.deltaTime);
|
||||
else
|
||||
_offsetDragging = false;
|
||||
if (cameraOrbit)
|
||||
cameraOrbit.LookSuppressed = false;
|
||||
cameraOrbit.LookSuppressed = _offsetDragging;
|
||||
|
||||
// Glide the leftover wall lean back to the authored pose (set when leaving the wall).
|
||||
// Glide the leftover wall lean home and re-apply the composed mesh offset (residual lean +
|
||||
// ground raise + pose y-offset) in this frame's single write of meshOffsetTarget.
|
||||
EaseMeshOffsetToRest(Time.deltaTime);
|
||||
|
||||
// A ground normal steeper than the slope limit is too steep to stand on
|
||||
@@ -371,7 +390,8 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
if (!_groundNormalUpdated)
|
||||
_groundNormal = Vector3.up;
|
||||
|
||||
if (rotateTowardMovement && moveDir.sqrMagnitude > 0.0001f)
|
||||
// Face the movement direction, unless the right-button turn is actively steering the body.
|
||||
if (rotateTowardMovement && !groundYawDrag && moveDir.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
Quaternion target = Quaternion.LookRotation(moveDir, Vector3.up);
|
||||
transform.rotation = Quaternion.RotateTowards(
|
||||
@@ -431,6 +451,7 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
_verticalVelocity = 0f;
|
||||
_slideSpeed = 0f;
|
||||
_wallOffset = 0f;
|
||||
_groundOffset = 0f; // drop any ground pose-adjust raise so it doesn't ride onto the wall
|
||||
_meshOffsetResidual = 0f; // fresh grab: the climb path owns the mesh, drop any leftover ease
|
||||
_wallYaw = 0f;
|
||||
_wallStrafeSign = 0;
|
||||
@@ -446,8 +467,9 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
/// <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
|
||||
/// (like on the ground), facing the move direction unless the right mouse button + horizontal
|
||||
/// drag turns it; 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, bool sprinting, float dt)
|
||||
@@ -565,9 +587,10 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
|
||||
// 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.
|
||||
// the wall where the capsule can't); right button + horizontal drag turns the body. The
|
||||
// offset eases back to zero while moving and never detaches. On a WALL the turn is a held
|
||||
// yaw offset from the wall normal (kept until leaving); on a CEILING the body is free like
|
||||
// on the ground, so the drag rotates the transform directly. Either drag pauses the orbit.
|
||||
bool offsetDrag = found && IsLeftMouseHeld();
|
||||
bool yawDrag = found && IsRightMouseHeld();
|
||||
if (offsetDrag || yawDrag)
|
||||
@@ -577,7 +600,12 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
if (offsetDrag)
|
||||
_wallOffset = Mathf.Clamp(_wallOffset - drag.y * wallOffsetSensitivity, -wallMaxInset, wallMaxOffset);
|
||||
if (yawDrag)
|
||||
_wallYaw += drag.x * wallRotateSensitivity;
|
||||
{
|
||||
if (onCeiling)
|
||||
transform.Rotate(0f, drag.x * wallRotateSensitivity, 0f, Space.World);
|
||||
else
|
||||
_wallYaw += drag.x * wallRotateSensitivity;
|
||||
}
|
||||
}
|
||||
if (!offsetDrag && hasMoveInput)
|
||||
{
|
||||
@@ -632,14 +660,18 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
{
|
||||
if (onCeiling)
|
||||
{
|
||||
// Stay upright and face the horizontal move direction, like on the ground.
|
||||
Vector3 dir = alongSurface;
|
||||
dir.y = 0f;
|
||||
if (dir.sqrMagnitude > 1e-4f)
|
||||
// Stay upright and face the horizontal move direction, like on the ground -- unless
|
||||
// the right-button turn is actively steering the body (then the manual yaw holds).
|
||||
if (!yawDrag)
|
||||
{
|
||||
Quaternion target = Quaternion.LookRotation(dir.normalized, Vector3.up);
|
||||
transform.rotation = Quaternion.RotateTowards(
|
||||
transform.rotation, target, rotationSpeed * dt);
|
||||
Vector3 dir = alongSurface;
|
||||
dir.y = 0f;
|
||||
if (dir.sqrMagnitude > 1e-4f)
|
||||
{
|
||||
Quaternion target = Quaternion.LookRotation(dir.normalized, Vector3.up);
|
||||
transform.rotation = Quaternion.RotateTowards(
|
||||
transform.rotation, target, rotationSpeed * dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -651,9 +683,7 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
if (faceWall.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
Vector3 dir = Quaternion.AngleAxis(_wallYaw, Vector3.up) * faceWall.normalized;
|
||||
Quaternion target = Quaternion.LookRotation(dir, Vector3.up);
|
||||
transform.rotation = Quaternion.RotateTowards(
|
||||
transform.rotation, target, rotationSpeed * dt);
|
||||
transform.rotation = Quaternion.LookRotation(dir, Vector3.up);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,21 +708,58 @@ public partial class PlayerCharacterController : MonoBehaviour
|
||||
Vector3 worldOffset = dir * offset;
|
||||
meshOffsetTarget.localPosition = _meshBaseLocalPos
|
||||
+ transform.InverseTransformVector(worldOffset)
|
||||
+ Vector3.up * _poseYOffset; // active pose's ceiling y-offset (0 except while hanging on a ceiling)
|
||||
+ Vector3.up * (_poseYOffset + _groundOffset); // ceiling pose y-offset + ground mouse raise (both 0 unless in use)
|
||||
}
|
||||
|
||||
/// <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).
|
||||
/// Re-applies the resting mesh offset every non-climbing frame in a single write of
|
||||
/// <see cref="meshOffsetTarget"/>: eases the leftover wall lean back toward the authored pose
|
||||
/// (latched exit normal) while composing in the ground mouse raise and any pose y-offset. Writing
|
||||
/// unconditionally (even at rest) keeps this the sole owner of the offset transform, so a held
|
||||
/// ground raise stays applied after the residual has reached zero. Offset 0 restores the pose.
|
||||
/// </summary>
|
||||
private void EaseMeshOffsetToRest(float dt)
|
||||
{
|
||||
if (_meshOffsetResidual == 0f) return;
|
||||
_meshOffsetResidual = Mathf.MoveTowards(_meshOffsetResidual, 0f, wallOffsetReturnSpeed * dt);
|
||||
if (_meshOffsetResidual != 0f)
|
||||
_meshOffsetResidual = Mathf.MoveTowards(_meshOffsetResidual, 0f, wallOffsetReturnSpeed * dt);
|
||||
SetMeshLean(_meshOffsetExitNormal, _meshOffsetResidual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ground pose-adjust via the mouse, mirroring the wall-climb scheme: holding the LEFT button and
|
||||
/// dragging up/down raises/lowers the visual mesh vertically (<see cref="_groundOffset"/>, capsule
|
||||
/// untouched), holding the RIGHT button and dragging left/right turns the body (world-up yaw). The
|
||||
/// raise eases back to the authored height while moving and holds while standing or dragging; the
|
||||
/// turn persists until movement re-aims the body. Returns true while a right-button turn is active
|
||||
/// so the caller skips auto-rotate-toward-movement that frame. Call site gates on ControlEnabled so
|
||||
/// paint mode keeps the left button for painting.
|
||||
/// </summary>
|
||||
private bool HandleGroundPoseAdjust(float h, float v, float dt)
|
||||
{
|
||||
_offsetDragging = false;
|
||||
|
||||
bool offsetDrag = IsLeftMouseHeld();
|
||||
bool yawDrag = IsRightMouseHeld();
|
||||
bool hasMoveInput = (h * h + v * v) > 1e-4f;
|
||||
|
||||
if (offsetDrag || yawDrag)
|
||||
{
|
||||
_offsetDragging = true;
|
||||
Vector2 drag = ReadMouseDelta();
|
||||
if (offsetDrag)
|
||||
_groundOffset = Mathf.Clamp(
|
||||
_groundOffset + drag.y * wallOffsetSensitivity, -wallMaxInset, wallMaxOffset);
|
||||
if (yawDrag)
|
||||
transform.Rotate(0f, drag.x * wallRotateSensitivity, 0f, Space.World);
|
||||
}
|
||||
|
||||
// The raise eases home only while actually moving; standing still holds it for posing.
|
||||
if (!offsetDrag && hasMoveInput)
|
||||
_groundOffset = Mathf.MoveTowards(_groundOffset, 0f, wallOffsetReturnSpeed * dt);
|
||||
|
||||
return yawDrag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a local-up offset applied to the visual model only, composed into the wall-lean's single
|
||||
/// write of <see cref="meshOffsetTarget"/>. Used by <see cref="PlayerPoseController"/> to hang a
|
||||
|
||||
@@ -89,8 +89,9 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
// evaluates. An AnimationClipPlayable only writes the bones its curves cover, so a clip that
|
||||
// doesn't key a bone (an empty clip like the T-pose, or a partial one) would otherwise leave that
|
||||
// bone frozen on the previous animation. Restoring this before each pose start makes every pose
|
||||
// define the full body. meshOffsetTarget is an ancestor of the Animator, so it is never in this
|
||||
// set and the controller's lean/ceiling offset stays untouched.
|
||||
// define the full body. The controller's model-offset transform (meshOffsetTarget) is explicitly
|
||||
// dropped from this set in CaptureRestPose — with the humanoid rig the Animator sits on that node,
|
||||
// so it is the root of this subtree, and resetting it would wipe the wall-lean / ceiling offset.
|
||||
private Transform[] _restBones;
|
||||
private Vector3[] _restPos;
|
||||
private Quaternion[] _restRot;
|
||||
@@ -336,6 +337,20 @@ public partial class PlayerPoseController : MonoBehaviour
|
||||
_restRot[i] = t.localRotation;
|
||||
_restScale[i] = t.localScale;
|
||||
}
|
||||
|
||||
// Drop the model-offset transform from the reset set so the per-frame ResetToRestPose never
|
||||
// touches it: the wall-lean / ceiling offset (PlayerCharacterController.SetMeshLean) is its sole
|
||||
// writer. With the humanoid rig the Animator shares this node, putting it in the captured subtree;
|
||||
// resetting it each frame is exactly what was wiping the offsets. Nulling the slot makes
|
||||
// ResetToRestPose's null-guard skip it. _player is resolved (and meshOffsetTarget too, since the
|
||||
// controller's Awake runs first at its lower execution order) before this runs.
|
||||
Transform offsetTarget = _player ? _player.MeshOffsetTarget : null;
|
||||
if (offsetTarget)
|
||||
{
|
||||
int idx = System.Array.IndexOf(_restBones, offsetTarget);
|
||||
if (idx >= 0)
|
||||
_restBones[idx] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Restores the captured rest pose so an empty or partial pose clip fully defines the body.</summary>
|
||||
|
||||
Reference in new issue
Block a user