Modules / Players & social

playerplugin + addon

The seats a scene declares, and the seat's thing: presses sent up the wire as acts on its player row.

Putting it in a scene

The scene declares how many seats it has, and every connection that takes one gets a player row. List store before player if the scene uses actions, so the player's rebound keys have been read when the keys are checked.

"seats": 4,
"plugins": ["loader", "scenemanager", "store", "player", "render2d"],
"world": {
  "player": {
    "deadzone": 0.25,
    "resolution": 20,
    "repeat": 0.15,
    "actions": {
      "move": {
        "label": "Move",
        "keys": [["ArrowLeft", "a", "Pad14", "PadStickLeft"], ["ArrowRight", "d", "Pad15", "PadStickRight"], ["ArrowUp", "w", "Pad12", "PadStickUp"], ["ArrowDown", "s", "Pad13", "PadStickDown"]]
      },
      "throttle": { "label": "Throttle", "keys": ["Shift", "Pad7"] },
      "jump": { "label": "Jump", "keys": [" ", "Pad0"] }
    }
  }
},
"security": { "player": { "cash": "me" } },
"prefabs": {
  "player": { "seat": 0, "masks": [], "held": null, "cash": 0 }
}

A game's player actor binds keys in its setup, with one table:

plugins.player.bind({
  press: { ' ': ['jump'], Mouse0: ['fire'] },
  stick: [{ keys: [['ArrowLeft', 'a', 'PadStickLeft'], ['ArrowRight', 'd', 'PadStickRight'], ['ArrowUp', 'w', 'PadStickUp'], ['ArrowDown', 's', 'PadStickDown']], send: (x, y) => ['steer', x, y], analog: true }],
  hold: [{ keys: ['Shift', 'Pad7'], send: v => ['throttle', v], analog: true }],
})

Seats and slots

A connection takes the lowest slot below the scene's seats that no seat holds and no player row sits in. It gets a player row with that slot in its seat field and mine: true, and the seat possesses the row, so every press it sends is an act on that row. A player row spawned by something else keeps its slot, and a connection that comes after takes another.

The slot is not the seat number. The seat an act is handed counts every seat the truth has opened, so a connection that leaves and comes back gets a higher one. A rule about who is who, such as whose turn, which pad or which side, reads the slot off the player row and passes that on. Where a seat starts is a start actor per slot, which the player row's before finds by slot.

Every seat is sent every player row, so the row is the thing its seat plays as: the car, the gun or the body. A row every seat shares and none owns is an actors entry, which the player row's acts reach with the slot. A kind reached only through the player row lists no open, so a seat cannot send its acts at it directly. A row that rule code reaches with w.call runs its acts without that check.

Properties

The player row

FieldMeaning
seatThe slot, from 0.
minetrue in the row a seat plays as. Every other seat is sent the row without it, so a page finds its own row with possessed(rows).
held{ kind, id } of the row this player holds, or absent or null while it holds nothing (see Rules).
owner, masksOnly in a scene with a security block (below). owner is the id of a player row, and a player row owns itself. masks is the list of <group>/<name> strings the row holds, and join adds me/<row id>.

Scene settings

Read from world.player unless noted.

SettingMeaning
seats (top level of the scene file)The number of slots. A connection finding none free is refused.
.actions{ name: { label, keys } }: the actions the scene binds keys to (see Actions). label is the name a rebinding screen shows.
.deadzoneHow far out either stick of a gamepad must be pushed before a side of it is held, above 0 and below 1, as a fraction of its travel. Each axis is read on its own, so a push both ways holds a diagonal. Both bind and control read it, and the player's own player.deadzone in the store is read in its place. With none, no stick is read, and a table that names a side of either stick throws.
.resolutionThe number of steps an analog value is rounded to between 0 and 1, a whole number above 0 (see Analog). At 20 a value moves in steps of 0.05. A table that reads anything analog in a scene with no resolution throws.
.repeatSeconds between presses while a press action is held. 0 when left out. The player can change it.
security (top level of the scene file)Hides fields of a row from seats that do not own it (below).
world.visionWith this block, each seat is also sent only the rows its team sees, its team read off its slot (see vision).

security

{ <kind>: { <field>: <group> } }. A group is a name without a /. The player prefab must give a masks list.

RuleMeaning
Who owns a rowA player row owns itself. A row spawned by a hook or act of a row with an owner gets that owner, unless the code spawning it wrote one. A shared row that spawns something for a player writes that player's id as the owner itself.
Who is sent a fieldA listed field of an owned row is sent to a seat when its player row's masks and the owner's hold the same <group>/<name>. me is a group of one: me/<row id> is in only that player's masks.
Everything elseA row with no owner, a field nobody listed and every field of a scene with no security block go to every seat. A row whose owner has left is sent to every seat without its listed fields.
TeamsServer code may push onto a masks list, such as team/red. Each packet reads the lists as they are when it is made.

In the browser a field the seat was not sent is not a key of the row's data, so an actor that reads one tests for it with in.

Keys, mice and gamepads

Keyboard keys, mouse buttons and gamepad buttons are all keys, named alike in every table.

KeyName
KeyboardNamed by its place on the keyboard: what it types on a US keyboard without Shift, such as a, 1, ' ', -, ArrowLeft, Escape, Enter, Tab, F1. Shift does not change the name: w is held from its press as W to its release as w. A key on another layout is named by where it is, so w a s d are the same four keys under the left hand on every keyboard. The modifiers are Shift, Control, Alt and Meta, either side. A key with no place, as an on-screen keyboard's, is named by what it typed, a letter in lower case.
MouseMouse0 the left button, Mouse1 the middle, Mouse2 the right.
Gamepad buttonsThe browser's standard mapping: Pad0 the bottom face button (A on an Xbox pad, cross on a PlayStation's), Pad1 right, Pad2 left, Pad3 top, Pad4 and Pad5 the left and right shoulders, Pad6 and Pad7 the triggers, Pad8 select, Pad9 start, Pad10 and Pad11 the sticks pressed in, Pad12 to Pad15 the d-pad's up, down, left and right, Pad16 the home button. A button is down while the browser calls it pressed. A gamepad the browser does not map to the standard numbers its buttons its own way.
Left stickPadStickLeft, PadStickRight, PadStickUp and PadStickDown, held while the stick is pushed that way past the deadzone.
Right stickPadRightStickLeft, PadRightStickRight, PadRightStickUp and PadRightStickDown, the same way. It is the standard mapping's third and fourth axes, so a gamepad with only two axes has none.

Every connected gamepad is read each frame. A gamepad key goes down the frame it is first seen down on any gamepad, comes up the frame none has it down, and has no repeat. It works in every part of a table that takes a key, so stick keys of [['ArrowLeft', 'a', 'Pad14', 'PadStickLeft'], …] move a seat from the arrows, the letters, the d-pad or the stick.

A gamepad key that went down while a modal had the input, or that was already down the first time the client read the gamepads, is held only on a new press. So the button that pressed a menu's button to change scene presses nothing in the scene it opens, as a keyboard key held while a page opens sends nothing.

The scene's ui is asked first. A click or mouse button going down over something on the UI that takes the pointer is the UI's: it is not held and asks nothing of the table. Every key is the UI's while a modal has the input, except the back key of world.ui on a screen the game put up. Each frame a modal has the input, every held key and button is let go as its release would, so an axis sends 0 and the seat stops, and a pointer lock this plugin took is given back. A key that goes down in an input, text area, select or anything editable on the page is the field's, so typing in a panel beside the game plays nothing.

A key or mouse button the table names has its browser default cancelled: Tab does not move the focus, the right button opens no menu and the wheel does not scroll. With Control, Command or Alt down the default is kept, the wheel too, so the browser's shortcuts and zoom still work; the game still hears the key.

The table bind takes

Every part is optional. Input is [act, ...args], an act of the player row sent up the wire as a message, or undefined to send nothing.

PartMeaning
press{ key: Input | function }. The message is sent the moment the key goes down. Given a function, the function runs then and nothing is sent, for what is the page's alone, such as a key of the store.
release{ key: function }. Run when the key comes up, or when the page lets go of every key. A key named here is pressed once per hold, the keyboard's repeats of it doing nothing, as a synth's note is.
axis[{ keys: [negative, positive], send(v), analog? }]. v is -1, 0 or 1, sent when it changes. Each side may be a list of keys, any of which holds it.
stick[{ keys: [left, right, up, down], send(x, y), analog? }]. x and y are each -1, 0 or 1, with y growing down, sent as one message when either changes, so a rule hears a direction at once rather than two halves of it.
hold[{ keys: [key, …], send(v), analog? }]. A button of one or more keys, held while any of them is down. v is 1 held and 0 let go, sent when it changes.
every(held) => Input. Asked each frame for a message. held(key) says whether a key is held.
click(x, y) => Input. Asked on a click, with its position in canvas pixels from the top left: page pixels divided by the renderer's scale, which is more than 1 when render2d's fit draws a canvas pixel across several.
pointer(dx, dy) => Input. A click on the page takes the pointer lock for the element clicked. While the page holds it, the mouse's movement is added up and this is asked once each frame it moved, with the page pixels it moved across and down.
drag{ key, send(dx, dy) }. The mouse's movement while the key is down and the page holds no pointer lock, in page pixels, asked once each frame it moved.
hover(at) => Input. Asked once each frame the mouse moved without the pointer lock. at is [x, y] in canvas pixels, or null the frame the pointer went over something on the UI that takes it, or off the page. Use it to draw what the pointer is over, such as a cell a tool would build on.
stroke{ key, start, move, end, cancel }. A press of key, a mouse button, that is not the UI's begins a stroke. start(x, y) is asked where it went down, move(x, y) once each frame the mouse moved while it is down, and end(x, y) where it came up, over the UI or not. cancel() is asked when the plugin lets the stroke go without a release, as a modal does. Points are in canvas pixels. Each is left out to ask nothing.
wheel(dy) => Input. Asked once each frame the wheel turned, with the pixels it turned down since the last one.
lockedA list of keys the game hears only while the page holds the pointer lock, pressed under it. Without the lock they are not held and press hears nothing of them. A key already down when a click takes the lock is not held until pressed again. A strike on the left button and a block on the right are the game's while the mouse looks, and a click without the lock only takes it. The frame the lock is lost, every key held is let go.

Analog

An axis, stick or hold with analog: true sends how far each key is held, from 0 to 1, instead of only whether it is held. On control the same is asked by naming the action in the table's analog list.

KeyHow far it is held
Keyboard key, mouse button1 while it is down.
Gamepad buttonThe browser's value for it, so a trigger runs from where the browser first calls it pressed to 1 at the floor.
Side of a stickHow far past the deadzone it is pushed, 0 at the deadzone and 1 at the rim.

A side of an axis or a stick is the most any of its keys holds, and the value sent is the positive side's less the negative side's, so an axis sends from -1 to 1. A hold sends the most any of its keys holds, and 1 while it is toggled on. Each value is rounded to a whole number of steps of world.player.resolution, a half rounded away from 0. A stick swept slowly sends one message each time it crosses a step, and a stick held still sends nothing however its reading wanders inside a step.

The function of an axis, stick or hold may return undefined to send nothing, as one that keeps the right stick for a camera only the page moves does.

Actions

With bind the game binds what a seat does straight to keys. With control it binds it to the scene's actions, and the scene binds the keys to the actions, so a player can change which keys play which action and the game never knows. Each action in world.player.actions has a shape given by its keys.

ShapeIts keys
buttonOne list of keys, held while any of them is down.
axisTwo lists: the keys that push it to -1 and the ones that push it to 1.
stickFour lists: left, right, up and down.

The table control takes is the bind table keyed by action instead of by key. press, release and hold name button actions, axis names axes and stick names sticks. every is handed held(action), which says whether a button action is held. locked lists button actions. drag and stroke take action in place of key, a button action whose keys they follow. click, pointer, hover and wheel are as in bind. analog lists the axis, stick and hold actions read analog. It throws on an action the scene does not name, on one whose keys are not the shape the part naming it reads, and on an analog action that no axis, stick or hold of the table names.

On this avenue a gamepad key going down is offered to the UI first, so a pad moves through the UI's menus. What the player chose is the page's, never the server's, and is kept in four keys of store. A scene that lists no store plays the scene's actions and settings and changes none.

Store keyMeaning
player.keysThe actions rebound, each to keys of the shape the scene gives it. An action it leaves out has the scene's keys.
player.toggleThe button actions played as toggles: a press of one of its keys holds it and the next lets it go, so a hold sends the 1 and the 0 a held key sends. A modal, or a pointer lock lost, lets a toggle go as it lets a key go.
player.deadzoneIn place of world.player.deadzone, read on bind as well.
player.repeatIn place of world.player.repeat. Above 0, the keyboard's own repeats press nothing and the action is pressed again every that many seconds, the first that long after it went down, from a gamepad as from a key. At 0 a keyboard's repeat presses it again, as bind's does. An action with a release is pressed once per hold either way.

Methods

On the page

Through plugins.player. One per client, and its table and listeners belong to the scene it was made for: they go when the page closes that scene's client, so a key pressed after the seat moved on cannot reach the rows of the scene it left.

(any other name)( ...args )

Any function called on plugins.player that is not listed below sends a message up the wire, naming that act and carrying the args, at this seat's player row. It is sent the moment it is called, so a key that goes down between frames is in the truth's inbox before the next step. A call made before the plugin has its wire, as an actor's setup makes on a client's first frame, is sent at its first frame.

Nothing runs here. On the page that runs the truth in its tab, the truth takes the message on its next step. On a page that runs ahead of its server (predict), the copy takes it on its next step and the seat's own player row is drawn from the copy the frame after the key, while every other row is drawn as the server's packets bring it. A message the connection refuses is a press that never happened.

bind( table )

Binds keys to messages with a table as above, and starts listening on the page. The table in force is the last one bound, whether from bind or control, each replacing the other.

control( table )

Binds the scene's actions to messages, with a table keyed by action.

unbind( )

Drops the table and its listeners, lets go of everything held and gives back a pointer lock this plugin took.

actions( ) : Shown[]

Each action the scene names, in its order, for a screen that shows and changes them. Each is { name, label, shape, keys, defaults, bound }: the shape (button, axis or stick), the keys it has now, the scene's keys, and the parts of the table in force that name it.

listen( fn )

The next key, mouse button or gamepad button that goes down is handed to fn and to nothing else, neither the game nor the UI. listening is true until then. Escape hands fn undefined, as does unbind. A click after a mouse button it took is nobody's.

rebind( action, keys )

Stores new keys for the action in player.keys. They must have the shape the scene gives the action.

restore( action? )

Puts the action's keys and its toggle back to the scene's. With no action, every action's, and the deadzone and the repeat.

conflicts( ) : { key, actions }[]

Each key more than one action holds, with the actions holding it.

settings( ) : { toggle, deadzone, repeat }

The toggles (a list of button actions), the deadzone and the repeat in force.

configure( changes )

Writes any of toggle, deadzone and repeat to the store. A toggle must be a button action, a deadzone above 0 and below 1, and a repeat 0 or more seconds.

device( ) : string

What was used last: keyboard (a key or a mouse button), xbox or playstation by the gamepad's name, or pad for any other gamepad. A gamepad counts only under control.

rumble( { strong, weak, seconds } )

Shakes the gamepad that last had a key go down, a button pressed or a side of a stick pushed past the deadzone, for seconds. strong drives its heavy motor and weak its light one, each 0 to 1, and a new rumble replaces one still playing. Nothing shakes when a keyboard key or mouse button went down after it, when no gamepad has been used, or when the browser gives the gamepad no motors, as Firefox gives none. It sends no message, so no other seat and no rule hears it. Throws when strong or weak is outside 0 to 1 or seconds is not above 0.

pads( ) : Pad[]

Every connected gamepad as the last read of the gamepads found it, in the browser's order, for a screen that shows what the plugin hears. Each is { index, id, family, mapping, used, buttons, axes }: its place in the browser's list, its name, xbox, playstation or pad as device names it, standard or '' for one the browser does not map, whether it is the one rumble would shake, each button's pressed and value, and each axis from -1 to 1 before any deadzone. Each is a copy. Empty before the first read or with no gamepad connected.

held( ) : string[]

Every key held now, by its name in a table (ArrowUp, Mouse0, Pad7, PadStickLeft), in the order they went down. A key a modal let go of, or a locked key without the pointer lock, is not in it.

holding( rows ) : Row | undefined

Imported from #plugins/player/player.plugin.ts. The row this page's player row holds, among the rows it was sent, or undefined while it holds none. Use it to choose which keys to bind and what the camera follows.

possessed( rows ) : Row | undefined

The player row this page plays as, the one that is mine. Also exported by #addons/player/player.addon.ts.

keyOf( event ) : string

The name every table gives the key a keyboard event is from. The key that comes up is the one that went down, whatever Shift did in between. Also the name world.ui uses.

keyName( code ) : string

The name of the key whose browser code is given: KeyA is a, Digit1 and Numpad1 are 1, Space is ' ', ShiftLeft is Shift, Minus is -.

keyCode( name ) : string

keyName run backwards: the code of the key a table names. It takes the main keyboard's key over the number pad's and the left of a pair of modifiers, and a letter in either case.

shapeOf( keys ) : 'button' | 'axis' | 'stick'

The shape of an action's keys: one list is a button, two an axis, four a stick. Throws on any other.

Rules

From #plugins/player/player.sim.ts. The player row stays the seat's thing, the body, the driver or the rider, and can take hold of other rows that stand in the world without it: a vehicle, a mount or a turret. The row it holds is its held field. A row is held by one seat at most, and a held row that ends lets go of its holder by being gone. A seat that leaves ends its player row and with it the hold.

Taking hold and letting go are the game's acts on its player row, which check what only the game knows, such as being in reach, slow enough or a door free, and then call these. A row nobody holds is the held kind's to rest or to drive itself: its rule asks holderOf each step.

take( me, w, row ) : boolean

Makes me hold row, and returns true. Returns false and changes nothing when me already holds a row, when row is a player row, or when another player row holds it.

letGo( me )

me holds nothing.

holding( w, me ) : Row | undefined

The row me holds, or undefined when it holds none or the row it held has ended.

holderOf( w, row ) : Row | undefined

The player row that holds row, or undefined.

relay( table ) : Record<string, Act>

Makes acts for the player kind that pass a press on to the row it holds. The table is { <held kind>: [act, …] }, each act one the held kind has. A press of one is w.call on the held row with the seat and args as they came, when the row held is of a kind that lists it, and nothing otherwise, so a press for a car while on foot is a press at the wrong moment and not an error. The held kind keeps those acts out of its open, so a seat reaches them only through the player row that holds it, and each checks its args as an act a seat sends does.

On the server

From #addons/player/player.addon.ts.

join( truth ) : number

Seats one more connection: spawns its player row, opens the seat on it and returns the seat number. In a scene with a security block the row is given own(id, masks). Throws when no slot is free.

free( truth ) : boolean

Whether one more connection finds a slot free, that is, whether join would seat it.

leave( truth, seat )

Ends the seat's player row and closes the seat.

own( id, masks ) : { owner, masks }

What a player row of that id holds of its own in a scene with a security block: owner is the id and masks is the list given plus me/<id>. join writes it, and so does anything else that spawns a player row.

mirror( truth, seats )

For a truth loaded from a snapshot, such as the copy a page runs ahead of its server. Gives each open seat that possesses a player row that row's slot, so the copy shows, hides and spawns as the server's truth does. A truth put back to a save (rewind) holds the seats the save held.

playtest( truth, think ) : { send, close }

Joins a seat the server drives itself. Each step think(rows) is handed the rows that seat sees and returns a press, [act, ...args], or undefined. The press is sent at the seat's player row as a hand's would be. On a step a server that rewinds runs again it thinks nothing, because the press it made on the first run is taken again. send(msg) puts a message in the seat's inbox, and close() leaves the seat. The page-side counterpart is playtester.

Used by

  • predict
    Runs the scene ahead of its server, so the seat's own player row answers a key on the next frame.
  • vision
    Reads each seat's team off its slot.
  • ui
    Is asked first about every key and click, so a key a modal took or a click on a button never reaches the game.

pancamera, scenemanager and simulator also use it: see pancamera, scenemanager and simulator. player itself needs store for rebound keys, rewind and vision.

In the guide