feat(gameengine): add player inertia and tune Q3 movement to feel weighty

physics.fps.move was directly setting linearVelocity each frame on the
ground, producing the classic "no inertia" snap-stop feel. Replace with
a CalcFriction-style ramp: velocity accelerates toward target by
ground_accel * dt, decelerates to zero by ground_friction * dt when
input is released.

Tune the quake3 frame loop to match a modern shooter rather than vanilla
Q3 arcade speeds:
  - walk 4.5 m/s (was 8.0 — ~30 km/h is a sprint, not a walk)
  - sprint 1.5x (was 2.0x — 6.75 m/s top speed)
  - jump 0.9m / 0.22s (was 2.0m / 0.4s — Halo-ish hop, not a moon jump)
  - air control 0.15 (was 0.5 — less floaty mid-jump)
  - gravity_scale 1.6 (was 0.3 — was actually pushing the player UP
    when falling because of how the (gravityScale - 1.0) extra-force
    multiplier worked)

Defaults for the new ground_accel (35) / ground_friction (30) params
keep other workflows that use physics.fps.move feeling snappy without
having to update them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 15:13:20 +01:00
parent 2704e9d0e9
commit a52ffd4ba9
2 changed files with 40 additions and 9 deletions
@@ -14,13 +14,15 @@
"typeVersion": 1,
"position": [200, 0],
"parameters": {
"move_speed": 8.0,
"sprint_multiplier": 2.0,
"crouch_multiplier": 0.4,
"jump_height": 2.0,
"jump_duration": 0.4,
"air_control": 0.5,
"gravity_scale": 0.3,
"move_speed": 4.5,
"sprint_multiplier": 1.5,
"crouch_multiplier": 0.45,
"jump_height": 0.9,
"jump_duration": 0.22,
"air_control": 0.15,
"gravity_scale": 1.6,
"ground_accel": 30.0,
"ground_friction": 24.0,
"crouch_height": 0.5,
"stand_height": 1.4
}
@@ -42,6 +42,11 @@ void WorkflowPhysicsFpsMoveStep::Execute(
const float standHeight = getNum("stand_height", 1.6f);
const float airControl = getNum("air_control", 0.3f);
const float gravityScale = getNum("gravity_scale", 1.0f);
// Acceleration model (CalcFriction-style). Velocity accelerates toward the
// target each frame instead of snapping. Equal accel/friction = modern feel.
const float groundAccel = getNum("ground_accel", 35.0f);
const float groundFriction = getNum("ground_friction", 30.0f);
const float dtMove = context.Get<float>("physics_dt", 1.0f / 60.0f);
// Read input state from context (set by input.poll)
bool keyW = context.GetBool("input_key_w", false);
@@ -100,8 +105,32 @@ void WorkflowPhysicsFpsMoveStep::Execute(
}
if (grounded) {
// Full ground control
body->setLinearVelocity(btVector3(moveX, currentVel.y(), moveZ));
// Inertia model: accelerate horizontal velocity toward target instead of
// snap-setting it. When input released, friction decelerates to zero.
float horizX = currentVel.x();
float horizZ = currentVel.z();
if (len > 0.001f) {
float diffX = moveX - horizX;
float diffZ = moveZ - horizZ;
float diffLen = std::sqrt(diffX * diffX + diffZ * diffZ);
float maxStep = groundAccel * dtMove;
if (diffLen > maxStep && diffLen > 0.0f) {
float k = maxStep / diffLen;
diffX *= k;
diffZ *= k;
}
horizX += diffX;
horizZ += diffZ;
} else {
float curSpeed = std::sqrt(horizX * horizX + horizZ * horizZ);
if (curSpeed > 0.001f) {
float drop = std::min(curSpeed, groundFriction * dtMove);
float k = (curSpeed - drop) / curSpeed;
horizX *= k;
horizZ *= k;
}
}
body->setLinearVelocity(btVector3(horizX, currentVel.y(), horizZ));
} else {
// Air control: blend input with current horizontal velocity (Quake-style)
float curX = currentVel.x();