feat(physics): wire CTransition sphere-sweep into player movement

Replace simple Z-snap PhysicsEngine.Resolve with ResolveWithTransition
that uses the ported CTransition sphere-sweep pipeline. Movement is
subdivided into sphere-radius steps, terrain collision tested at each
step with step-down for ground contact maintenance.

Falls back to simple Resolve if transition fails. Player controller
now passes pre/post integration positions to the transition system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-14 10:58:55 +02:00
parent 6523c7199b
commit 246713e2cc
2 changed files with 66 additions and 22 deletions

View file

@ -287,35 +287,35 @@ public sealed class PlayerMovementController
}
// ── 4. Integrate physics (gravity, friction, sub-stepping) ────────────
// Drive the integration directly rather than via update_object's wall-clock
// path — update_object silently skips frames shorter than MinQuantum (~33ms),
// which would drop 60fps frames entirely. Calling calc_acceleration +
// UpdatePhysicsInternal(dt) directly gives us the same Euler integration
// and friction with a caller-controlled dt, which is what we want.
var preIntegratePos = _body.Position;
_body.calc_acceleration();
_body.UpdatePhysicsInternal(dt);
var postIntegratePos = _body.Position;
// ── 5. Terrain/cell Z snap and ground-contact detection ───────────────
// Use PhysicsEngine.Resolve to find the ground surface Z under the player.
// We pass a zero delta because PhysicsBody already moved the position.
var resolveResult = _physics.Resolve(
_body.Position, CellId, Vector3.Zero, StepUpHeight);
// ── 5. Collision resolution via CTransition sphere-sweep ─────────────
// The Transition system subdivides the movement from pre→post into
// sphere-radius steps, testing terrain collision at each step.
// Falls back to simple Z-snap if transition fails.
var resolveResult = _physics.ResolveWithTransition(
preIntegratePos, postIntegratePos, CellId,
sphereRadius: 0.48f, // human player radius from Setup
sphereHeight: 1.2f, // human player height from Setup
stepUpHeight: StepUpHeight,
stepDownHeight: 0.04f, // retail default
isOnGround: _body.OnWalkable);
// Apply resolved position.
_body.Position = resolveResult.Position;
if (resolveResult.IsOnGround)
{
float groundZ = resolveResult.Position.Z;
float bodyZ = _body.Position.Z;
if (bodyZ <= groundZ + 0.05f && _body.Velocity.Z <= 0f)
if (_body.Velocity.Z <= 0f)
{
// Player is at or below the ground AND not jumping upward — snap to surface.
_body.Position = new Vector3(_body.Position.X, _body.Position.Y, groundZ);
// Grounded — snap to resolved position and land.
bool wasAirborne = !_body.OnWalkable;
_body.TransientState |= TransientStateFlags.Contact | TransientStateFlags.OnWalkable;
_body.calc_acceleration(); // re-zero gravity acceleration now grounded
_body.calc_acceleration();
// Zero out downward velocity so we don't keep integrating through terrain.
if (_body.Velocity.Z < 0f)
_body.Velocity = new Vector3(_body.Velocity.X, _body.Velocity.Y, 0f);
@ -324,13 +324,18 @@ public sealed class PlayerMovementController
}
else
{
// Player is above the ground — airborne.
// Moving upward (jump) — stay airborne even though terrain is below.
_body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
_body.calc_acceleration(); // re-enable gravity
_body.calc_acceleration();
}
}
else
{
// No ground found — airborne.
_body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
_body.calc_acceleration();
}
// Update CellId from the resolve result.
CellId = resolveResult.CellId;
// ── 6. Determine outbound motion commands ─────────────────────────────