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
|
|
@ -643,11 +643,13 @@ primary motion + collision resolution.
|
||||||
- Verification: side-by-side vs legacy default in 2-client setup,
|
- Verification: side-by-side vs legacy default in 2-client setup,
|
||||||
identical visible behavior.
|
identical visible behavior.
|
||||||
|
|
||||||
## #38 — Chase camera + player feel "30 fps" since L.5 physics-tick gate
|
## #38 — [DONE 2026-05-06 · (this commit)] Chase camera + player feel "30 fps" since L.5 physics-tick gate
|
||||||
|
|
||||||
**Status:** OPEN
|
**Status:** DONE
|
||||||
**Severity:** MEDIUM (gameplay-feel regression; not a correctness bug)
|
**Severity:** MEDIUM (gameplay-feel regression; not a correctness bug)
|
||||||
**Filed:** 2026-05-01
|
**Filed:** 2026-05-01
|
||||||
|
**Closed:** 2026-05-06
|
||||||
|
**Commit:** `(this commit)`
|
||||||
**Component:** rendering / physics / camera
|
**Component:** rendering / physics / camera
|
||||||
|
|
||||||
**Description:** User reports that running around in third-person /
|
**Description:** User reports that running around in third-person /
|
||||||
|
|
@ -704,7 +706,8 @@ collision fixes.)
|
||||||
**Acceptance:**
|
**Acceptance:**
|
||||||
|
|
||||||
- Chase-camera run-around at 60+ FPS feels as smooth as render rate
|
- Chase-camera run-around at 60+ FPS feels as smooth as render rate
|
||||||
suggests (no perceptual stepping)
|
suggests (no perceptual stepping) — user visually confirmed
|
||||||
|
2026-05-06.
|
||||||
- Network outbound (MoveToState / AutonomousPosition cadence + values)
|
- Network outbound (MoveToState / AutonomousPosition cadence + values)
|
||||||
unchanged from current behavior
|
unchanged from current behavior
|
||||||
- Collision behavior unchanged (the L.5 wedge / steep-roof scenarios
|
- Collision behavior unchanged (the L.5 wedge / steep-roof scenarios
|
||||||
|
|
@ -1360,10 +1363,11 @@ If hypothesis (a) is correct, this issue effectively rolls into **#28** — the
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## #47 — [DONE 2026-05-06] Humanoid Setup 0x02000001 renders bulky / lacks shape detail vs retail
|
## #47 — [DONE 2026-05-06 · 0bd9b96] Humanoid Setup 0x02000001 renders bulky / lacks shape detail vs retail
|
||||||
|
|
||||||
**Status:** DONE (commit pending)
|
**Status:** DONE
|
||||||
**Closed:** 2026-05-06
|
**Closed:** 2026-05-06
|
||||||
|
**Commit:** `0bd9b96`
|
||||||
**Severity:** MEDIUM (cosmetic — characters readable but visibly different from retail)
|
**Severity:** MEDIUM (cosmetic — characters readable but visibly different from retail)
|
||||||
**Filed:** 2026-05-06
|
**Filed:** 2026-05-06
|
||||||
**Component:** rendering / mesh / character animation
|
**Component:** rendering / mesh / character animation
|
||||||
|
|
@ -1382,10 +1386,10 @@ Concrete swaps the resolver now performs:
|
||||||
- Heritage variants: `0x010004BF → 0x010017A8`, `0x010004BD → 0x010017A7`,
|
- Heritage variants: `0x010004BF → 0x010017A8`, `0x010004BD → 0x010017A7`,
|
||||||
`0x010004B7 → 0x0100179A`, etc.
|
`0x010004B7 → 0x0100179A`, etc.
|
||||||
|
|
||||||
Fix landed as `GfxObjDegradeResolver`, gated behind
|
Fix landed as `GfxObjDegradeResolver`, default-on and scoped to humanoid
|
||||||
`ACDREAM_RETAIL_CLOSE_DEGRADES=1` and scoped to humanoid setups
|
setups (34-part with ≥8 null-sentinel attachment slots). Set
|
||||||
(34-part with ≥8 null-sentinel attachment slots). User confirmed
|
`ACDREAM_RETAIL_CLOSE_DEGRADES=0` only for diagnostic before/after
|
||||||
visually 2026-05-06.
|
comparisons. User confirmed visually 2026-05-06.
|
||||||
|
|
||||||
Files: `src/AcDream.Core/Meshing/GfxObjDegradeResolver.cs`,
|
Files: `src/AcDream.Core/Meshing/GfxObjDegradeResolver.cs`,
|
||||||
`src/AcDream.App/Rendering/GameWindow.cs` (wiring), 5 unit tests in
|
`src/AcDream.App/Rendering/GameWindow.cs` (wiring), 5 unit tests in
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
# Issue #38 render interpolation pseudocode
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Phase L.5 correctly restored retail's `CPhysicsObj::update_object`
|
||||||
|
physics gate: the body only integrates when accumulated frame time reaches
|
||||||
|
`PhysicsBody.MinQuantum` (1/30 second). That keeps collision behavior aligned
|
||||||
|
with retail, but the renderer currently reads `_body.Position` directly.
|
||||||
|
|
||||||
|
At 60+ FPS that means several rendered frames can show the same physics
|
||||||
|
position, then jump to the next 30 Hz position. Chase camera makes the stepping
|
||||||
|
obvious because both the player mesh and camera target follow that discrete
|
||||||
|
physics sample.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Named retail: `CPhysicsObj::update_object` at `0x00515d10`
|
||||||
|
(`docs/research/named-retail/acclient_2013_pseudo_c.txt:283950`) skips or
|
||||||
|
consumes time outside the valid quantum window and calls
|
||||||
|
`CPhysicsObj::UpdateObjectInternal` only for accepted physics quanta.
|
||||||
|
- ACE mirrors the same constants in `PhysicsGlobals`: `MinQuantum = 1/30`,
|
||||||
|
`MaxQuantum = 0.1`, `HugeQuantum = 2.0`.
|
||||||
|
- ACE's `InterpolationManager` / `PositionManager` prove client-style
|
||||||
|
smoothing is a known AC-family concept, but their queue chases network target
|
||||||
|
positions. Issue #38 is narrower: local render-only interpolation between the
|
||||||
|
last two authoritative local physics ticks.
|
||||||
|
- Glenn Fiedler's "Fix Your Timestep!" describes the canonical fixed-timestep
|
||||||
|
accumulator plus render interpolation pattern:
|
||||||
|
https://gafferongames.com/post/fix_your_timestep/
|
||||||
|
|
||||||
|
## Intentional divergence
|
||||||
|
|
||||||
|
Retail did not need a separate high-FPS render interpolation layer because the
|
||||||
|
2013 client effectively presented near the same cadence as its physics gate.
|
||||||
|
acdream renders at modern display rates, so we preserve retail's 30 Hz physics
|
||||||
|
truth but draw a blended visual position between the last two physics truths.
|
||||||
|
|
||||||
|
## Pseudocode
|
||||||
|
|
||||||
|
State stored by `PlayerMovementController`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
physicsAccum: leftover render time not yet consumed by physics
|
||||||
|
prevPhysicsPos: body position at the start of the most recently completed tick
|
||||||
|
currPhysicsPos: body position at the end of the most recently completed tick
|
||||||
|
```
|
||||||
|
|
||||||
|
On spawn, teleport, or any authoritative SetPosition:
|
||||||
|
|
||||||
|
```text
|
||||||
|
body.Position = newPosition
|
||||||
|
prevPhysicsPos = newPosition
|
||||||
|
currPhysicsPos = newPosition
|
||||||
|
physicsAccum = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Each Update(dt):
|
||||||
|
|
||||||
|
```text
|
||||||
|
physicsAccum += dt
|
||||||
|
|
||||||
|
if physicsAccum > HugeQuantum:
|
||||||
|
physicsAccum = 0
|
||||||
|
prevPhysicsPos = body.Position
|
||||||
|
currPhysicsPos = body.Position
|
||||||
|
return/render body.Position
|
||||||
|
|
||||||
|
if physicsAccum >= MinQuantum:
|
||||||
|
oldTickEnd = currPhysicsPos
|
||||||
|
preIntegratePos = body.Position
|
||||||
|
|
||||||
|
tickDt = min(physicsAccum, MaxQuantum)
|
||||||
|
calc_acceleration()
|
||||||
|
UpdatePhysicsInternal(tickDt)
|
||||||
|
|
||||||
|
postIntegratePos = body.Position
|
||||||
|
resolve collision from preIntegratePos to postIntegratePos
|
||||||
|
body.Position = resolved physics position
|
||||||
|
update contact/cell/velocity exactly as before
|
||||||
|
|
||||||
|
prevPhysicsPos = oldTickEnd
|
||||||
|
currPhysicsPos = body.Position
|
||||||
|
physicsAccum -= tickDt
|
||||||
|
|
||||||
|
alpha = clamp(physicsAccum / MinQuantum, 0, 1)
|
||||||
|
renderPosition = lerp(prevPhysicsPos, currPhysicsPos, alpha)
|
||||||
|
```
|
||||||
|
|
||||||
|
Important constraints:
|
||||||
|
|
||||||
|
- Never write `renderPosition` back to `_body.Position`.
|
||||||
|
- `MovementResult.Position` remains the authoritative physics/collision/network
|
||||||
|
position.
|
||||||
|
- Add `MovementResult.RenderPosition` for mesh and camera drawing only.
|
||||||
|
- Outbound `MoveToState` / `AutonomousPosition` keep using
|
||||||
|
`MovementResult.Position`.
|
||||||
|
- If no physics tick has completed yet, `prevPhysicsPos == currPhysicsPos`, so
|
||||||
|
interpolation is a no-op.
|
||||||
|
|
@ -41,6 +41,7 @@ public readonly record struct MovementInput(
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct MovementResult(
|
public readonly record struct MovementResult(
|
||||||
Vector3 Position,
|
Vector3 Position,
|
||||||
|
Vector3 RenderPosition,
|
||||||
uint CellId,
|
uint CellId,
|
||||||
bool IsOnGround,
|
bool IsOnGround,
|
||||||
bool MotionStateChanged,
|
bool MotionStateChanged,
|
||||||
|
|
@ -128,6 +129,7 @@ public sealed class PlayerMovementController
|
||||||
|
|
||||||
public float Yaw { get; set; }
|
public float Yaw { get; set; }
|
||||||
public Vector3 Position => _body.Position;
|
public Vector3 Position => _body.Position;
|
||||||
|
public Vector3 RenderPosition => ComputeRenderPosition();
|
||||||
public uint CellId { get; private set; }
|
public uint CellId { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -213,6 +215,8 @@ public sealed class PlayerMovementController
|
||||||
// ACE: PhysicsObj.UpdateObject (Physics.cs).
|
// ACE: PhysicsObj.UpdateObject (Physics.cs).
|
||||||
// Named-retail: CPhysicsObj::update_object (acclient_2013_pseudo_c.txt:283950).
|
// Named-retail: CPhysicsObj::update_object (acclient_2013_pseudo_c.txt:283950).
|
||||||
private float _physicsAccum;
|
private float _physicsAccum;
|
||||||
|
private Vector3 _prevPhysicsPos;
|
||||||
|
private Vector3 _currPhysicsPos;
|
||||||
|
|
||||||
public PlayerMovementController(PhysicsEngine physics)
|
public PlayerMovementController(PhysicsEngine physics)
|
||||||
{
|
{
|
||||||
|
|
@ -287,6 +291,8 @@ public sealed class PlayerMovementController
|
||||||
public void SetPosition(Vector3 pos, uint cellId)
|
public void SetPosition(Vector3 pos, uint cellId)
|
||||||
{
|
{
|
||||||
_body.Position = pos;
|
_body.Position = pos;
|
||||||
|
_prevPhysicsPos = pos;
|
||||||
|
_currPhysicsPos = pos;
|
||||||
CellId = cellId;
|
CellId = cellId;
|
||||||
|
|
||||||
// Treat as grounded after a server-side position snap.
|
// 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.
|
// Reset physics clock so any subsequent update_object calls start fresh.
|
||||||
_body.LastUpdateTime = 0.0;
|
_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)
|
public MovementResult Update(float dt, MovementInput input)
|
||||||
|
|
@ -306,6 +319,7 @@ public sealed class PlayerMovementController
|
||||||
{
|
{
|
||||||
return new MovementResult(
|
return new MovementResult(
|
||||||
Position: Position,
|
Position: Position,
|
||||||
|
RenderPosition: RenderPosition,
|
||||||
CellId: CellId,
|
CellId: CellId,
|
||||||
IsOnGround: _body.OnWalkable,
|
IsOnGround: _body.OnWalkable,
|
||||||
MotionStateChanged: false,
|
MotionStateChanged: false,
|
||||||
|
|
@ -524,12 +538,16 @@ public sealed class PlayerMovementController
|
||||||
// accumulated dt) when the threshold is reached. See _physicsAccum
|
// accumulated dt) when the threshold is reached. See _physicsAccum
|
||||||
// declaration for the full retail trace evidence.
|
// declaration for the full retail trace evidence.
|
||||||
var preIntegratePos = _body.Position;
|
var preIntegratePos = _body.Position;
|
||||||
|
bool physicsTickRan = false;
|
||||||
|
Vector3 oldTickEndPos = _currPhysicsPos;
|
||||||
_physicsAccum += dt;
|
_physicsAccum += dt;
|
||||||
|
|
||||||
if (_physicsAccum > PhysicsBody.HugeQuantum)
|
if (_physicsAccum > PhysicsBody.HugeQuantum)
|
||||||
{
|
{
|
||||||
// Stale frame (debugger break, GC pause). Discard accumulated dt.
|
// Stale frame (debugger break, GC pause). Discard accumulated dt.
|
||||||
_physicsAccum = 0f;
|
_physicsAccum = 0f;
|
||||||
|
_prevPhysicsPos = _body.Position;
|
||||||
|
_currPhysicsPos = _body.Position;
|
||||||
}
|
}
|
||||||
else if (_physicsAccum >= PhysicsBody.MinQuantum)
|
else if (_physicsAccum >= PhysicsBody.MinQuantum)
|
||||||
{
|
{
|
||||||
|
|
@ -539,6 +557,7 @@ public sealed class PlayerMovementController
|
||||||
_body.calc_acceleration();
|
_body.calc_acceleration();
|
||||||
_body.UpdatePhysicsInternal(tickDt);
|
_body.UpdatePhysicsInternal(tickDt);
|
||||||
_physicsAccum -= tickDt;
|
_physicsAccum -= tickDt;
|
||||||
|
physicsTickRan = true;
|
||||||
}
|
}
|
||||||
// Else: dt below MinQuantum threshold — skip integration. Position
|
// Else: dt below MinQuantum threshold — skip integration. Position
|
||||||
// and velocity remain unchanged; Resolve below runs as a zero-distance
|
// and velocity remain unchanged; Resolve below runs as a zero-distance
|
||||||
|
|
@ -591,6 +610,11 @@ public sealed class PlayerMovementController
|
||||||
|
|
||||||
// Apply resolved position.
|
// Apply resolved position.
|
||||||
_body.Position = resolveResult.Position;
|
_body.Position = resolveResult.Position;
|
||||||
|
if (physicsTickRan)
|
||||||
|
{
|
||||||
|
_prevPhysicsPos = oldTickEndPos;
|
||||||
|
_currPhysicsPos = _body.Position;
|
||||||
|
}
|
||||||
|
|
||||||
// L.3a (2026-04-30): retail wall-bounce / velocity reflection.
|
// L.3a (2026-04-30): retail wall-bounce / velocity reflection.
|
||||||
//
|
//
|
||||||
|
|
@ -874,6 +898,7 @@ public sealed class PlayerMovementController
|
||||||
|
|
||||||
return new MovementResult(
|
return new MovementResult(
|
||||||
Position: Position,
|
Position: Position,
|
||||||
|
RenderPosition: RenderPosition,
|
||||||
CellId: CellId,
|
CellId: CellId,
|
||||||
IsOnGround: _body.OnWalkable,
|
IsOnGround: _body.OnWalkable,
|
||||||
MotionStateChanged: changed,
|
MotionStateChanged: changed,
|
||||||
|
|
|
||||||
|
|
@ -173,15 +173,15 @@ public sealed class GameWindow : IDisposable
|
||||||
private static readonly int s_hidePartIndex =
|
private static readonly int s_hidePartIndex =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("ACDREAM_HIDE_PART"), out var hp) ? hp : -1;
|
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
|
// humanoid setups. When enabled, every per-part GfxObj id (after
|
||||||
// server AnimPartChanges are applied) is replaced with Degrades[0]
|
// server AnimPartChanges are applied) is replaced with Degrades[0]
|
||||||
// from its DIDDegrade table when present. See GfxObjDegradeResolver
|
// from its DIDDegrade table when present. See GfxObjDegradeResolver
|
||||||
// for the full retail-decomp citation. Off by default while the fix
|
// for the full retail-decomp citation. Default-on after visual
|
||||||
// bakes; flip to default-on once we've confirmed no scenery/setup
|
// confirmation; set ACDREAM_RETAIL_CLOSE_DEGRADES=0 only for
|
||||||
// regressions.
|
// diagnostic before/after comparisons.
|
||||||
private static readonly bool s_retailCloseDegrades =
|
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>
|
/// <summary>
|
||||||
/// Issue #47 humanoid-setup detector. Matches Aluvian Male
|
/// Issue #47 humanoid-setup detector. Matches Aluvian Male
|
||||||
|
|
@ -5728,7 +5728,7 @@ public sealed class GameWindow : IDisposable
|
||||||
// the physics-resolved location each frame.
|
// the physics-resolved location each frame.
|
||||||
if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe))
|
if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe))
|
||||||
{
|
{
|
||||||
pe.Position = result.Position;
|
pe.Position = result.RenderPosition;
|
||||||
pe.Rotation = System.Numerics.Quaternion.CreateFromAxisAngle(
|
pe.Rotation = System.Numerics.Quaternion.CreateFromAxisAngle(
|
||||||
System.Numerics.Vector3.UnitZ, _playerController.Yaw - MathF.PI / 2f);
|
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
|
// position never changes. With the pin: player visibly
|
||||||
// rises above the camera, matching retail "you can see
|
// rises above the camera, matching retail "you can see
|
||||||
// yourself jump" feedback.
|
// yourself jump" feedback.
|
||||||
_chaseCamera.Update(result.Position, _playerController.Yaw,
|
_chaseCamera.Update(result.RenderPosition, _playerController.Yaw,
|
||||||
isOnGround: result.IsOnGround,
|
isOnGround: result.IsOnGround,
|
||||||
dt: (float)dt);
|
dt: (float)dt);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,81 @@ public class PlayerMovementControllerTests
|
||||||
Assert.True(result.Position.X > 96f + 2f, $"X={result.Position.X} should have moved forward");
|
Assert.True(result.Position.X > 96f + 2f, $"X={result.Position.X} should have moved forward");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Update_SubQuantumFrame_InterpolatesRenderPositionWithoutAdvancingPhysicsPosition()
|
||||||
|
{
|
||||||
|
var engine = MakeFlatEngine();
|
||||||
|
var controller = new PlayerMovementController(engine);
|
||||||
|
var start = new Vector3(96f, 96f, 50f);
|
||||||
|
controller.SetPosition(start, 0x0001);
|
||||||
|
controller.Yaw = 0f;
|
||||||
|
|
||||||
|
var firstTick = controller.Update(PhysicsBody.MinQuantum, new MovementInput(Forward: true));
|
||||||
|
Assert.True(firstTick.Position.X > start.X, "Physics tick should advance the authoritative body position");
|
||||||
|
Assert.Equal(start.X, firstTick.RenderPosition.X, precision: 4);
|
||||||
|
|
||||||
|
var halfFrame = controller.Update(PhysicsBody.MinQuantum * 0.5f, new MovementInput(Forward: true));
|
||||||
|
|
||||||
|
Assert.Equal(firstTick.Position.X, halfFrame.Position.X, precision: 4);
|
||||||
|
Assert.True(halfFrame.RenderPosition.X > start.X, "Render position should move between physics ticks");
|
||||||
|
Assert.True(halfFrame.RenderPosition.X < firstTick.Position.X,
|
||||||
|
$"Render X={halfFrame.RenderPosition.X} should stay between {start.X} and {firstTick.Position.X}");
|
||||||
|
|
||||||
|
float expectedMidpoint = start.X + ((firstTick.Position.X - start.X) * 0.5f);
|
||||||
|
Assert.Equal(expectedMidpoint, halfFrame.RenderPosition.X, precision: 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetPosition_ResnapsRenderInterpolationEndpoints()
|
||||||
|
{
|
||||||
|
var engine = MakeFlatEngine();
|
||||||
|
var controller = new PlayerMovementController(engine);
|
||||||
|
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||||
|
controller.Yaw = 0f;
|
||||||
|
|
||||||
|
controller.Update(PhysicsBody.MinQuantum, new MovementInput(Forward: true));
|
||||||
|
controller.Update(PhysicsBody.MinQuantum * 0.5f, new MovementInput(Forward: true));
|
||||||
|
|
||||||
|
var snapped = new Vector3(120f, 80f, 50f);
|
||||||
|
controller.SetPosition(snapped, 0x0001);
|
||||||
|
var result = controller.Update(PhysicsBody.MinQuantum * 0.5f, new MovementInput());
|
||||||
|
|
||||||
|
Assert.Equal(snapped, result.Position);
|
||||||
|
Assert.Equal(snapped, result.RenderPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Update_HugeQuantumDiscard_ResnapsRenderInterpolationEndpoints()
|
||||||
|
{
|
||||||
|
var engine = MakeFlatEngine();
|
||||||
|
var controller = new PlayerMovementController(engine);
|
||||||
|
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||||
|
controller.Yaw = 0f;
|
||||||
|
|
||||||
|
var moved = controller.Update(PhysicsBody.MinQuantum, new MovementInput(Forward: true));
|
||||||
|
var stale = controller.Update(PhysicsBody.HugeQuantum + 0.1f, new MovementInput(Forward: true));
|
||||||
|
|
||||||
|
Assert.Equal(moved.Position.X, stale.Position.X, precision: 4);
|
||||||
|
Assert.Equal(stale.Position, stale.RenderPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Update_LeftoverAboveMinQuantum_ClampsRenderAlphaToCurrentPhysicsPosition()
|
||||||
|
{
|
||||||
|
var engine = MakeFlatEngine();
|
||||||
|
var controller = new PlayerMovementController(engine);
|
||||||
|
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||||
|
controller.Yaw = 0f;
|
||||||
|
|
||||||
|
var result = controller.Update(
|
||||||
|
PhysicsBody.MaxQuantum + PhysicsBody.MinQuantum,
|
||||||
|
new MovementInput(Forward: true));
|
||||||
|
|
||||||
|
Assert.Equal(result.Position.X, result.RenderPosition.X, precision: 4);
|
||||||
|
Assert.Equal(result.Position.Y, result.RenderPosition.Y, precision: 4);
|
||||||
|
Assert.Equal(result.Position.Z, result.RenderPosition.Z, precision: 4);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Update_RunForward_MoveFasterThanWalk()
|
public void Update_RunForward_MoveFasterThanWalk()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue