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 ─────────────────────────────

View file

@ -259,4 +259,43 @@ public sealed class PhysicsEngine
targetCellId,
IsOnGround: true);
}
/// <summary>
/// Resolve movement using the CTransition sphere-sweep system.
/// 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.
/// </summary>
public ResolveResult ResolveWithTransition(
Vector3 currentPos, Vector3 targetPos, uint cellId,
float sphereRadius, float sphereHeight,
float stepUpHeight, float stepDownHeight,
bool isOnGround)
{
var transition = new Transition();
transition.ObjectInfo.StepUpHeight = stepUpHeight;
transition.ObjectInfo.StepDownHeight = stepDownHeight;
transition.ObjectInfo.StepDown = true;
if (isOnGround)
transition.ObjectInfo.State |= ObjectInfoState.Contact | ObjectInfoState.OnWalkable;
transition.SpherePath.InitPath(currentPos, targetPos, cellId, sphereRadius, sphereHeight);
bool ok = transition.FindTransitionalPosition(this);
if (ok)
{
var sp = transition.SpherePath;
var ci = transition.CollisionInfo;
bool onGround = ci.ContactPlaneValid
|| transition.ObjectInfo.State.HasFlag(ObjectInfoState.OnWalkable);
return new ResolveResult(sp.CheckPos, sp.CheckCellId, onGround);
}
// Transition failed — fall back to simple resolve.
return Resolve(currentPos, cellId, targetPos - currentPos, stepUpHeight);
}
}