Making a game

Building a controls screen

A menu screen where the player sees each control and rebinds its keys.

A controls screen answers one question for the player: what do I press to do this? It is a ui modal opened from the menu before play, built on the player plugin's actions, so the game's keys are on control rather than bind (Input and seats).

1. Design the lines

  1. Give every part of every action its own line: a stick is four lines, an axis two, a button one. Never put an action's parts side by side on one row.
  2. Name each line for what happens, in the player's words. Read the rule the act reaches before naming a direction: a stick's up key that pitches the nose down is NOSE DOWN, an axis that moves a throttle is THROTTLE DOWN and THROTTLE UP. A button's line is its label as a verb phrase, such as FIRE GUNS, and one held to work says so: LOOK AROUND (HOLD).
  3. Spell every key out in words: SPACE, MINUS, CTRL, LEFT ARROW, RIGHT MOUSE, LEFT TRIGGER, D-PAD UP. Never a glyph or a code such as Pad7. Join several keys on one line with "or", and write NOT SET on a line with none.
  4. Show only the keys of the device in the player's hands, by device(), and say which in the hint: SHOWING KEYBOARD · ESC TO GO BACK. Rebinding a line replaces that device's keys and keeps the other's.
  5. Group the actions in columns by purpose, each in order of use: moving the thing in one, acting with it in the other.
  6. Write every message as a sentence: F does both BOOST and FIRE MISSILE. While listening, PRESS A KEY · ESC TO CANCEL.

2. Put the pieces in place

PieceWhere it goes
The actionsworld.player.actions in <game>.scene.json, so every scene reads the same ones, each with keyboard and pad keys, and world.player.deadzone when a side of a stick is bound.
The thing's keysplayer.client.ts calls plugins.player.control(...) in place of bind.
The storestore listed before player in every scene that lists player, and "store": {} in the manager's prefabs.
The screenA kind of the game's own, controls: an empty rule in controls.sim.ts, listed in the menu scene's sim, a prefab "controls": {} in the manager, and { "is": "controls" } in the menu scene's actors.
Every word, place and sizeThe controls definition in assets/<game>.json.
The menu's buttonA CONTROLS button in the menu's own definition, with "open": "controls", which the menu's look answers by opening the panel.

3. Give the menu scene the player plugin

  1. List store then player in the menu scene's plugins, since the screen calls the player plugin. Each connection then gets a player row there.
  2. Give the menu scene seats: every child scene's seats added up.
  3. List the game's player kind in its sim, and check that its before and after do nothing for a row that holds no thing. Its keys are bound on the menu too, where the modal takes every key.
  4. Anything that took listing player to mean a seat arrives to play, such as an arrival sequence, reads a world flag instead, true on the manager and false on the menu scene.

4. The panel and the definition

  1. In the menu scene's world.ui, add a modal panel, "controls": { "anchor": "center", "size": [1000, 640], "modal": true, "layer": 2, "backdrop": { … } }, on a layer over the menu's own panel, and a button style for the key buttons in the menu's font.
  2. Give world.ui.keys the pad's keys beside the keyboard's, Pad12 to Pad15 for the focus, Pad0 to press and Pad1 to back out, or a pad cannot reach the screen.
  3. In the definition, give the title, the columns and the actions in each, the step between lines, the side names of every axis and stick action (left, right, up, down; the -1 side, then the +1 side), a name for every key a player could bind, and the restore, done, listening, conflicts and hint lines. Every action is in exactly one column.
  4. Size each button from its longest text. A monospace character at size s with letter spacing l is about 0.6 s + l pixels wide, so RIGHT ARROW or D at 12 and 1 is about 150 pixels and fits a 240 pixel button.

5. The look

  1. In controls.client.ts, draw the screen each frame the panel is open, from the player plugin's actions, listen, rebind, restore, conflicts and device.
  2. Draw an opaque fill first, so the menu does not show through, then the key buttons, so the first takes the focus, then the text.
  3. Give each button's label the text part of the button style, not the whole style, or it draws large, black and in a serif font.
  4. Pressing a line's button listens for the next key and rebinds that part; Escape changes nothing. RESTORE DEFAULTS restores every action, and DONE closes the panel.

Checking it

  1. A page test, games/<game>/controls.client.test.ts, opens the screen from the menu's CONTROLS and checks that every part has its line in words, a pressed line rebinds into the store's player.keys and keeps the other device's keys, Escape while listening changes nothing, a shared key shows on the conflicts line, RESTORE DEFAULTS empties player.keys, a pad moves the focus and shows the pad's keys, and a rebound key plays in the game while the old one does nothing.
  2. Open it in Chrome and screenshot it closed, open, listening and showing a conflict, with a pad and without. Every name is in plain words, every label is inside its button in the button's font, and nothing of the menu shows through.