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
- 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.
- 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
labelas a verb phrase, such as FIRE GUNS, and one held to work says so: LOOK AROUND (HOLD). - 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. - 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. - Group the actions in columns by purpose, each in order of use: moving the thing in one, acting with it in the other.
- 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
| Piece | Where it goes |
|---|---|
| The actions | world.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 keys | player.client.ts calls plugins.player.control(...) in place of bind. |
| The store | store listed before player in every scene that lists player, and "store": {} in the manager's prefabs. |
| The screen | A 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 size | The controls definition in assets/<game>.json. |
| The menu's button | A 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
- List
storethenplayerin the menu scene'splugins, since the screen calls the player plugin. Each connection then gets aplayerrow there. - Give the menu scene
seats: every child scene's seats added up. - List the game's
playerkind in itssim, and check that itsbeforeandafterdo nothing for a row that holds no thing. Its keys are bound on the menu too, where the modal takes every key. - Anything that took listing
playerto mean a seat arrives to play, such as an arrival sequence, reads aworldflag instead, true on the manager and false on the menu scene.
4. The panel and the definition
- 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. - Give
world.ui.keysthe pad's keys beside the keyboard's,Pad12toPad15for the focus,Pad0to press andPad1to back out, or a pad cannot reach the screen. - 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.
- Size each button from its longest text. A monospace character at size
swith letter spacinglis about0.6 s + lpixels wide, so RIGHT ARROW or D at 12 and 1 is about 150 pixels and fits a 240 pixel button.
5. The look
- In
controls.client.ts, draw the screen each frame the panel is open, from the player plugin'sactions,listen,rebind,restore,conflictsanddevice. - 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.
- 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.
- 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
- 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'splayer.keysand keeps the other device's keys, Escape while listening changes nothing, a shared key shows on the conflicts line, RESTORE DEFAULTS emptiesplayer.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. - 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.