feat(app): Phase B.2 — PlayerMovementController (input → physics → motion state)
Per-frame controller that reads MovementInput (WASD/ZX/Shift/mouse), drives PhysicsEngine.Resolve for collision, and tracks motion state changes for outbound server messages + animation switching. Walk (~4 u/s) and run (~7 u/s) speeds match AC retail. Heartbeat timer triggers AutonomousPosition every ~200ms while moving. 5 new tests covering idle, forward, run, turn, and state-change detection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
84d7d06008
commit
d9cd2b0b1d
2 changed files with 281 additions and 0 deletions
178
src/AcDream.App/Input/PlayerMovementController.cs
Normal file
178
src/AcDream.App/Input/PlayerMovementController.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Input state for a single frame of player movement.
|
||||
/// </summary>
|
||||
public readonly record struct MovementInput(
|
||||
bool Forward = false,
|
||||
bool Backward = false,
|
||||
bool StrafeLeft = false,
|
||||
bool StrafeRight = false,
|
||||
bool TurnLeft = false,
|
||||
bool TurnRight = false,
|
||||
bool Run = false,
|
||||
float MouseDeltaX = 0f);
|
||||
|
||||
/// <summary>
|
||||
/// Result of a single frame's movement update.
|
||||
/// </summary>
|
||||
public readonly record struct MovementResult(
|
||||
Vector3 Position,
|
||||
uint CellId,
|
||||
bool IsOnGround,
|
||||
bool MotionStateChanged,
|
||||
uint? ForwardCommand,
|
||||
uint? SidestepCommand,
|
||||
uint? TurnCommand,
|
||||
float? ForwardSpeed,
|
||||
float? SidestepSpeed,
|
||||
float? TurnSpeed);
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame player movement controller. Reads input, drives the
|
||||
/// physics engine, tracks motion state for animation + server messages.
|
||||
/// </summary>
|
||||
public sealed class PlayerMovementController
|
||||
{
|
||||
private readonly PhysicsEngine _physics;
|
||||
|
||||
public float WalkSpeed { get; set; } = 4f;
|
||||
public float RunSpeed { get; set; } = 7f;
|
||||
public float TurnSpeed { get; set; } = 1.5f;
|
||||
public float MouseTurnSensitivity { get; set; } = 0.003f;
|
||||
public float StepUpHeight { get; set; } = 1.0f;
|
||||
|
||||
public float Yaw { get; set; }
|
||||
public Vector3 Position { get; private set; }
|
||||
public uint CellId { get; private set; }
|
||||
|
||||
// Previous frame's motion commands for change detection.
|
||||
private uint? _prevForwardCmd;
|
||||
private uint? _prevSidestepCmd;
|
||||
private uint? _prevTurnCmd;
|
||||
|
||||
// Heartbeat timer.
|
||||
private float _heartbeatAccum;
|
||||
public const float HeartbeatInterval = 0.2f; // 200ms
|
||||
public bool HeartbeatDue { get; private set; }
|
||||
|
||||
public PlayerMovementController(PhysicsEngine physics)
|
||||
{
|
||||
_physics = physics;
|
||||
}
|
||||
|
||||
public void SetPosition(Vector3 pos, uint cellId)
|
||||
{
|
||||
Position = pos;
|
||||
CellId = cellId;
|
||||
}
|
||||
|
||||
public MovementResult Update(float dt, MovementInput input)
|
||||
{
|
||||
// 1. Apply turning from keyboard + mouse.
|
||||
if (input.TurnRight)
|
||||
Yaw -= TurnSpeed * dt;
|
||||
if (input.TurnLeft)
|
||||
Yaw += TurnSpeed * dt;
|
||||
Yaw -= input.MouseDeltaX * MouseTurnSensitivity;
|
||||
|
||||
// 2. Compute movement delta in the player's facing direction.
|
||||
float speed = input.Run ? RunSpeed : WalkSpeed;
|
||||
float forwardX = MathF.Cos(Yaw);
|
||||
float forwardY = MathF.Sin(Yaw);
|
||||
float rightX = MathF.Sin(Yaw);
|
||||
float rightY = -MathF.Cos(Yaw);
|
||||
|
||||
float dx = 0f, dy = 0f;
|
||||
if (input.Forward) { dx += forwardX * speed * dt; dy += forwardY * speed * dt; }
|
||||
if (input.Backward) { dx -= forwardX * speed * dt * 0.65f; dy -= forwardY * speed * dt * 0.65f; }
|
||||
if (input.StrafeRight) { dx += rightX * speed * dt * 0.5f; dy += rightY * speed * dt * 0.5f; }
|
||||
if (input.StrafeLeft) { dx -= rightX * speed * dt * 0.5f; dy -= rightY * speed * dt * 0.5f; }
|
||||
|
||||
var delta = new Vector3(dx, dy, 0f);
|
||||
|
||||
// 3. Resolve via physics engine.
|
||||
var result = _physics.Resolve(Position, CellId, delta, StepUpHeight);
|
||||
Position = result.Position;
|
||||
CellId = result.CellId;
|
||||
|
||||
// 4. Determine current motion commands.
|
||||
uint? forwardCmd = null;
|
||||
float? forwardSpeed = null;
|
||||
uint? sidestepCmd = null;
|
||||
float? sidestepSpeed = null;
|
||||
uint? turnCmd = null;
|
||||
float? turnSpeed = null;
|
||||
|
||||
if (input.Forward)
|
||||
{
|
||||
forwardCmd = input.Run ? 0x44000007u : 0x45000005u; // RunForward / WalkForward
|
||||
forwardSpeed = input.Run ? RunSpeed / WalkSpeed : 1.0f;
|
||||
}
|
||||
else if (input.Backward)
|
||||
{
|
||||
forwardCmd = 0x45000005u; // WalkForward (backward is negative speed)
|
||||
forwardSpeed = -0.65f;
|
||||
}
|
||||
|
||||
if (input.StrafeRight)
|
||||
{
|
||||
sidestepCmd = 0x6500000Fu; // SideStepRight
|
||||
sidestepSpeed = speed * 0.5f / WalkSpeed;
|
||||
}
|
||||
else if (input.StrafeLeft)
|
||||
{
|
||||
sidestepCmd = 0x65000010u; // SideStepLeft
|
||||
sidestepSpeed = speed * 0.5f / WalkSpeed;
|
||||
}
|
||||
|
||||
if (input.TurnRight || input.MouseDeltaX > 0.5f)
|
||||
{
|
||||
turnCmd = 0x6500000Du; // TurnRight
|
||||
turnSpeed = TurnSpeed;
|
||||
}
|
||||
else if (input.TurnLeft || input.MouseDeltaX < -0.5f)
|
||||
{
|
||||
turnCmd = 0x6500000Eu; // TurnLeft
|
||||
turnSpeed = TurnSpeed;
|
||||
}
|
||||
|
||||
// 5. Detect motion state change.
|
||||
bool changed = forwardCmd != _prevForwardCmd
|
||||
|| sidestepCmd != _prevSidestepCmd
|
||||
|| turnCmd != _prevTurnCmd;
|
||||
_prevForwardCmd = forwardCmd;
|
||||
_prevSidestepCmd = sidestepCmd;
|
||||
_prevTurnCmd = turnCmd;
|
||||
|
||||
// 6. Heartbeat timer (only while moving).
|
||||
bool isMoving = forwardCmd is not null || sidestepCmd is not null || turnCmd is not null;
|
||||
if (isMoving)
|
||||
{
|
||||
_heartbeatAccum += dt;
|
||||
HeartbeatDue = _heartbeatAccum >= HeartbeatInterval;
|
||||
if (HeartbeatDue) _heartbeatAccum = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
_heartbeatAccum = 0f;
|
||||
HeartbeatDue = false;
|
||||
}
|
||||
|
||||
return new MovementResult(
|
||||
Position: result.Position,
|
||||
CellId: result.CellId,
|
||||
IsOnGround: result.IsOnGround,
|
||||
MotionStateChanged: changed,
|
||||
ForwardCommand: forwardCmd,
|
||||
SidestepCommand: sidestepCmd,
|
||||
TurnCommand: turnCmd,
|
||||
ForwardSpeed: forwardSpeed,
|
||||
SidestepSpeed: sidestepSpeed,
|
||||
TurnSpeed: turnSpeed);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue