Talking to the server
So far your mod has run entirely in one browser. To make a multiplayer mod, each player's client sends what it's doing to a shared server, and the server relays it to everyone else. This chapter covers the client half: sending.
The two halves, one file
Remember that a .munos file holds both sides. The client gathers data and sends it; the server receives and re-broadcasts it. They communicate through two primitives that are built into the language:
send(data)— a builtin that ships bytes to the other side.event receive(data)— an event that fires when bytes arrive.
No sockets, no URLs, no connection code. The channel is implicit.
Sending bytes
From the client, send takes a byte array (u8[]):
client {
event frame(frame_num: i32) {
var x = read_u8(0x0086)
var y = read_u8(0x00CE)
send([x, y]) // ship two bytes to the server
}
}That already works — but sending raw [x, y] is wasteful and inflexible. The idiomatic way to build a message is to pack it.
Packing a message
Real updates have several fields of different sizes: a position, a small pose ID, a one-bit "facing" flag. pack lets you lay them out to the bit:
pack(values, widths) // -> u8[]You give it a list of integer values and a matching list of bit widths. Each value is written using exactly that many bits:
// player_x and player_y as 8 bits each, a 4-bit sprite id,
// and a 2-bit palette id — 22 bits, packed into 3 bytes.
var msg: u8[] = pack(
[player_x, player_y, sprite_id, palette_id],
[8, 8, 4, 2]
)
send(msg)A one-bit flag costs exactly one bit on the wire. This matters when you're sending 60 updates a second.
Reliable vs. unreliable
send has an optional second argument:
send(msg, reliable = true) // default: guaranteed, in order
send(msg, reliable = false) // fire-and-forget: may drop, may reorder- Reliable (the default) is for things that must not be lost: a chat message, a "player joined" announcement, one-time setup.
- Unreliable is for high-frequency streams where the next update is coming right behind this one. Position updates at 60 Hz are the textbook case — if one drops, the next frame fixes it, and you don't want the network stalling to redeliver stale coordinates.
So a position broadcaster wants send(msg, reliable = false).
Receiving on the client
The server will send updates back — the positions of every other player. Those arrive in event receive:
client {
event receive(data: u8[]) {
var vals = unpack(data, [8, 8, 8]) // same widths the sender used
var slot = vals[0]
var x = vals[1]
var y = vals[2]
// ...store and draw the other player...
}
}unpack(bytes, widths) is the mirror of pack: same widths, in the same order, and you get your values back. The two width-lists — one on each side of the wire — are your protocol. Because both halves are in one file, they can't disagree.
Widths must match
pack and unpack share no schema beyond the widths array you pass each. If the sender packs [8, 8, 8] and the receiver unpacks [8, 8, 16], you'll get garbage. Keep the widths together — many scripts define them once as a shared const array and use it on both sides.
A complete client
Here's a client that broadcasts its position every frame and remembers where everyone else is:
const PLAYER_X: u32 = 0x0086
const PLAYER_Y: u32 = 0x00CE
client {
var otherX: u8[64]
var otherY: u8[64]
var occupied: bool[64]
event frame(frame_num: i32) {
// 1. tell the server where we are
var x = read_u8(PLAYER_X)
var y = read_u8(PLAYER_Y)
send(pack([i32(x), i32(y)], [8, 8]), reliable = false)
// 2. draw everyone we've heard about
for (var i = 0; i < 64; i = i + 1) {
if occupied[i] {
draw_image(ghostSprite, ghostPal, i32(otherX[i]), i32(otherY[i]))
}
}
}
event receive(data: u8[]) {
var vals = unpack(data, [8, 8, 8])
var slot = vals[0]
otherX[slot] = u8(vals[1])
otherY[slot] = u8(vals[2])
occupied[slot] = true
}
}Each other player has a slot — a small index the server assigns them. The client keeps parallel arrays (otherX, otherY, occupied) keyed by that slot. We'll see where slots come from next.
What you learned
send(data)ships bytes;event receive(data)receives them.pack(values, widths)/unpack(bytes, widths)build and parse messages to the bit; the widths arrays are your wire format.reliable = falsesuits high-frequency position streams.- The client keeps other players in parallel arrays indexed by slot.
Next
The client is sending into the void — there's no server yet to relay anything. Let's write the other half.