Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Playing together

Games are better with friends. CrashNet is my multiplayer: a server runs the game, and everyone’s CrashPlayer connects to it. You still write one program. I run copies of it in different places, and each copy knows which one it is.

One program, four places

In a CrashNet game, your program runs as:

  • the host, on the server. There’s one, it has no screen, and it’s in charge of the shared world: the level, the enemies, the round.
  • a player’s server, also on the server, one for each player. It’s in charge of that player: their sprites, their score.
  • a client, in each player’s own CrashPlayer. It draws, plays the sound, and hears the keys.

The fourth place is on its own, a single-player game, like every game in this book so far. gamecontext says which of the four a copy is, as "HOST", "PLAYER", "CLIENT" or "SINGLE", and me is the player it runs for.

SCREEN 320, 240
PRINT "I'm running as", gamecontext
PRINT "for", me.name, "in slot", me.slot
PRINT "host?", me.ishost, " server?", me.isserver, " client?", me.isclient
PRINT "online?", crashnet.connected
I'm running as SINGLE
for Player in slot 1
host? 0  server? 0  client? 1
online? 0

On my own, I’m a one-player client called Player in slot 1, so a game written for CrashNet runs alone with no changes.

Saying what’s shared

A crashnet.json file in the game’s folder, beside main.crash, sets up CrashNet. Everyone reads it before the game starts.

{
  "maxPlayers": 4,
  "tickRate": 20,
  "playerData": { "score": "INTEGER", "team": "STRING" },
  "hostData": { "round": "INTEGER" }
}

playerData is what every player has, and hostData is what the game as a whole has. A field is INTEGER, FLOAT, STRING or BOOLEAN. tickRate is how many times a second the changes go out to everyone.

Everyone’s records

me is this player’s record, players(n) is the player in slot n, and players.count is how many there are. host is the game’s record. Change a field and it goes out to everyone.

SCREEN 320, 240
me.score = 10
me.score = me.score + 5
host.round = 1
PRINT me.name, "has", me.score, "in round", host.round
PRINT players.count, "playing; slot 1 is", players(1).name
Player has 15 in round 1
1 playing; slot 1 is Player

Not everyone may change everything. A player’s server changes its own player’s fields, and the host may change anyone’s, and its own. A client only reads them. Playing alone, I let you change any field you like.

Who’s here?

CALL PLAYER calls a function whenever a player joins or leaves, with that player’s record: their slot, their name, and joined, which is 1 when they’ve come and 0 when they’ve gone. CALL HOST calls a function when this copy becomes the host, at the start or when the host leaves and someone takes over. STOP PLAYER and STOP HOST stop them by name.

A guard says which copies a function is for: here, only the host and a single-player game hear about players.

SCREEN 320, 240
FUNCTION roster(player)
    IF player.joined
        PRINT player.name, "joined in slot", player.slot
    ELSE
        PRINT player.name, "left"
    END IF
END FUNCTION
FUNCTION promoted()
    PRINT "I'm the host now"
END FUNCTION

CALL PLAYER roster, "roster" !! guard(["host", "single"])
CALL HOST promoted, "promoted"
PRINT "waiting in the lobby with", players.count, "player"
// ... and when the game starts:
STOP PLAYER "roster"
STOP HOST "promoted"
waiting in the lobby with 1 player

Playing alone, nobody comes or goes, so neither function is ever called.

Asking the server

A client can’t change the world. It asks its player’s server, with an EVENT and !! target("server"). The server has a CALL EVENT for it, does what was asked, and its changes go out to everyone. target("host") asks the host instead.

SCREEN 320, 240
ATOMIC jumps = 0
ATOMIC climbed = 0
FUNCTION jump(height)
    climbed = climbed + height
    jumps = jumps + 1
END FUNCTION
CALL EVENT jump, "jump" !! guard(["player", "single"])

EVENT "jump" !! target("server") args([20])
EVENT "jump" !! target("server") args([35])
DO WHILE jumps < 2
    IF COOLDOWN(1) THEN SYNC
LOOP
PRINT jumps, "jumps,", climbed, "high in all"
2 jumps, 55 high in all

Playing alone, every target is here, so the event lands in this same copy.

Two events can be handled at the same time, so the second jump can start before the first is done. That’s why jump only adds to ATOMIC variables and the program prints once both are in: two PRINTs in jump could come out in either order.

SYNC marks the end of a tick’s worth of changes, so they go out to everyone together.

Running code over there

WITH CLIENT runs one of your functions on the clients: from the host, on every one, and from a player’s server, on that player’s own. WITH PLAYER runs one on a player’s server. !! args(...) is what it’s called with, and !! sync waits for it to finish there.

SCREEN 320, 240
FUNCTION banner(text)
    LOCATE 2, 2
    PRINT text
END FUNCTION
FUNCTION award(points)
    me.score = points
END FUNCTION

WITH PLAYER 1, award !! args(50) sync
WITH CLIENT banner !! args("Player 1 wins with " + str(me.score) + "!")
PAUSE

Think of that as the host’s code. It waits for player 1’s server to give out the points, then has every client show who won. That’s how the host gets something drawn: a server has no screen, so a PRINT or a FRAME on the server is an error. It goes the other way for files: the client can’t save, so it asks a server to. Playing alone, both run right here.

Crash-tan’s Tip: The host can’t sync with WITH CLIENT, because it would be waiting on every client at once, and a player’s server can’t sync with WITH PLAYER for its own slot, because it would be waiting on itself. Both are errors. Leave sync off and they go without waiting.

Rooms

A big game is split into rooms, and a player only sees the room they’re in. What’s made inside a WITH ROOM block belongs to that room, and SET ROOM moves this player into one (SET ROOM "" leaves them all). Anything made outside every room is seen everywhere: the score, say.

SCREEN 320, 240
LOADIMG "ball.png", "ball"
WITH ROOM "cave"
    bat = reserve_sprite()
    SET SPRITE bat !! image("ball") x(40) y(40) scale((0.25, 0.25))
    SHOW SPRITE bat
END WITH ROOM
SET ROOM "cave"
PRINT me.name, "is in the", me.room
Player is in the cave

The whole thing

Here’s a game for several players: everyone gets a ball, and the arrow keys move yours. It runs alone too, as one player. Click the screen and steer.

SCREEN 320, 240
LOADIMG "ball.png", "ball"
ATOMIC mine = -1

// A player's server makes that player's ball, and shares it with everyone.
FUNCTION make_ball()
    mine = reserve_sprite()
    SET SPRITE mine !! image("ball") x(40 * me.slot) y(100)
        !! scale((0.25, 0.25)) sync
    SHOW SPRITE mine
END FUNCTION

// ...and moves it when its client asks.
FUNCTION move(dx, dy)
    IF mine < 0 THEN RETURN
    SET SPRITE mine !! x(clamp(spritex(mine) + dx, 0, 288))
        !! y(clamp(spritey(mine) + dy, 0, 208))
END FUNCTION
CALL EVENT move, "move" !! guard(["player", "single"])

IF gamecontext = "PLAYER" OR gamecontext = "SINGLE" THEN make_ball()

DO
    IF COOLDOWN(1)
        IF gamecontext = "CLIENT" OR gamecontext = "SINGLE"
            dx = 0
            dy = 0
            IF keydown("Left") THEN dx = -3
            IF keydown("Right") THEN dx = 3
            IF keydown("Up") THEN dy = -3
            IF keydown("Down") THEN dy = 3
            IF dx <> 0 OR dy <> 0 THEN EVENT "move" !! target("server") args([dx, dy])
        END IF
        SYNC
    END IF
LOOP

Every copy loads the image: the servers need to know how big it is, even though they never draw it. A sprite a server makes is shared with the clients when it’s set with !! sync. A sprite a client makes is its own, and nobody else sees it, which is just right for a menu or a sparkle.

Trying it out

CrashNet games run on a crashnet-server. To try one on your own computer, start a server in simulation mode, which lets anyone in, then point a CrashPlayer at it for each player:

crashnet-server -s my_game
crashplayer --remote ws://localhost:9005 --name "Ada"
crashplayer --remote ws://localhost:9005 --name "Bob"

Crash-tan’s Tip: Send the server what a player did (“jump”, “fire”, “buy the sword”) instead of every key they press. It’s far less to send, and the server stays the one that decides what happened.