Momentum based vs snappy top down
// --- Momentum-based (Asteroids-style: rotate + thrust) ---------------------
const TURN_SPEED = 3.5 // radians/sec
const THRUST_ACCEL = 250.0 // units/sec^2, applied along current FACING
const SPACE_DRAG = 0.0 // 0 = true Newtonian (no natural deceleration)
const MOMENTUM_MAX_SPEED = 300.0 // hard clamp — pure Newtonian motion has none naturally
state facing_angle = 0.0 // radians; independent of velocity direction
state velocity_2d = Vector2(0, 0)
function apply_rotation(dt):
turn_input = 0
if input.is_down("turn_right"): turn_input += 1
if input.is_down("turn_left"): turn_input -= 1
facing_angle += turn_input * TURN_SPEED * dt
function apply_momentum_thrust(dt):
if input.is_down("thrust"):
facing_vec = Vector2(cos(facing_angle), sin(facing_angle))
velocity_2d += facing_vec * THRUST_ACCEL * dt
if SPACE_DRAG > 0.0:
velocity_2d *= (1.0 - SPACE_DRAG * dt) // optional slight drag, off by default
if velocity_2d.length() > MOMENTUM_MAX_SPEED:
velocity_2d = velocity_2d.normalized() * MOMENTUM_MAX_SPEED
function update_momentum(dt):
apply_rotation(dt)
apply_momentum_thrust(dt)
position += velocity_2d * dt
// Note: no collision-based velocity zeroing on most axes here — momentum
// games typically bounce or lose only a fraction of speed on impact,
// not stop dead, or the "drift" feel is undermined immediately.
// --- Snappy top-down (direct target-velocity, tight constants) -------------
const SNAPPY_ACCEL = 3000.0 // very high — reads as near-instant
const SNAPPY_FRICTION = 3500.0
function update_snappy(dt):
input_vec = get_2d_input_vector() // section 12
target = input_vec * MAX_SPEED_2D
rate = SNAPPY_ACCEL if input_vec.length() > 0 else SNAPPY_FRICTION
velocity_2d = move_toward_vector(velocity_2d, target, rate * dt)
if input_vec.length() > 0:
facing_direction = input_vec.normalized() // facing follows movement directly
position += velocity_2d * dt
resolve_collision(axis = "x")
resolve_collision(axis = "y")
How it works¶
These two aren't a tuning difference on the same system — they're genuinely different models of what input means.
Momentum-based treats input as forces applied to an object with real inertia. Turning (apply_rotation) only ever changes which way the ship is facing — it does not touch velocity_2d at all. Thrust (apply_momentum_thrust) is the only thing that changes velocity, and it always pushes along the current facing, not along whatever direction the player is holding. The result is the signature Asteroids feel: you can be flying one direction while facing (and thrusting toward) another, sliding sideways through your own momentum, and redirecting your actual trajectory takes sustained counter-thrust rather than an instant flick of the stick. With SPACE_DRAG at 0, velocity persists indefinitely once applied — true to "space," but it also means a speed clamp (MOMENTUM_MAX_SPEED) is necessary as a game-design concession, since unbounded velocity is rarely actually fun or controllable. A small nonzero drag is a common softening — "space, but slightly forgiving" — without abandoning the drift feel entirely.
Snappy top-down movement is, mechanically, nothing more than section 12's accel/friction model with the constants pushed very high — SNAPPY_ACCEL/SNAPPY_FRICTION closing the gap to target velocity so fast per frame that direction changes read as effectively instant. Facing is coupled directly to the movement input (unless combined with the twin-stick decoupling from section 13), reinforcing the read that "the character is where the stick is pointing," full stop, with no persistent inertia to fight against.
The practical takeaway: if a design brief says "make it feel like Asteroids," reach for rotation+thrust — reducing friction on the accel/friction model does not reproduce that feel, because facing and velocity are still the same vector. If a design brief says "tight, responsive twin-stick shooter movement," the accel/friction model with aggressive constants is correct and momentum/thrust would actively fight the intended feel.