Add pose-switching system with continuous hold and sprint reset

New PlayerPose ScriptableObject (AnimationClip + ceiling Y-offset) and
PlayerPoseController that drives poses via a PlayableGraph, overriding the
Animator controller. Poses are held continuously (idle, moving, on wall/
ceiling) so cycle and sprint changes show immediately. Sprinting in any
state resets to pose 0. Non-keyed bones fall back to a captured rest pose
so empty/partial clips apply fully. Ceiling Y-offset is folded into the
controller's single mesh-offset writer.

PlayerCharacterController gains IsIdle / IsSprinting / IsOnCeiling /
AnimatorOverridden accessors and SetPoseLocalYOffset; SetAnimatorRunning is
guarded while a pose owns the Animator. Removed obsolete Paintman.controller.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-26 08:21:28 +02:00
1 parent d867c617df
commit c41fb398e4
18 files changed
+649 -465

No files matched your search

+52 -4
View File
@@ -147,6 +147,26 @@ public partial class PlayerCharacterController : MonoBehaviour
/// <summary>True while in wall-climb mode.</summary>
public bool IsWallClimbing => _isWallClimbing;
/// <summary>
/// True while the player is idle (under control and essentially still): standing on the floor
/// below the run threshold, or attached to a wall/ceiling with no climb input. Snapshotted at the
/// end of Update so a sibling reads a stable, order-independent value.
/// </summary>
public bool IsIdle => _isIdle;
/// <summary>True while the sprint key is held, in any state (ground, air, wall, or ceiling).</summary>
public bool IsSprinting => _sprinting;
/// <summary>True while stuck to a ceiling (wall-climb mode with a downward-facing surface normal).</summary>
public bool IsOnCeiling => _isWallClimbing && _wallNormal.y < CeilingNormalThreshold;
/// <summary>
/// Set by <see cref="PlayerPoseController"/> while a pose Playable graph owns the Animator output.
/// While set, the run-bool is not poked: it would be moot (the graph overrides it) and Unity would
/// log "Animator is not playing an AnimatorController" every frame.
/// </summary>
public bool AnimatorOverridden { get; set; }
// 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
@@ -154,6 +174,9 @@ public partial class PlayerCharacterController : MonoBehaviour
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 float _poseYOffset; // local-up model offset from the active pose (ceiling only); folded into SetMeshLean
// Ground contact normal from the latest Move (for slope sliding).
private Vector3 _groundNormal = Vector3.up;
@@ -241,6 +264,8 @@ public partial class PlayerCharacterController : MonoBehaviour
#endif
}
_sprinting = sprinting;
Vector3 inputDir = new Vector3(h, 0f, v);
if (inputDir.sqrMagnitude > 1f)
inputDir.Normalize();
@@ -282,6 +307,7 @@ public partial class PlayerCharacterController : MonoBehaviour
if (cameraOrbit)
cameraOrbit.LookSuppressed = _offsetDragging;
_horizontalVelocity = Vector3.zero; // drop ground momentum so resuming starts from rest
_isIdle = ControlEnabled && (h * h + v * v) <= 1e-4f; // attached & no climb input = holding a pose
SetAnimatorRunning(false); // climbing is its own pose, not the ground run
return;
}
@@ -350,7 +376,9 @@ public partial class PlayerCharacterController : MonoBehaviour
// Real horizontal speed, so pushing into a wall (blocked) reads as idle, not foot-sliding.
Vector3 horizontalVel = _controller.velocity;
horizontalVel.y = 0f;
SetAnimatorRunning(_controller.isGrounded && horizontalVel.sqrMagnitude > RunningSpeedThresholdSqr);
bool movingOnGround = _controller.isGrounded && horizontalVel.sqrMagnitude > RunningSpeedThresholdSqr;
SetAnimatorRunning(movingOnGround);
_isIdle = ControlEnabled && _controller.isGrounded && !movingOnGround; // standing still on the floor
}
/// <summary>
@@ -642,7 +670,9 @@ public partial class PlayerCharacterController : MonoBehaviour
{
if (!meshOffsetTarget) return;
Vector3 worldOffset = dir * offset;
meshOffsetTarget.localPosition = _meshBaseLocalPos + transform.InverseTransformVector(worldOffset);
meshOffsetTarget.localPosition = _meshBaseLocalPos
+ transform.InverseTransformVector(worldOffset)
+ Vector3.up * _poseYOffset; // active pose's ceiling y-offset (0 except while hanging on a ceiling)
}
/// <summary>
@@ -657,6 +687,24 @@ public partial class PlayerCharacterController : MonoBehaviour
SetMeshLean(_meshOffsetExitNormal, _meshOffsetResidual);
}
/// <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
/// ceiling pose at the right height; 0 = none. Re-applies immediately so a change shows this frame.
/// </summary>
public void SetPoseLocalYOffset(float y)
{
if (_poseYOffset == y) return;
_poseYOffset = y;
if (!meshOffsetTarget) return;
// While climbing the lean is the live _wallOffset along _wallNormal; otherwise re-apply along
// whatever residual lean is easing home (both 0 when there's none, restoring the authored pose).
if (_isWallClimbing)
SetMeshLean(_wallNormal, _wallOffset);
else
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
@@ -897,10 +945,10 @@ public partial class PlayerCharacterController : MonoBehaviour
#endif
}
/// <summary>Drives the animator's ground-run bool (null-safe if no animator is assigned).</summary>
/// <summary>Drives the animator's ground-run bool (null-safe; skipped while a pose graph overrides the Animator).</summary>
private void SetAnimatorRunning(bool running)
{
if (animator)
if (animator && !AnimatorOverridden)
animator.SetBool(_runningHash, running);
}