Skip to content

Dash with cooldown and invincibility

const DASH_SPEED               = 700.0  // units/sec, constant during dash
const DASH_DURATION            = 0.15   // seconds
const DASH_COOLDOWN            = 0.80   // seconds, starts when dash ENDS
const DASH_INVINCIBLE_DURATION = 0.10   // seconds, subset of DASH_DURATION
const DASH_AFFECTED_BY_GRAVITY = false

enum MoveState { NORMAL, DASHING }

state move_state       = MoveState.NORMAL
state dash_timer        = 0.0
state cooldown_timer    = 0.0
state invincible_timer  = 0.0
state dash_direction    = Vector2(0, 0)

function handle_dash_input():
    if move_state == MoveState.NORMAL
       and input.was_pressed("dash")
       and cooldown_timer <= 0.0:
        start_dash()

function start_dash():
    move_state = MoveState.DASHING
    dash_timer = DASH_DURATION
    invincible_timer = DASH_INVINCIBLE_DURATION

    // Capture direction ONCE at dash start — held input, not re-polled per frame,
    // so the dash always goes in a single straight line. Fall back to facing
    // direction if no directional input is held.
    input_dir = get_2d_input_vector()   // see section 12
    dash_direction = input_dir.normalized() if input_dir.length() > 0 else facing_direction

    velocity = dash_direction * DASH_SPEED
    if not DASH_AFFECTED_BY_GRAVITY:
        // Freeze vertical fall/rise for the duration; re-enabled on dash end.

function update_dash(dt):
    if move_state != MoveState.DASHING:
        return

    dash_timer -= dt
    invincible_timer = max(0.0, invincible_timer - dt)

    if not DASH_AFFECTED_BY_GRAVITY:
        velocity = dash_direction * DASH_SPEED   // hold constant, ignore gravity/friction

    if dash_timer <= 0.0:
        end_dash()

function end_dash():
    move_state = MoveState.NORMAL
    cooldown_timer = DASH_COOLDOWN
    // Velocity is left as-is (usually clamped to MAX_SPEED) so momentum
    // carries into normal movement instead of snapping to zero.
    velocity = velocity.clamped(MAX_SPEED)

function update_cooldown(dt):
    cooldown_timer = max(0.0, cooldown_timer - dt)

function is_invincible():
    return invincible_timer > 0.0

function update(dt):
    update_cooldown(dt)
    handle_dash_input()

    if move_state == MoveState.DASHING:
        update_dash(dt)
    else:
        apply_horizontal_acceleration(dt)
        handle_jump_input()
        apply_gravity(dt)

    integrate_position(dt)
    integrate_vertical(dt)

How it works

A dash is deliberately not built on the accel/friction model from Acceleration friction movement — it overrides velocity directly for its duration instead of chasing a target. That's the difference in feel: running ramps up, a dash is instantaneous and reads as a committed, unmodifiable burst. That's also why direction is captured once in start_dash() rather than re-read every frame — if you kept polling input during the dash, players could curve it mid-air, which undermines the "committed burst" feeling most dash designs are going for.

Three separate timers do three separate jobs, and conflating them is the most common bug in dash implementations:

  • dash_timer — how long the dash itself lasts (movement override active).
  • cooldown_timer — how long until you can dash again. Critically, this starts counting from end_dash(), not from start_dash(), so DASH_COOLDOWN represents time-between-dashes, not a fixed rhythm.
  • invincible_timer — the i-frame window. It's a subset of dash_timer (DASH_INVINCIBLE_DURATION <= DASH_DURATION), not an independent timer, specifically so invincibility can never accidentally outlive the dash — that would let players sit in a damage-immune state doing nothing, which is exploitable. Many games set this equal to the full dash duration for simplicity, or shorter to cover only the "commit" startup frames, punishing badly-timed dashes into a hazard.

DASH_AFFECTED_BY_GRAVITY is called out as an explicit flag because it's a real design fork, not an implementation detail: horizontal-only dashes that null out gravity read as more "video-gamey" and give more precise spacing tools, while gravity-affected dashes (an angled burst that still arcs) feel more physically grounded but are harder to use reliably for platforming.