diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index e96269c..eb2d92c 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -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 ───────────────────────────── diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 2cd02ef..9cdb173 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -259,4 +259,43 @@ public sealed class PhysicsEngine targetCellId, IsOnGround: true); } + + /// + /// 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 if the transition fails. + /// + 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); + } }