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
syncwithWITH CLIENT, because it would be waiting on every client at once, and a player’s server can’tsyncwithWITH PLAYERfor its own slot, because it would be waiting on itself. Both are errors. Leavesyncoff 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.