fix(physics): full retail per-frame chain for remote motion + persist ContactPlane across frames
Two linked issues both rooted in skipping parts of the retail physics chain. ## 1. Remote staircase on slopes — Euler never integrated between UPs TickAnimations called rm.Body.update_object(now) for remote integration, but PhysicsBody.update_object gates on MinQuantum = 1/30s (retail FUN_00515020 early-return). At our 60fps render tick (~16 ms), deltaTime < MinQuantum on almost every frame → early return AND LastUpdateTime never advances → position effectively never integrates. Remote Position changed only on UP hard-snap, producing visible teleport strides uphill (the "staircase" the user reported). Fix: call UpdatePhysicsInternal(dt) directly for the remote tick — the same pattern PlayerMovementController.cs:358 uses for the local player. Wire ResolveWithTransition in afterwards so the remote's Euler-advanced position gets swept through the same retail collision chain (find_env_collisions + find_obj_collisions + step_down + 6-path BSP dispatcher) that the local player already goes through. New field RemoteMotion.CellId tracks the remote's cell across frames; set from UpdatePosition.p.LandblockId and updated from transition output. ## 2. Local player floating on downhill slopes — ContactPlane not persisted Running a character down a slope faster than ~0.5 m/s vertical: per-frame Euler moves feet horizontally (no Z component since velocity is world-XY). After Euler, feet are above the new-XY terrain. ValidateWalkable takes the "above surface" branch without setting a contact plane, DoStepDown probes ~4 cm down (the retail StepDownHeight default), fails to find the surface 8-10 cm below, and the character stays at the old Z. Over a sustained descent this accumulates into a visible hover. Retail's PhysicsObj carries ContactPlane + ContactPlaneCellID as persistent fields (ACE PhysicsObj.cs:2598-2604 get_object_info → InitContactPlane). Each transition call seeds CollisionInfo.ContactPlane from the previous frame's plane. That seed is what lets AdjustOffset project horizontal velocity onto the slope surface — so the Euler offset acquires a Z component matching the slope and the sphere tracks terrain without needing step-down to do the catch-up every frame. Fix: add PhysicsBody.ContactPlane* fields mirroring PhysicsObj's. Extend ResolveWithTransition with an optional `body` parameter; when provided, seed the transition's CollisionInfo from body.ContactPlane at the start, copy back (preferring current, falling back to LastKnown) at the end. Both local (PlayerMovementController) and remote (TickAnimations) pass their body. Verified live: DIAG samples showed pre/post/resolved Z all exactly equal before the MinQuantum bypass (Euler frozen). After bypass, deltas dropped to floating-point noise on slopes for remotes. Local hover on downhill resolved in separate visual pass. All 717 tests green. No API breaks (ResolveWithTransition's body param is optional, backwards-compatible). Cross-refs: - decompile: FUN_00515020 update_object, FUN_005111D0 UpdatePhysicsInternal, FUN_005148A0 transition init - ACE: PhysicsObj.cs:2586-2621 get_object_info, Transition.cs:613-620 InitContactPlane Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
56975f8919
commit
93cbabbc87
4 changed files with 166 additions and 8 deletions
|
|
@ -310,12 +310,33 @@ public sealed class PhysicsEngine
|
|||
/// Subdivides movement into sphere-radius steps, tests terrain collision
|
||||
/// at each step, handles step-down for ground contact.
|
||||
/// Falls back to the simple <see cref="Resolve"/> if the transition fails.
|
||||
///
|
||||
/// <para>
|
||||
/// <paramref name="body"/> is optional but highly recommended for movement
|
||||
/// that runs across multiple frames. When provided, the previous frame's
|
||||
/// contact plane is copied INTO the transition's CollisionInfo (mirroring
|
||||
/// retail's <c>PhysicsObj.get_object_info → InitContactPlane</c> at
|
||||
/// <c>PhysicsObj.cs:2598-2604</c>). That seed is critical for slope
|
||||
/// tracking: <c>AdjustOffset</c> projects the Euler offset onto the plane
|
||||
/// so horizontal velocity acquires the correct Z component for the slope,
|
||||
/// preventing the character from floating on downhill runs where the
|
||||
/// per-frame descent exceeds the 4 cm step-down budget.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// On return, the plane discovered during this call is written BACK to
|
||||
/// <paramref name="body"/>, so the next frame's transition starts with
|
||||
/// an up-to-date plane seed. Callers without a persistent body (tests,
|
||||
/// one-shot movements) can pass <c>null</c> and accept the first-frame
|
||||
/// hiccup.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public ResolveResult ResolveWithTransition(
|
||||
Vector3 currentPos, Vector3 targetPos, uint cellId,
|
||||
float sphereRadius, float sphereHeight,
|
||||
float stepUpHeight, float stepDownHeight,
|
||||
bool isOnGround)
|
||||
bool isOnGround,
|
||||
PhysicsBody? body = null)
|
||||
{
|
||||
var transition = new Transition();
|
||||
transition.ObjectInfo.StepUpHeight = stepUpHeight;
|
||||
|
|
@ -325,6 +346,19 @@ public sealed class PhysicsEngine
|
|||
if (isOnGround)
|
||||
transition.ObjectInfo.State |= ObjectInfoState.Contact | ObjectInfoState.OnWalkable;
|
||||
|
||||
// Seed the transition's CollisionInfo with the previous frame's
|
||||
// contact plane (retail PhysicsObj field). Without this, every
|
||||
// ResolveWithTransition call starts with a fresh plane, AdjustOffset's
|
||||
// "Have a contact plane" branch never fires, and slope projection
|
||||
// never happens.
|
||||
if (body is not null && body.ContactPlaneValid)
|
||||
{
|
||||
transition.CollisionInfo.SetContactPlane(
|
||||
body.ContactPlane,
|
||||
body.ContactPlaneCellId,
|
||||
body.ContactPlaneIsWater);
|
||||
}
|
||||
|
||||
transition.SpherePath.InitPath(currentPos, targetPos, cellId, sphereRadius, sphereHeight);
|
||||
|
||||
bool ok = transition.FindTransitionalPosition(this);
|
||||
|
|
@ -332,6 +366,31 @@ public sealed class PhysicsEngine
|
|||
var sp = transition.SpherePath;
|
||||
var ci = transition.CollisionInfo;
|
||||
|
||||
// Persist the resulting contact plane state back to the body so the
|
||||
// next frame's transition can seed from it. Uses LastKnownContactPlane
|
||||
// when current is invalid (e.g., airborne this frame), matching retail.
|
||||
if (body is not null)
|
||||
{
|
||||
if (ci.ContactPlaneValid)
|
||||
{
|
||||
body.ContactPlaneValid = true;
|
||||
body.ContactPlane = ci.ContactPlane;
|
||||
body.ContactPlaneCellId = ci.ContactPlaneCellId;
|
||||
body.ContactPlaneIsWater = ci.ContactPlaneIsWater;
|
||||
}
|
||||
else if (ci.LastKnownContactPlaneValid)
|
||||
{
|
||||
body.ContactPlaneValid = true;
|
||||
body.ContactPlane = ci.LastKnownContactPlane;
|
||||
body.ContactPlaneCellId = ci.LastKnownContactPlaneCellId;
|
||||
body.ContactPlaneIsWater = ci.LastKnownContactPlaneIsWater;
|
||||
}
|
||||
else
|
||||
{
|
||||
body.ContactPlaneValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok)
|
||||
{
|
||||
bool onGround = ci.ContactPlaneValid
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue