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 fromend_dash(), not fromstart_dash(), soDASH_COOLDOWNrepresents time-between-dashes, not a fixed rhythm.invincible_timer— the i-frame window. It's a subset ofdash_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.