fix(app): Phase B.2 — use server position directly, fix yaw wrap + turn spam
Three more fixes from the diagnostic dump: 1. Initial position: PhysicsEngine.Resolve was mapping the player into an indoor EnvCell (foundry at Z=66) when they're standing on outdoor terrain at Z=93+. The cell-containment check was too aggressive for initial placement. Now uses the server-sent position directly — the server already gave us a valid position. 2. Yaw unbounded: mouse delta accumulated without wrapping, growing to 24+ radians. Now wraps to [-PI, PI] after every turn. 3. Turn command spam: MouseDeltaX > 0.5 threshold was too low for raw pixel deltas. Any mouse jitter triggered turnCmd flips every frame → stateChanged=True → MoveToState flood to the server. Mouse turning now only affects yaw directly; turn COMMANDS only come from A/D keyboard (matching retail client behavior where mouse-look doesn't generate a TurnRight/TurnLeft command). 265 tests still green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6202c5d153
commit
97c17c5bc3
3 changed files with 39 additions and 8 deletions
|
|
@ -79,6 +79,10 @@ public sealed class PlayerMovementController
|
||||||
if (input.TurnLeft)
|
if (input.TurnLeft)
|
||||||
Yaw += TurnSpeed * dt;
|
Yaw += TurnSpeed * dt;
|
||||||
Yaw -= input.MouseDeltaX * MouseTurnSensitivity;
|
Yaw -= input.MouseDeltaX * MouseTurnSensitivity;
|
||||||
|
// Wrap yaw to [-PI, PI] so it doesn't grow unbounded and cause
|
||||||
|
// NaN/overflow in sin/cos and confuse the chase camera offset.
|
||||||
|
while (Yaw > MathF.PI) Yaw -= 2f * MathF.PI;
|
||||||
|
while (Yaw < -MathF.PI) Yaw += 2f * MathF.PI;
|
||||||
|
|
||||||
// 2. Compute movement delta in the player's facing direction.
|
// 2. Compute movement delta in the player's facing direction.
|
||||||
float speed = input.Run ? RunSpeed : WalkSpeed;
|
float speed = input.Run ? RunSpeed : WalkSpeed;
|
||||||
|
|
@ -130,12 +134,17 @@ public sealed class PlayerMovementController
|
||||||
sidestepSpeed = speed * 0.5f / WalkSpeed;
|
sidestepSpeed = speed * 0.5f / WalkSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.TurnRight || input.MouseDeltaX > 0.5f)
|
// Turn commands from KEYBOARD only (A/D). Mouse turning is applied
|
||||||
|
// directly to Yaw above and doesn't generate a turn command — if it
|
||||||
|
// did, mouse jitter would flip turnCmd between TurnRight/TurnLeft
|
||||||
|
// every frame, causing stateChanged=True on every frame and flooding
|
||||||
|
// the server with MoveToState spam.
|
||||||
|
if (input.TurnRight)
|
||||||
{
|
{
|
||||||
turnCmd = 0x6500000Du; // TurnRight
|
turnCmd = 0x6500000Du; // TurnRight
|
||||||
turnSpeed = TurnSpeed;
|
turnSpeed = TurnSpeed;
|
||||||
}
|
}
|
||||||
else if (input.TurnLeft || input.MouseDeltaX < -0.5f)
|
else if (input.TurnLeft)
|
||||||
{
|
{
|
||||||
turnCmd = 0x6500000Eu; // TurnLeft
|
turnCmd = 0x6500000Eu; // TurnLeft
|
||||||
turnSpeed = TurnSpeed;
|
turnSpeed = TurnSpeed;
|
||||||
|
|
|
||||||
|
|
@ -203,12 +203,15 @@ public sealed class GameWindow : IDisposable
|
||||||
float plocalX = playerEntity.Position.X - (plbX - _liveCenterX) * 192f;
|
float plocalX = playerEntity.Position.X - (plbX - _liveCenterX) * 192f;
|
||||||
float plocalY = playerEntity.Position.Y - (plbY - _liveCenterY) * 192f;
|
float plocalY = playerEntity.Position.Y - (plbY - _liveCenterY) * 192f;
|
||||||
uint pinitCellId = ((uint)plbX << 24) | ((uint)plbY << 16) | 0x0001u;
|
uint pinitCellId = ((uint)plbX << 24) | ((uint)plbY << 16) | 0x0001u;
|
||||||
// Let the physics engine resolve the correct Z for the initial position
|
// Use the server-sent position directly — the server already
|
||||||
// so we don't start underground.
|
// gave us a valid position at CreateObject time. Running it
|
||||||
var initResult = _physicsEngine.Resolve(
|
// through PhysicsEngine.Resolve was mapping the player into
|
||||||
playerEntity.Position, pinitCellId,
|
// an indoor cell (e.g., the foundry at Z=66) when they're
|
||||||
System.Numerics.Vector3.Zero, 10f);
|
// actually standing on outdoor terrain at Z=93+. The physics
|
||||||
_playerController.SetPosition(initResult.Position, initResult.CellId);
|
// engine's cell-containment check is too aggressive for the
|
||||||
|
// initial placement. Once the player starts moving, Resolve
|
||||||
|
// will keep them on the correct surface.
|
||||||
|
_playerController.SetPosition(playerEntity.Position, pinitCellId & 0xFFFFu);
|
||||||
// Derive initial yaw from the entity's server-sent rotation
|
// Derive initial yaw from the entity's server-sent rotation
|
||||||
// rather than hardcoding. Extract yaw from the quaternion.
|
// rather than hardcoding. Extract yaw from the quaternion.
|
||||||
var q = playerEntity.Rotation;
|
var q = playerEntity.Rotation;
|
||||||
|
|
@ -216,6 +219,12 @@ public sealed class GameWindow : IDisposable
|
||||||
2f * (q.W * q.Z + q.X * q.Y),
|
2f * (q.W * q.Z + q.X * q.Y),
|
||||||
1f - 2f * (q.Y * q.Y + q.Z * q.Z));
|
1f - 2f * (q.Y * q.Y + q.Z * q.Z));
|
||||||
_playerController.Yaw = yaw;
|
_playerController.Yaw = yaw;
|
||||||
|
|
||||||
|
Console.WriteLine($"[PLAYER-INIT] entityPos=({playerEntity.Position.X:F1},{playerEntity.Position.Y:F1},{playerEntity.Position.Z:F1}) " +
|
||||||
|
$"entityRot=({q.X:F3},{q.Y:F3},{q.Z:F3},{q.W:F3}) " +
|
||||||
|
$"initCellId=0x{(pinitCellId & 0xFFFFu):X4} " +
|
||||||
|
$"yaw={yaw:F3} " +
|
||||||
|
$"physics landblocks={_physicsEngine.LandblockCount}");
|
||||||
_chaseCamera = new AcDream.App.Rendering.ChaseCamera
|
_chaseCamera = new AcDream.App.Rendering.ChaseCamera
|
||||||
{
|
{
|
||||||
Aspect = _window!.Size.X / (float)_window.Size.Y,
|
Aspect = _window!.Size.X / (float)_window.Size.Y,
|
||||||
|
|
@ -1489,6 +1498,16 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
var result = _playerController.Update((float)dt, input);
|
var result = _playerController.Update((float)dt, input);
|
||||||
|
|
||||||
|
// DIAG: dump player state every ~60 frames to see what's happening.
|
||||||
|
if (_perfFrameCount % 60 == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[PLAYER] pos=({result.Position.X:F1},{result.Position.Y:F1},{result.Position.Z:F1}) " +
|
||||||
|
$"cell=0x{result.CellId:X8} ground={result.IsOnGround} " +
|
||||||
|
$"yaw={_playerController.Yaw:F2} " +
|
||||||
|
$"fwdCmd={result.ForwardCommand?.ToString("X8") ?? "idle"} " +
|
||||||
|
$"stateChanged={result.MotionStateChanged}");
|
||||||
|
}
|
||||||
|
|
||||||
// Update the player entity's position + rotation so it renders at
|
// Update the player entity's position + rotation so it renders at
|
||||||
// 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))
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ public sealed class PhysicsEngine
|
||||||
{
|
{
|
||||||
private readonly Dictionary<uint, LandblockPhysics> _landblocks = new();
|
private readonly Dictionary<uint, LandblockPhysics> _landblocks = new();
|
||||||
|
|
||||||
|
/// <summary>Number of registered landblocks (diagnostic).</summary>
|
||||||
|
public int LandblockCount => _landblocks.Count;
|
||||||
|
|
||||||
private sealed record LandblockPhysics(
|
private sealed record LandblockPhysics(
|
||||||
TerrainSurface Terrain,
|
TerrainSurface Terrain,
|
||||||
IReadOnlyList<CellSurface> Cells,
|
IReadOnlyList<CellSurface> Cells,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue