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
- In a scene that lists player, a connection takes the lowest free slot below the scene's
seatsand is given aplayerrow with that slot in itsseatfield. A scene without the plugin, such as a menu, gives a plain seat with no row. - For a second seat, raise the scene's
seatsto 2 and add astartfor slot 1. The child scene'sseatscounts; the manager's is not read. - 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
- Add one
startactor per slot to the scene'sactors, holdingslot,posand anything else the thing starts with:{ "is": "start", "slot": 1, "pos": [700, 270] }. - The
playerrow'sbeforefinds its start withw.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. - 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'spos,placedandbefore.
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 undercontrol.
Triggers, sticks and rumble
- Where an act takes a fraction, such as a throttle on a trigger or steering on a stick, read the
axis,stickorholdanalog:analog: trueon its entry underbind, or its action in the table'sanaloglist undercontrol. Read plainly, a trigger sends 1 for the lightest squeeze. - Give the scene
world.player.resolution, the number of steps a value is rounded to, so a stick held still sends nothing. See player. - 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.
- 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.
- 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. - A debug view or a controls screen shows what the plugin hears with
plugins.player.pads()andplugins.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
- Write the act first, for example
dashonplayer.sim.ts. A key bound to an act the row does not have does nothing. - Add
world.playerto 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 } - List store before
playerinpluginsand give the scene astoreprefab,{}when the game reads no keys of its own. The player's choices live in the store asplayer.keys,player.toggle,player.deadzoneandplayer.repeat, on the page only. - In
player.client.ts, replacebindwithcontrol({ stick: { move: (x, y) => ['move', x, y] }, press: { dash: ['dash'] } }). - In a game of several scenes,
world.playerin<game>.scene.jsonapplies to every scene. A child scene that gives its ownplayerkey 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.htmlthe truth is in the tab, so a press is drawn on the next frame. - On
client.htmlthe page wraps its link in predict and the scene setsworld.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
playerrow. 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
openand sent withplugins.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
- Run
yarn editor <game>, openindex.html, and move with every key of each action. Open the controls screen, rebind a part, play the new key and Restore. - Run the server and open
client.htmlin two windows side by side (a background tab stops drawing). Each seat's keys move only its own thing, on the next frame.