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:
1 parent
dcfd668422
commit
5dd35dd0f9
24 files changed
+40464
-2273
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user