fix(anim): physics velocity now sourced from MotionData — option B / r03 §1.3
The decompiled get_state_velocity (FUN_00528960) literally computes
`RunAnimSpeed * ForwardSpeed` — a 4.0 × runRate world velocity. That
matches retail only when the character's MotionTable happens to bake
MotionData.Velocity.Y = 4.0 on RunForward (true for Humanoid, not
necessarily for other creatures or swapped weapon-style cycles).
When MotionData.Velocity ≠ RunAnimSpeed, the body's world velocity
drifts away from the animation's baked-in root-motion velocity, and
you see the classic "legs cycle too slowly for how fast the body is
sliding" visual bug. User reports ~30% discrepancy ("running animation
is too slow"), consistent with Humanoid RunForward's actual dat
Velocity being ~3.0 rather than the 4.0 constant.
The fix per r03 §1.3: physics body velocity = MotionData.Velocity ×
speedMod. That's exactly what AnimationSequencer.CurrentVelocity
already exposes. Route it into MotionInterpreter via an opt-in
Func<Vector3> accessor. When wired, get_state_velocity uses the
sequencer's cycle velocity as the primary forward-axis drive; when
unwired (tests, physics bodies without a sequencer), falls back to
the decompiled constant path — byte-compatible with retail on the
shapes where it actually matters.
The RunAnimSpeed × rate max-speed clamp at the bottom of
FUN_00528960 stays intact — Option B only replaces the *drive*, not
the clamp. 20 m/s phantom MotionData can't teleport the player.
Wiring: GameWindow attaches `playerAE.Sequencer.CurrentVelocity` to
`_playerController` on Tab-player-mode entry. The sequencer is always
built before the player enters chase mode, so timing is safe.
Sidestep continues to use SidestepAnimSpeed — the sequencer only
tracks the current forward cycle, so strafe is a separate axis.
6 new MotionInterpreterTests verify: accessor overrides constant path,
zero Y falls back to constant (link transitions), clamp still applies,
Ready state doesn't leak accessor value, sidestep axis is untouched.
All 717 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5bd976e0c6
commit
795d9c8a88
4 changed files with 267 additions and 17 deletions
|
|
@ -164,6 +164,33 @@ public sealed class PlayerMovementController
|
|||
_weenie.SetSkills(runSkill, jumpSkill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wire the player's AnimationSequencer current cycle velocity into
|
||||
/// <see cref="MotionInterpreter.GetCycleVelocity"/>. When attached,
|
||||
/// <c>get_state_velocity</c> uses <c>MotionData.Velocity * speedMod</c>
|
||||
/// as the primary forward-axis drive, keeping the body's world velocity
|
||||
/// locked to the animation's baked-in root-motion velocity.
|
||||
///
|
||||
/// <para>
|
||||
/// Without this accessor, the decompiled constant path
|
||||
/// (<c>RunAnimSpeed * ForwardSpeed</c>) is used — matches retail only
|
||||
/// when the character's MotionTable happens to bake Velocity=4.0 on
|
||||
/// RunForward, which is true for Humanoid but not for arbitrary
|
||||
/// creatures. See <see cref="MotionInterpreter.GetCycleVelocity"/>
|
||||
/// for the full rationale.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Called once from <c>GameWindow.CreateAnimatedEntity</c> after the
|
||||
/// player's <c>AnimatedEntity.Sequencer</c> is constructed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void AttachCycleVelocityAccessor(Func<Vector3> accessor)
|
||||
{
|
||||
if (accessor is null) throw new ArgumentNullException(nameof(accessor));
|
||||
_motion.GetCycleVelocity = accessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a server-echoed run rate (ForwardSpeed from UpdateMotion) to the
|
||||
/// player's MotionInterpreter. The server broadcasts the real RunRate
|
||||
|
|
|
|||
|
|
@ -579,6 +579,21 @@ public sealed class GameWindow : IDisposable
|
|||
playerEntity.Position, pinitCellId & 0xFFFFu,
|
||||
System.Numerics.Vector3.Zero, 100f); // huge step height for initial snap
|
||||
_playerController.SetPosition(initResult.Position, initResult.CellId);
|
||||
|
||||
// Option B (r03 §1.3): wire the player's sequencer current
|
||||
// cycle velocity into MotionInterpreter.get_state_velocity so
|
||||
// body physics uses MotionData.Velocity * speedMod instead of
|
||||
// the hardcoded RunAnimSpeed/WalkAnimSpeed. Keeps legs-per-meter
|
||||
// invariant regardless of which MotionTable drives the player
|
||||
// (Humanoid RunForward happens to bake Velocity=4.0 so the old
|
||||
// path looked right there, but the decompiled constant is a
|
||||
// MotionTable property, not a global).
|
||||
if (_animatedEntities.TryGetValue(playerEntity.Id, out var playerAE)
|
||||
&& playerAE.Sequencer is { } playerSeq)
|
||||
{
|
||||
_playerController.AttachCycleVelocityAccessor(() => playerSeq.CurrentVelocity);
|
||||
}
|
||||
|
||||
// Derive initial yaw from the entity's rotation.
|
||||
// The render loop stores rotation as Yaw - PI/2 (to
|
||||
// compensate for AC models facing +Y at identity), so
|
||||
|
|
|
|||
|
|
@ -267,6 +267,45 @@ public sealed class MotionInterpreter
|
|||
/// <summary>True when crouching-in-place for a standing long jump (offset +0x70).</summary>
|
||||
public bool StandingLongJump;
|
||||
|
||||
/// <summary>
|
||||
/// Optional accessor for the owning entity's current animation cycle
|
||||
/// velocity (AnimationSequencer.CurrentVelocity, i.e. MotionData.Velocity
|
||||
/// scaled by speedMod). When wired, <see cref="get_state_velocity"/>
|
||||
/// uses it as the primary forward-axis drive instead of the hardcoded
|
||||
/// <see cref="RunAnimSpeed"/> / <see cref="WalkAnimSpeed"/> constants.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Why:</b> the decompiled <c>get_state_velocity</c> (FUN_00528960)
|
||||
/// literally computes <c>RunAnimSpeed * ForwardSpeed</c>. That works in
|
||||
/// retail because retail's Humanoid MotionTable happens to bake
|
||||
/// <c>MotionData.Velocity == RunAnimSpeed (4.0)</c> for the RunForward
|
||||
/// cycle — so the constant and the dat data agree. For MotionTables
|
||||
/// where they disagree (other creatures; swapped weapon-style cycles;
|
||||
/// modded dats), the constant causes the body's world velocity to
|
||||
/// drift away from the animation's baked-in root-motion velocity,
|
||||
/// producing the classic "legs cycle too slowly for how fast the body
|
||||
/// is sliding" visual bug.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Per <c>docs/research/deepdives/r03-motion-animation.md</c> §1.3,
|
||||
/// the retail animation pipeline treats <c>MotionData.Velocity *
|
||||
/// speedMod</c> as the canonical per-cycle world velocity. The
|
||||
/// <see cref="RunAnimSpeed"/> constant survives in our port only as
|
||||
/// the max-speed clamp (see below), which matches the decompile's
|
||||
/// <c>if (|velocity| > RunAnimSpeed * rate)</c> guard.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Call site: <c>PlayerMovementController.AttachCycleVelocityAccessor</c>
|
||||
/// wires this to <c>AnimatedEntity.Sequencer.CurrentVelocity</c> once
|
||||
/// the player's sequencer is built. Null = fall back to the decompiled
|
||||
/// constant-based path (used by tests and by any physics body with
|
||||
/// no sequencer).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public Func<Vector3>? GetCycleVelocity { get; set; }
|
||||
|
||||
// ── constructor ────────────────────────────────────────────────────────────
|
||||
|
||||
public MotionInterpreter()
|
||||
|
|
@ -492,21 +531,50 @@ public sealed class MotionInterpreter
|
|||
/// <summary>
|
||||
/// Compute the body-local velocity vector for the current interpreted motion.
|
||||
///
|
||||
/// Decompiled logic (FUN_00528960):
|
||||
/// velocity = (0, 0, 0)
|
||||
/// if InterpretedState.SideStepCommand == 0x6500000F:
|
||||
/// velocity.X = _DAT_007c96e8 * InterpretedState.SideStepSpeed
|
||||
/// = SidestepAnimSpeed * SideStepSpeed
|
||||
/// if InterpretedState.ForwardCommand == 0x45000005 (WalkForward):
|
||||
/// velocity.Y = _DAT_007c96e4 * InterpretedState.ForwardSpeed
|
||||
/// = WalkAnimSpeed * ForwardSpeed
|
||||
/// elif InterpretedState.ForwardCommand == 0x44000007 (RunForward):
|
||||
/// velocity.Y = _DAT_007c96e0 * InterpretedState.ForwardSpeed
|
||||
/// = RunAnimSpeed * ForwardSpeed
|
||||
/// rate = InqRunRate() or MyRunRate
|
||||
/// maxSpeed = RunAnimSpeed * rate
|
||||
/// if |velocity| > maxSpeed: velocity = normalize(velocity) * maxSpeed
|
||||
/// return velocity
|
||||
/// <para>
|
||||
/// <b>Decompiled path (FUN_00528960)</b>:
|
||||
/// <code>
|
||||
/// velocity = (0, 0, 0)
|
||||
/// if InterpretedState.SideStepCommand == 0x6500000F:
|
||||
/// velocity.X = _DAT_007c96e8 * InterpretedState.SideStepSpeed
|
||||
/// = SidestepAnimSpeed * SideStepSpeed
|
||||
/// if InterpretedState.ForwardCommand == 0x45000005 (WalkForward):
|
||||
/// velocity.Y = _DAT_007c96e4 * InterpretedState.ForwardSpeed
|
||||
/// = WalkAnimSpeed * ForwardSpeed
|
||||
/// elif InterpretedState.ForwardCommand == 0x44000007 (RunForward):
|
||||
/// velocity.Y = _DAT_007c96e0 * InterpretedState.ForwardSpeed
|
||||
/// = RunAnimSpeed * ForwardSpeed
|
||||
/// rate = InqRunRate() or MyRunRate
|
||||
/// maxSpeed = RunAnimSpeed * rate
|
||||
/// if |velocity| > maxSpeed: velocity = normalize(velocity) * maxSpeed
|
||||
/// return velocity
|
||||
/// </code>
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Option B — MotionData-sourced forward velocity</b>:
|
||||
/// when <see cref="GetCycleVelocity"/> is wired (i.e. the owning
|
||||
/// entity has an AnimationSequencer), we prefer
|
||||
/// <c>MotionData.Velocity.Y * speedMod</c> over the hardcoded
|
||||
/// <see cref="WalkAnimSpeed"/> / <see cref="RunAnimSpeed"/> constants.
|
||||
/// This keeps the body's world velocity locked to the animation's
|
||||
/// baked-in root-motion velocity (<c>r03 §1.3</c>), so the
|
||||
/// legs-per-meter ratio is invariant regardless of which motion table
|
||||
/// drives the character. The decompiled constant was a
|
||||
/// MotionTable-specific value that happens to equal the Humanoid
|
||||
/// RunForward MotionData.Velocity — fine as a fallback, but the dat
|
||||
/// is the ground truth for any non-humanoid creature.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The <see cref="RunAnimSpeed"/> constant survives as the max-speed
|
||||
/// clamp at the bottom, faithfully matching the decompile's
|
||||
/// <c>if (|velocity| > RunAnimSpeed * rate)</c> guard. Sidestep
|
||||
/// continues to use <see cref="SidestepAnimSpeed"/> because the
|
||||
/// sequencer only tracks the current forward cycle — strafe is
|
||||
/// implemented as a separate axis in our controller (see
|
||||
/// <c>PlayerMovementController.Update</c>).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public Vector3 get_state_velocity()
|
||||
{
|
||||
|
|
@ -515,10 +583,29 @@ public sealed class MotionInterpreter
|
|||
if (InterpretedState.SideStepCommand == MotionCommand.SideStepRight)
|
||||
velocity.X = SidestepAnimSpeed * InterpretedState.SideStepSpeed;
|
||||
|
||||
// Forward axis — prefer sequencer's current cycle velocity when available.
|
||||
// Sequencer's CurrentVelocity is already `MotionData.Velocity * speedMod`
|
||||
// where speedMod == ForwardSpeed for locomotion cycles, so we use it as-is
|
||||
// (no additional ForwardSpeed multiplication). Fall back to the decompiled
|
||||
// constant-based path when the accessor is unwired or returns zero Y
|
||||
// (e.g. during zero-velocity link transitions — in which case the constant
|
||||
// is the safe default to keep physics moving at ForwardSpeed).
|
||||
Vector3? cycleVel = GetCycleVelocity?.Invoke();
|
||||
bool haveCycleForward = cycleVel.HasValue
|
||||
&& MathF.Abs(cycleVel.Value.Y) > float.Epsilon;
|
||||
|
||||
if (InterpretedState.ForwardCommand == MotionCommand.WalkForward)
|
||||
velocity.Y = WalkAnimSpeed * InterpretedState.ForwardSpeed;
|
||||
{
|
||||
velocity.Y = haveCycleForward
|
||||
? cycleVel!.Value.Y
|
||||
: WalkAnimSpeed * InterpretedState.ForwardSpeed;
|
||||
}
|
||||
else if (InterpretedState.ForwardCommand == MotionCommand.RunForward)
|
||||
velocity.Y = RunAnimSpeed * InterpretedState.ForwardSpeed;
|
||||
{
|
||||
velocity.Y = haveCycleForward
|
||||
? cycleVel!.Value.Y
|
||||
: RunAnimSpeed * InterpretedState.ForwardSpeed;
|
||||
}
|
||||
|
||||
// Determine the current run rate via WeenieObj or fall back to MyRunRate.
|
||||
// Decompile: calls vtable+0x34 (InqRunRate).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue