fix(camera): #38 render-interpolate player motion
Keep local physics authoritative at the retail 30 Hz MinQuantum, but expose a render-only position that lerps between completed physics ticks for the player mesh and chase-camera target. Network outbound continues to use the discrete physics position. Also make the visually confirmed #47 humanoid close-detail DIDDegrade path default-on, with ACDREAM_RETAIL_CLOSE_DEGRADES=0 left as a diagnostic opt-out. Verification: dotnet build AcDream.slnx -c Debug; focused #38 interpolation tests passed; visual confirmed smooth 2026-05-06. Full dotnet test AcDream.slnx -c Debug --no-build still has the known 8 AcDream.Core.Tests baseline failures. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
e3d8a44c48
commit
71b1622293
5 changed files with 218 additions and 16 deletions
|
|
@ -41,6 +41,7 @@ public readonly record struct MovementInput(
|
|||
/// </summary>
|
||||
public readonly record struct MovementResult(
|
||||
Vector3 Position,
|
||||
Vector3 RenderPosition,
|
||||
uint CellId,
|
||||
bool IsOnGround,
|
||||
bool MotionStateChanged,
|
||||
|
|
@ -128,6 +129,7 @@ public sealed class PlayerMovementController
|
|||
|
||||
public float Yaw { get; set; }
|
||||
public Vector3 Position => _body.Position;
|
||||
public Vector3 RenderPosition => ComputeRenderPosition();
|
||||
public uint CellId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -213,6 +215,8 @@ public sealed class PlayerMovementController
|
|||
// ACE: PhysicsObj.UpdateObject (Physics.cs).
|
||||
// Named-retail: CPhysicsObj::update_object (acclient_2013_pseudo_c.txt:283950).
|
||||
private float _physicsAccum;
|
||||
private Vector3 _prevPhysicsPos;
|
||||
private Vector3 _currPhysicsPos;
|
||||
|
||||
public PlayerMovementController(PhysicsEngine physics)
|
||||
{
|
||||
|
|
@ -287,6 +291,8 @@ public sealed class PlayerMovementController
|
|||
public void SetPosition(Vector3 pos, uint cellId)
|
||||
{
|
||||
_body.Position = pos;
|
||||
_prevPhysicsPos = pos;
|
||||
_currPhysicsPos = pos;
|
||||
CellId = cellId;
|
||||
|
||||
// Treat as grounded after a server-side position snap.
|
||||
|
|
@ -295,6 +301,13 @@ public sealed class PlayerMovementController
|
|||
|
||||
// Reset physics clock so any subsequent update_object calls start fresh.
|
||||
_body.LastUpdateTime = 0.0;
|
||||
_physicsAccum = 0f;
|
||||
}
|
||||
|
||||
private Vector3 ComputeRenderPosition()
|
||||
{
|
||||
float alpha = Math.Clamp(_physicsAccum / PhysicsBody.MinQuantum, 0f, 1f);
|
||||
return Vector3.Lerp(_prevPhysicsPos, _currPhysicsPos, alpha);
|
||||
}
|
||||
|
||||
public MovementResult Update(float dt, MovementInput input)
|
||||
|
|
@ -306,6 +319,7 @@ public sealed class PlayerMovementController
|
|||
{
|
||||
return new MovementResult(
|
||||
Position: Position,
|
||||
RenderPosition: RenderPosition,
|
||||
CellId: CellId,
|
||||
IsOnGround: _body.OnWalkable,
|
||||
MotionStateChanged: false,
|
||||
|
|
@ -524,12 +538,16 @@ public sealed class PlayerMovementController
|
|||
// accumulated dt) when the threshold is reached. See _physicsAccum
|
||||
// declaration for the full retail trace evidence.
|
||||
var preIntegratePos = _body.Position;
|
||||
bool physicsTickRan = false;
|
||||
Vector3 oldTickEndPos = _currPhysicsPos;
|
||||
_physicsAccum += dt;
|
||||
|
||||
if (_physicsAccum > PhysicsBody.HugeQuantum)
|
||||
{
|
||||
// Stale frame (debugger break, GC pause). Discard accumulated dt.
|
||||
_physicsAccum = 0f;
|
||||
_prevPhysicsPos = _body.Position;
|
||||
_currPhysicsPos = _body.Position;
|
||||
}
|
||||
else if (_physicsAccum >= PhysicsBody.MinQuantum)
|
||||
{
|
||||
|
|
@ -539,6 +557,7 @@ public sealed class PlayerMovementController
|
|||
_body.calc_acceleration();
|
||||
_body.UpdatePhysicsInternal(tickDt);
|
||||
_physicsAccum -= tickDt;
|
||||
physicsTickRan = true;
|
||||
}
|
||||
// Else: dt below MinQuantum threshold — skip integration. Position
|
||||
// and velocity remain unchanged; Resolve below runs as a zero-distance
|
||||
|
|
@ -591,6 +610,11 @@ public sealed class PlayerMovementController
|
|||
|
||||
// Apply resolved position.
|
||||
_body.Position = resolveResult.Position;
|
||||
if (physicsTickRan)
|
||||
{
|
||||
_prevPhysicsPos = oldTickEndPos;
|
||||
_currPhysicsPos = _body.Position;
|
||||
}
|
||||
|
||||
// L.3a (2026-04-30): retail wall-bounce / velocity reflection.
|
||||
//
|
||||
|
|
@ -874,6 +898,7 @@ public sealed class PlayerMovementController
|
|||
|
||||
return new MovementResult(
|
||||
Position: Position,
|
||||
RenderPosition: RenderPosition,
|
||||
CellId: CellId,
|
||||
IsOnGround: _body.OnWalkable,
|
||||
MotionStateChanged: changed,
|
||||
|
|
|
|||
|
|
@ -173,15 +173,15 @@ public sealed class GameWindow : IDisposable
|
|||
private static readonly int s_hidePartIndex =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("ACDREAM_HIDE_PART"), out var hp) ? hp : -1;
|
||||
|
||||
// Issue #47 — opt in to retail's close-detail GfxObj selection on
|
||||
// Issue #47 — use retail's close-detail GfxObj selection on
|
||||
// humanoid setups. When enabled, every per-part GfxObj id (after
|
||||
// server AnimPartChanges are applied) is replaced with Degrades[0]
|
||||
// from its DIDDegrade table when present. See GfxObjDegradeResolver
|
||||
// for the full retail-decomp citation. Off by default while the fix
|
||||
// bakes; flip to default-on once we've confirmed no scenery/setup
|
||||
// regressions.
|
||||
// for the full retail-decomp citation. Default-on after visual
|
||||
// confirmation; set ACDREAM_RETAIL_CLOSE_DEGRADES=0 only for
|
||||
// diagnostic before/after comparisons.
|
||||
private static readonly bool s_retailCloseDegrades =
|
||||
string.Equals(Environment.GetEnvironmentVariable("ACDREAM_RETAIL_CLOSE_DEGRADES"), "1", StringComparison.Ordinal);
|
||||
!string.Equals(Environment.GetEnvironmentVariable("ACDREAM_RETAIL_CLOSE_DEGRADES"), "0", StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Issue #47 humanoid-setup detector. Matches Aluvian Male
|
||||
|
|
@ -5728,7 +5728,7 @@ public sealed class GameWindow : IDisposable
|
|||
// the physics-resolved location each frame.
|
||||
if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe))
|
||||
{
|
||||
pe.Position = result.Position;
|
||||
pe.Position = result.RenderPosition;
|
||||
pe.Rotation = System.Numerics.Quaternion.CreateFromAxisAngle(
|
||||
System.Numerics.Vector3.UnitZ, _playerController.Yaw - MathF.PI / 2f);
|
||||
|
||||
|
|
@ -5750,7 +5750,7 @@ public sealed class GameWindow : IDisposable
|
|||
// position never changes. With the pin: player visibly
|
||||
// rises above the camera, matching retail "you can see
|
||||
// yourself jump" feedback.
|
||||
_chaseCamera.Update(result.Position, _playerController.Yaw,
|
||||
_chaseCamera.Update(result.RenderPosition, _playerController.Yaw,
|
||||
isOnGround: result.IsOnGround,
|
||||
dt: (float)dt);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue