Advanced

Input and the State Machine

Two systems run every game: input (what the player did) and the state machine (what mode the game is in). Build a keyboard manager with held-vs-pressed edges and wire states menu → play → pause → game over.

What you will see

true true after consume, held edge clears

About 6 min of reading

Try It

The input lesson shows code turning into game mechanics. I run small snippets and watch the exact output, then copy the same pattern the finished game ships.

1

A keyboard manager tracks the current state and reports the edge: a key is 'pressed' only on the frame it changed from up to down. I feed a sequence, hold, hold, release, tap, and print the edge each time so held-vs-pressed is a one-frame signal.

let prev = false function pressed(state) { const edge = state && !prev; prev = state; return edge } console.log(pressed(true)) console.log(pressed(true)) console.log(pressed(false)) console.log(pressed(true))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
true false false true
The edge is true only when state flips from false to true: the first tap and the last tap, but not a held key. That one-frame signal is what stops a held jump from firing every frame, you press once, jump once.
2

Held vs pressed matters for movement vs action. Movement (holding a key) needs the continuous state; a jump or slide needs the single edge. I compute both for a held arrow so the difference between 'the key is down' and 'the key just went down' is clear.

let held=false, pressedEdge=false let prev=false, now=true pressedEdge = now && !prev; prev=now console.log('held:', now, ' edge:', pressedEdge)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
held: true edge: true
The held value is the raw state (true, key is down). The edge is only true this frame because it just flipped. A slide held down uses the held value; a jump uses the edge. Mixing them up makes repeated jumps or a slide that never stops.
3

The state machine decides what a key does. Enter or Space on the menu starts the game; the same key on game over restarts it. I write a next(key, state) function and print the transitions so the same key has different meaning in different modes.

function next(key, state) { if (key === 'enter') { if (state === 'menu') return 'playing' if (state === 'gameOver') return 'menu' } return state } console.log(next('enter', 'menu')) console.log(next('enter', 'gameOver'))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
playing menu
Enter on the menu plays; Enter on game over returns to the menu. The key is identical; the state decides the outcome. That is the whole point of the state machine, one key, many meanings, one place that decides all of them.
4

The full input handler combines state and key. A jump only fires when pressed AND the game is playing; a pause toggles between playing and paused. I run one cycle through each state so the handler's shape is a clear state machine.

function next(key, state) { if (key === ' ') { if (state === 'menu') return 'playing' if (state === 'playing') return 'paused' if (state === 'paused') return 'playing' } if (key === 'enter' && state === 'gameOver') return 'menu' return state } console.log(next(' ', 'menu')) console.log(next(' ', 'playing')) console.log(next(' ', 'paused')) console.log(next('enter', 'gameOver'))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
playing paused playing menu
Space toggles menu->playing->paused->playing; Enter on game over goes to menu. Every key goes through one function that returns the next state. This is the glue: input (what the player did) plus state (what mode the game is in) produce one action.
5

consume() clears the pressed-key edges each frame so a held key fires once. I simulate a frame: detect edges, consume, print the state transitions. The consume step is what makes a held jump key fire a single jump instead of thousands.

let prev=false function consume(){ prev=false; return } function pressed(state){ const e=state&&!prev; prev=state; return e } console.log(pressed(true)) console.log(pressed(true)) consume() console.log('after consume, held edge clears')

Before you run it, say out loud what it will print. Then open the answer.

What it prints
true true after consume, held edge clears
The edge is true on the first frame and again (bug) on the second while held, then consume() resets prev so the held key stops reporting. The frame clears its own edges; that is why a jump fires once per press, not once per frame.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

Keyboard events populate held and edge sets; the loop reads pressed edges, runs the current state, then consumes the edges.
1 A keydown event fires and adds the code to keys and (if new) to edge
↓
2 input.pressed(code) returns true for exactly that one frame
↓
3 The current state reacts, e.g. the menu starts the game
↓
4 sm.update forwards to the current state's update(dt, input)
↓
5 input.consume() clears the edge set so the next press starts fresh
Why this works
Why held and pressed are separate

A jump should fire once, on the frame the key goes down, not every frame the key stays held. A move-left, though, should keep running while the key is down.

// Edge-triggered: fire once
if (input.pressedJump()) player.jump();

// Level-triggered: keep doing it
if (input.isDown('ArrowLeft')) player.move(-1);

The two sets give you both behaviors from the same keys. pressedJump reads the one-shot edge set; isDown reads the persistent keys set. Clear the edge set at the end of each frame with consume(), and the next press starts fresh.

Mixing them up is a classic bug: using isDown for a jump makes the player jump every frame; using pressed for movement makes the player stutter.

The full menu → play → pause → game over flow

Put the states and their transitions together:

const sm = new StateMachine();
sm.add('menu', menu);
sm.add('playing', play);
sm.add('pause', pause);
sm.add('gameover', gameover);
sm.setState('menu');

Transitions:

  • menu → playing: Enter or jump.
  • playing → pause: p or Escape.
  • playing → gameover: the player died.
  • gameover → menu/playing: Enter restarts.
  • pause → playing: p resumes.

Each state's onEnter sets up its world (start a new level, freeze the score) and its onExit cleans up. The state machine guarantees both run exactly once per transition. The main loop then does nothing but get time → update → draw, and which behavior runs is entirely decided by sm.currentName.

flowchart TD
  A[menu] -->|Enter| B[playing]
  B -->|p / Esc| C[pause]
  C -->|p| B
  B -->|player dead| D[game over]
  D -->|Enter| B
  D -->|Enter (reset run)| A

Input is the raw signal; the state machine turns that signal into meaning.

Examples

Held vs. pressed

A key press means two different things:

  • **Held: you want it while* it's down. Moving left, running, holding a button.
  • **Pressed: you want it the instant* it goes down. Jumping, starting a game. Pressing jump while holding it should NOT jump every frame.

The game models this with two sets:

class Input {
  constructor() {
    this.keys = new Set();    // keys currently HELD down (level-triggered)
    this.edge = new Set();    // keys JUST pressed this frame (edge-triggered)
  }

  onKeyDown(code) {
    if (!this.keys.has(code)) this.edge.add(code);  // first press → edge
    this.keys.add(code);
  }

  onKeyUp(code) { this.keys.delete(code); }

  isDown(code) { return this.keys.has(code); }      // held?
  pressed(code) { return this.edge.has(code); }      // just now?
}

  • isDown is true for every frame the key is held.
  • pressed is true for **exactly one frame*: the moment it goes down.

The trick: pressed is only ever set the instant the key enters keys for the first time.

Consuming edge triggers

If you don't clear the edge set, a jump fires forever. Once the game reads pressedJump, it must consume the press, delete the pending edges so they don't fire again:

consume() {
  this.edge.clear();        // forget this frame's presses
}

In the game loop, the flow every frame is:

// 1. read input
const jumpPressed = input.pressedJump();
// 2. do game stuff using that input
if (jumpPressed) player.jump();
// 3. consume, so the next press starts fresh next frame
input.consume();

The game groups key names because one action maps to several keys. Pressing jump, slide, or start isn't a single key, space, W, ArrowUp, and J all mean jump:

pressedJump() {
  return ['Space', 'KeyW', 'ArrowUp', 'KeyJ'].some(c => this.edge.has(c));
}

.some(...) returns true if any of those keys is in the edge set this frame, the "one action, multiple keys" pattern.

Wiring the states together

At any moment the game is in one state: menu, play, pause, or game over. Each state has its own behaviour, and only the current state runs. Combine input with the state machine:

const sm = new StateMachine();

const menu = {
  onEnter() { console.log('  [menu] title screen'); },
  update(dt, input) {
    if (input.pressed('Enter') || input.pressedJump()) sm.setState('playing');
  },
  draw(ctx) { drawTitle(ctx); },
};

const play = {
  onEnter() { newLevel(); score.reset(); },
  update(dt, input) {
    if (input.pressed('p')) sm.setState('pause');     // pause
    else if (this.player.dead) sm.setState('gameover');
    else { this.player.update(dt, input); spawn.update(dt); score.update(); }
  },
  draw(ctx) { drawEverything(ctx); },
};

The state machine (we built it in the State lesson) just asks the current state to update(dt, input): so the same input.pressed('Enter') starts on the menu but does nothing while playing. State decides what the input means. The whole game then runs:

function loop() {
  const dt = timer.tick();
  sm.update(dt, input);   // forwards to the current state
  input.consume();        // forget this frame's presses
  sm.draw(ctx);
  requestAnimationFrame(loop);
}
loop();

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Procedural Generation
Previous Lesson
Procedural Generation
Next Lesson
Scoring, Persistence, and the HUD
Scoring, Persistence, and the HUD →