Skip to content

Acceleration friction movement

const MAX_SPEED    = 200.0   // units/sec
const ACCELERATION = 1500.0  // units/sec^2, applied while there's input
const FRICTION     = 2000.0  // units/sec^2, applied while there's no input

state velocity = Vector2(0, 0)

function get_input_direction():
    dir = 0
    if input.is_down("move_right"): dir += 1
    if input.is_down("move_left"):  dir -= 1
    return dir

function apply_horizontal_acceleration(dt):
    input_dir = get_input_direction()
    target_speed = input_dir * MAX_SPEED
    rate = ACCELERATION if input_dir != 0 else FRICTION
    velocity.x = move_toward(velocity.x, target_speed, rate * dt)

function integrate_position(dt):
    position += velocity * dt
    resolve_collision(axis = "x")

function update(dt):
    apply_horizontal_acceleration(dt)
    integrate_position(dt)

How it works

Setting velocity.x = input_dir * MAX_SPEED directly gives you instant start/stop — the square teleports between "not moving" and "full speed" in one frame, which reads as robotic and gives the player zero room to make fine positional adjustments.

The fix is to treat velocity as something that chases a target rather than snapping to it. Each frame you compute a target_speed (either MAX_SPEED in the input direction, or 0 if there's no input), then move the current velocity toward that target by a limited amount per frame — that's what Move Toward does. The rate you're allowed to close that gap by is ACCELERATION * dt when the player is actively pushing a direction, and FRICTION * dt when they let go.

Splitting acceleration and friction into two separate constants (rather than one "responsiveness" value) is the important design move: it lets you tune how quickly a character starts moving independently of how quickly it stops. High accel + low friction feels like ice — quick to get going, slides forever once you let go. Low accel + high friction feels heavy and grounded but sluggish to react. Most platformers land somewhere with accel and friction both fairly high but friction slightly higher, so stopping feels crisp while starting still has a hint of ramp-up.

Note this uses a linear ramp (move_toward, constant rate) rather than an exponential/Linear Interpolation (Lerp)-based ramp (velocity = lerp(velocity, target, k * dt)). Linear ramps reach the target in a fixed, predictable amount of time regardless of frame rate; exponential smoothing technically never reaches the target and its effective "time to target" changes with dt in ways that are harder to reason about. Constant-rate acceleration is the standard choice for character controllers for this reason — save lerp-style smoothing for camera work or cosmetic effects.