Distributing it

Going multiplayer

Playing with other people over a network: servers, seats, prediction and lag.

A game is played by more than one person when its manager runs on node behind a socket and each player opens client.html. Every game yarn new-game writes already has both files.

Seats

  1. In the scene file, games/<game>/<scene>.scene.json, raise "seats" to the number of players. A scene that lists the player plugin takes that many seats.
  2. Add one start actor per slot: { "is": "start", "slot": 1, "pos": [x, y] }. Each connection gets the lowest free slot, and its player row is spawned with that slot in seat.
  3. A rule that needs to know who is who reads the slot off the player row. A page that reconnects is a new seat and may get the same slot.

A connection that finds every slot of the entry scene taken is refused. A scene that does not list player, such as a menu, takes any number of seats. When a connection closes, its player row ends and its slot is free again.

Running it

  1. From the repository root, run node --experimental-strip-types games/<game>/<game>.server.ts. Run it from the root: the server reads the game's files by paths from there. Or, in yarn editor <game>, use the Node panel's Run on node, which restarts the server whenever a file it runs changes.
  2. The port is scenemanager.port in the manager scene file. yarn new-game gives each game its own, starting at 9001. Only one server can hold a port.
  3. Open http://localhost:5173/games/<game>/client.html once per player, each in its own window. Chrome pauses a tab that is not in front, so two tabs in one window do not both play.

index.html is the whole game in one tab with no server. A second tab of it is a second game, not a second player.

What each seat is sent

By default every seat is sent every row, and the other seats' player rows without their mine. Two pieces send less; each limits what a seat is sent, never what the rules know.

  1. Fields by owner. A top-level security block in the scene file names, for each kind, the group of each field it lists, and the player prefab gets a masks list. A listed field of an owned row is sent only to seats whose masks share the owner's group; me is the owner alone. See player.
  2. Fog of war. Give the scene's world.vision block, read by vision. Set slot to "seat", at to the field holding the place ("pos"), and sight to { "player": <cells> }. A row the seat's rules spawn for its team carries seat too. Kinds listed in always are outside the fog.
  3. To draw the fog, list the vision plugin before render2d in the scene's plugins and give world.render2d with the fog's layer among its layers. Things the fog covers are drawn on a layer of that world.

What a seat may send

  1. A seat may run an act on its own player row, or on a row whose kind lists the act in open. Anything else is refused.
  2. An act on the player row that other kinds call to change it, such as hurt or award, could be sent by the seat. Make it a plain function of a game plugin, or have the act return when seat >= 0; a hook passes -1.
  3. Every argument arrives from outside the program. Each act checks each argument's type, that a number is finite, and its range.

Running ahead of the server

  1. Give every scene a seat plays in a world.predict block: { "epsilon": e, "cutoff": c, "halflife": h, "snap": s, "below": b }. The page then runs its own copy of the scene with predict, and the server puts a late press back on its step with rewind. The predict page says what each number does.
  2. The seat's own thing and everything its press moves are drawn on the frame after the key. A sound raised from the seat's player row is heard then too.
  3. A title or menu scene gives no world.predict, and its presses are drawn a round trip later.

Chat and parties

chat and party need an account per seat (Persisting players) and draw on ui, listed after them in plugins. Chat with party or whisper talk needs chatbox: { channels: 'me' } in security; party needs partyseat: { account: 'me', invite: 'me' }.

Checking it

  1. games/<game>/<game>.server.test.ts serves the manager and dials pages: each page is sent every seat's player row, one page past the last seat is refused, and a closed seat frees its slot. Run it with yarn vitest run on that file.
  2. games/<game>/<game>.predict.test.ts presses on predicting pages over clean, home, bad wifi, mobile and awful wires, and checks that the seat's thing takes every press by the next frame with no corrections.
  3. Play bad networks on node with the simulator, or record a real browser session with client.html?tap=<name>.
  4. Play it: the server running and client.html in two windows, each thing moving on the frame after its key and each player seeing the other move.