Making a game

Input and seats

Keys, mice and gamepads, rebinding, slots, starts and presses.

This page covers how a player controls a game: seats and slots, where each seat starts, binding keys, mice and gamepads, triggers and sticks read analog, rumble, rebinding, how a press is drawn, and a seat that plays itself. What the player row is and how it is built is Designing a seat.

Seats and slots

  1. In a scene that lists player, a connection takes the lowest free slot below the scene's seats and is given a player row with that slot in its seat field. A scene without the plugin, such as a menu, gives a plain seat with no row.
  2. For a second seat, raise the scene's seats to 2 and add a start for slot 1. The child scene's seats counts; the manager's is not read.
  3. A rule about who is who (whose turn, which side) reads the slot, me.data.seat, not the seat number an act is handed.

Where a seat starts

  1. Add one start actor per slot to the scene's actors, holding slot, pos and anything else the thing starts with: { "is": "start", "slot": 1, "pos": [700, 270] }.
  2. The player row's before finds its start with w.find('start', s => s.data.slot === me.data.seat), copies its fields once and marks itself placed. A respawn resets the same row from its start.
  3. A seat that stands nowhere, such as a quiz's or a card game's, has no start: remove start.sim.ts, its prefab and entry, and the row's pos, placed and before.

Binding keys: bind or control

player.client.ts hands the player plugin one table in setup, for the row that is mine, and calls plugins.player.unbind() in remove. Keys are named by place on the keyboard (a, ' ', Shift, ArrowLeft), mouse buttons Mouse0 to Mouse2, gamepad buttons Pad0 to Pad16, the left stick's sides PadStickLeft, PadStickRight, PadStickUp, PadStickDown, and the right stick's PadRightStickLeft, PadRightStickRight, PadRightStickUp, PadRightStickDown. A side of either stick is held past world.player.deadzone, which goes in the scene, never on the player prefab.

  • bind(table) binds behaviour straight to keys. A new game uses it.
  • control(table) binds behaviour to named actions, and the scene binds keys to the actions, so the player can rebind them. Move a game to it when it wants rebinding, or when a gamepad must move through its menus, which the UI hears from a pad only under control.

Triggers, sticks and rumble

  1. Where an act takes a fraction, such as a throttle on a trigger or steering on a stick, read the axis, stick or hold analog: analog: true on its entry under bind, or its action in the table's analog list under control. Read plainly, a trigger sends 1 for the lightest squeeze.
  2. Give the scene world.player.resolution, the number of steps a value is rounded to, so a stick held still sends nothing. See player.
  3. In the act, treat the value as data from outside the program: do nothing with one that is not a finite number, and keep one past its range at the end of the range.
  4. Where the right stick turns a camera only the page moves, its function keeps the value for the look and returns undefined, so nothing goes up the wire.
  5. Shake the pad from the look with plugins.player.rumble({ strong, weak, seconds }), from what it reads off the rows it draws, such as the seat's own car landing hard. It shakes the pad last used, and nothing once the player is on the keyboard.
  6. A debug view or a controls screen shows what the plugin hears with plugins.player.pads() and plugins.player.held().

A gamepad button already down on the first frame of a scene's client is held only on a new press, so the button that chose a menu item presses nothing in the scene it opens.

Moving a game to control

  1. Write the act first, for example dash on player.sim.ts. A key bound to an act the row does not have does nothing.
  2. Add world.player to the scene. One list of keys is a button, two an axis, four a stick (left, right, up, down).
    "player": {
      "actions": {
        "move": { "label": "Move", "keys": [["ArrowLeft", "a", "Pad14", "PadStickLeft"], ["ArrowRight", "d", "Pad15", "PadStickRight"], ["ArrowUp", "w", "Pad12", "PadStickUp"], ["ArrowDown", "s", "Pad13", "PadStickDown"]] },
        "dash": { "label": "Dash", "keys": [" ", "Pad0"] }
      },
      "deadzone": 0.25
    }
  3. List store before player in plugins and give the scene a store prefab, {} when the game reads no keys of its own. The player's choices live in the store as player.keys, player.toggle, player.deadzone and player.repeat, on the page only.
  4. In player.client.ts, replace bind with control({ stick: { move: (x, y) => ['move', x, y] }, press: { dash: ['dash'] } }).
  5. In a game of several scenes, world.player in <game>.scene.json applies to every scene. A child scene that gives its own player key replaces all of it.

A controls screen

The screen where the player sees and rebinds the actions is the game's own controls kind on a ui modal, opened from the menu before play. How it is laid out, where its pieces go, its definition, its look and its tests are in Building a controls screen.

Presses

  • On index.html the truth is in the tab, so a press is drawn on the next frame.
  • On client.html the page wraps its link in predict and the scene sets world.predict, so a press and everything it moves are drawn on the next frame at any lag.
  • A sound the seat's own thing makes is raised from its player row. Every other event goes on a ledger, such as audio's or particle2d's.
  • An act any seat may send at a row it does not possess is listed in the kind's open and sent with plugins.wire.send(row.id, act, ...args). Every act checks its arguments.

A seat that plays itself

On the page, playtester takes a think that reads the rows each frame and names an act, ['move', 1, 0]. On the server, playtest(truth, think) drives a seat with no page.

Checking it

  1. Run yarn editor <game>, open index.html, and move with every key of each action. Open the controls screen, rebind a part, play the new key and Restore.
  2. Run the server and open client.html in two windows side by side (a background tab stops drawing). Each seat's keys move only its own thing, on the next frame.