Integrate humanoid walk/run animations and per-clip pose speed sliders

- Import Mixamo humanoid Walking + Running FBX, switch rig type to Humanoid
- Wire walkClip/runClip with per-clip speeds on Pose_Idle (walk 1.4, run 1.5)
- Add per-animation speed sliders (Range 0.1-3, default 1) to PlayerPose SO,
  applied per frame in PlayerPoseController for live tuning
- Reorganize pose assets: replace Pose_TPose with Pose_Hold, prune legacy
  Paintman-Run/-TPose .anim and paintman_UV.FBX
- Ignore UMotion plugin data/editor folders

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-26 17:04:21 +02:00
1 parent dcfd668422
commit 5dd35dd0f9
24 files changed
+40464 -2273

No files matched your search

+10 -11
View File
@@ -49,8 +49,6 @@ public partial class PlayerCharacterController : MonoBehaviour
[Tooltip("Camera used for camera-relative movement. Empty = Camera.main.")]
public Transform cameraTransform;
public Animator animator;
[Tooltip("Animator bool parameter driven true while running on the ground (false in air and while wall/ceiling-climbing).")]
public string runningParameter = "Running";
@@ -160,6 +158,13 @@ public partial class PlayerCharacterController : MonoBehaviour
/// <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>
/// True while grounded and actually moving above the run threshold (the ground-locomotion state).
/// Airborne, wall/ceiling, and standing still all read false. This is the same signal that drives
/// the run bool, exposed so the pose system can pick its walk/run locomotion clips.
/// </summary>
public bool IsMovingOnGround => _movingOnGround;
/// <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
@@ -176,6 +181,7 @@ public partial class PlayerCharacterController : MonoBehaviour
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
// Ground contact normal from the latest Move (for slope sliding).
@@ -308,7 +314,7 @@ public partial class PlayerCharacterController : MonoBehaviour
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
_movingOnGround = false; // on a wall/ceiling, never ground locomotion
return;
}
@@ -377,7 +383,7 @@ public partial class PlayerCharacterController : MonoBehaviour
Vector3 horizontalVel = _controller.velocity;
horizontalVel.y = 0f;
bool movingOnGround = _controller.isGrounded && horizontalVel.sqrMagnitude > RunningSpeedThresholdSqr;
SetAnimatorRunning(movingOnGround);
_movingOnGround = movingOnGround;
_isIdle = ControlEnabled && _controller.isGrounded && !movingOnGround; // standing still on the floor
}
@@ -945,13 +951,6 @@ public partial class PlayerCharacterController : MonoBehaviour
#endif
}
/// <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 && !AnimatorOverridden)
animator.SetBool(_runningHash, running);
}
/// <summary>
/// Collects contact normals during Move: wall/ceiling contacts feed the
/// wall-climb detector, the flattest upward contact becomes the ground normal.
+40 -4
View File
@@ -1,9 +1,10 @@
using UnityEngine;
/// <summary>
/// A selectable player pose: an <see cref="AnimationClip"/> held while the player is idle, plus a
/// y-offset applied to the visual model only while stuck to the ceiling (so the pose hangs at the
/// right height). The first ScriptableObject in this project.
/// A selectable player pose: an idle <see cref="AnimationClip"/> held while the player stands still,
/// plus optional walk and run clips that the pose system plays itself (the Animator stays empty) while
/// moving on the ground, and a y-offset applied to the visual model only while stuck to the ceiling
/// (so the pose hangs at the right height). The first ScriptableObject in this project.
///
/// Named PlayerPose (not Pose) to avoid shadowing the built-in UnityEngine.Pose struct, mirroring
/// why the controller is PlayerCharacterController and not just "PlayerController".
@@ -16,13 +17,48 @@ public class PlayerPose : ScriptableObject
"other hierarchies will not retarget.")]
[SerializeField] private AnimationClip clip;
[Tooltip("Playback speed multiplier for the idle clip. 1 = authored speed.")]
[Range(0.1f, 3f)]
[SerializeField] private float clipSpeed = 1f;
[Tooltip("Optional. Played instead of the idle clip while walking on the ground (moving, not " +
"sprinting). Leave empty to keep holding the idle pose while walking. Should loop.")]
[SerializeField] private AnimationClip walkClip;
[Tooltip("Playback speed multiplier for the walk clip. 1 = authored speed.")]
[Range(0.1f, 3f)]
[SerializeField] private float walkClipSpeed = 1f;
[Tooltip("Optional. Played instead of the idle clip while sprinting on the ground. Leave empty to " +
"keep holding the idle pose while sprinting. Should loop.")]
[SerializeField] private AnimationClip runClip;
[Tooltip("Playback speed multiplier for the run clip. 1 = authored speed.")]
[Range(0.1f, 3f)]
[SerializeField] private float runClipSpeed = 1f;
[Tooltip("Local-up offset added to the visual model ONLY while stuck to the ceiling, so the pose " +
"hangs at the right height. Ignored on the ground and on walls. Meters.")]
[SerializeField] private float ceilingYOffset;
/// <summary>Idle clip for this pose (may be null).</summary>
/// <summary>Idle clip held while the player stands still in this pose (may be null).</summary>
public AnimationClip Clip => clip;
/// <summary>Playback speed multiplier for the idle clip (1 = authored speed).</summary>
public float ClipSpeed => clipSpeed;
/// <summary>Optional clip played while walking on the ground; null = hold the idle pose.</summary>
public AnimationClip WalkClip => walkClip;
/// <summary>Playback speed multiplier for the walk clip (1 = authored speed).</summary>
public float WalkClipSpeed => walkClipSpeed;
/// <summary>Optional clip played while sprinting on the ground; null = hold the idle pose.</summary>
public AnimationClip RunClip => runClip;
/// <summary>Playback speed multiplier for the run clip (1 = authored speed).</summary>
public float RunClipSpeed => runClipSpeed;
/// <summary>Local-up offset applied to the model only while on the ceiling.</summary>
public float CeilingYOffset => ceilingYOffset;
}
+43 -26
View File
@@ -8,18 +8,22 @@ using UnityEngine.InputSystem;
/// <summary>
/// Sits next to the <see cref="PlayerCharacterController"/> and holds a list of <see cref="PlayerPose"/>s
/// plus the active index. The active pose is held continuously — while idle, walking, and sprinting
/// alike — so cycling to the next pose, or the sprint reset to the default pose, shows up
/// immediately instead of only once the player comes to a stop. One key cycles to the next pose
/// (wrapping), and sprinting in any state (ground, air, wall, ceiling) snaps the selection back to the default pose.
/// plus the active index. The pose system owns all character animation — the Animator is left empty —
/// so it drives locomotion itself: standing still (or airborne / on a wall / on the ceiling) holds the
/// active pose's idle clip, walking on the ground plays that pose's optional walk clip, and sprinting
/// on the ground plays its optional run clip. Either locomotion clip is optional; when a pose leaves
/// one null, the idle pose is simply held in that state.
///
/// The pose clip is played through a tiny <see cref="PlayableGraph"/> (a two-input
/// <see cref="AnimationMixerPlayable"/> on an <see cref="AnimationPlayableOutput"/>) that overrides
/// the Animator Controller's run/idle animation for as long as a pose is selected (it only hands the
/// rig back when the list is empty / the clip is null). Switching pose crossfades the outgoing and
/// incoming clips over <see cref="poseBlendDuration"/> instead of snapping. The per-pose ceiling
/// y-offset is eased toward its target and routed through the controller (the single writer of the
/// model-offset transform) so it composes with the wall-lean instead of fighting it.
/// One key cycles to the next pose (wrapping), and sprinting in any state (ground, air, wall, ceiling)
/// snaps the selection back to the default pose. The selection is held continuously, so cycling or the
/// sprint reset shows up immediately instead of only once the player comes to a stop.
///
/// Clips are played through a tiny <see cref="PlayableGraph"/> (a two-input
/// <see cref="AnimationMixerPlayable"/> on an <see cref="AnimationPlayableOutput"/>) that drives the
/// Animator directly. Switching clip — between poses, or between idle/walk/run within a pose —
/// crossfades the outgoing and incoming clips over <see cref="poseBlendDuration"/> instead of snapping.
/// The per-pose ceiling y-offset is eased toward its target and routed through the controller (the
/// single writer of the model-offset transform) so it composes with the wall-lean instead of fighting it.
/// </summary>
[RequireComponent(typeof(PlayerCharacterController))]
[DisallowMultipleComponent]
@@ -93,7 +97,7 @@ public partial class PlayerPoseController : MonoBehaviour
void Awake()
{
_player = GetComponent<PlayerCharacterController>();
_animator = _player ? _player.animator : null;
_animator = _player ? _player.GetComponentInChildren<Animator>() : null;
_graphName = $"{name}-Pose";
if (_animator)
CaptureRestPose();
@@ -113,16 +117,23 @@ public partial class PlayerPoseController : MonoBehaviour
PlayerPose pose = ActivePose();
// Hold the pose continuously — idle, walking, and sprinting alike — so a selection change
// (cycling, or the sprint reset to the default pose above) shows immediately instead of only
// once the player stops. PlayPose hands the rig back to the controller only when there is no
// clip to play (empty list / null clip).
PlayPose(pose ? pose.Clip : null);
// Tell the controller the pose graph owns the Animator output so it stops poking the run-bool
// (moot while overridden, and it would warn every frame). Read next frame, when this matches
// the bound-graph state (both only change here).
_player.AnimatorOverridden = _posePlaying;
// Pick the clip for the current locomotion state. Standing still, airborne, on a wall or on the
// ceiling holds the idle pose; moving on the ground plays the run clip while sprinting or the
// walk clip otherwise. Both locomotion clips are optional — a null one falls back to the idle
// pose, so an unset walk clip keeps the held-pose behaviour. PlayPose crossfades on any change
// and hands the rig back only when there is no clip at all (empty list / null idle clip).
AnimationClip clip = pose ? pose.Clip : null;
float speed = pose ? pose.ClipSpeed : 1f;
if (pose && _player.IsMovingOnGround)
{
AnimationClip locomotion = _player.IsSprinting ? pose.RunClip : pose.WalkClip;
if (locomotion)
{
clip = locomotion;
speed = _player.IsSprinting ? pose.RunClipSpeed : pose.WalkClipSpeed;
}
}
PlayPose(clip, speed);
// Ceiling y-offset: only while stuck to the ceiling. Ease toward the target instead of
// snapping, so entering and leaving the ceiling (and switching ceiling poses) glides. Routed
@@ -143,7 +154,6 @@ public partial class PlayerPoseController : MonoBehaviour
if (_player)
{
_player.SetPoseLocalYOffset(0f);
_player.AnimatorOverridden = false;
}
}
@@ -183,10 +193,11 @@ public partial class PlayerPoseController : MonoBehaviour
/// Plays and holds the given clip, overriding the Animator Controller. Switching to a different
/// clip crossfades from the current pose over <see cref="poseBlendDuration"/> (the first pose, or a
/// zero duration, appears instantly). Called every frame: it starts a transition on a clip change,
/// advances any in-flight blend, and re-snaps un-keyed bones to rest before the graph evaluates. A
/// null clip (or no animator) releases back to the controller.
/// advances any in-flight blend, applies <paramref name="speed"/> to the target playback, and
/// re-snaps un-keyed bones to rest before the graph evaluates. A null clip (or no animator) releases
/// back to the controller.
/// </summary>
private void PlayPose(AnimationClip clip)
private void PlayPose(AnimationClip clip, float speed)
{
if (!clip || !_animator)
{
@@ -201,6 +212,12 @@ public partial class PlayerPoseController : MonoBehaviour
AdvanceBlend(Time.deltaTime);
// Set every frame (not just on clip change) so tuning a pose's speed slider live in the inspector
// takes effect immediately and re-selecting the same clip keeps its speed. The outgoing pose on
// input 0 keeps the speed it carried, so a crossfade fades it out at its own pace.
if (_toPlayable.IsValid())
PlayableExtensions.SetSpeed(_toPlayable, speed);
// Snap un-keyed bones to rest every frame, so each mixer input blends relative to rest: a
// partial or empty clip (e.g. the T-pose) defines the whole body, and a crossfade interpolates
// both poses against the same baseline. The graph overrides the keyed bones, weighted, when it