How to make a multiplayer game with AI
A network carries messages, and that is all it carries. The moment two computers disagree, one of them has to be the one that decides, and that choice shapes the whole game.
Jump to a section
Ask for one host. One player's computer runs the game, every other computer sends what that player is trying to do, and the host sends back what actually happened. That one rule is the difference between a multiplayer game and two copies of a game that slowly stop agreeing.
Any network you use, including the rooms Agent Games hosts, carries messages and nothing else. It cannot tell you whose shot landed first, because it has never been told the rules. Something has to decide, it has to be one machine, and you pick which one before you write anything that moves.
This guide is about two people on two computers. If they are sitting next to each other, split the keyboard instead and skip all of it. The 2 player guide covers that version, and it is an evening rather than a project.
Four steps to a game two computers can play#
Movement, collision, scoring, all on one machine. A game that is dull alone does not improve by also being late.
It names the host, the two message shapes and the room code. A model left to choose will have every client run everything, which is the fault this whole article is about.
Two windows on one screen is enough to watch the host working. Move in one window and check the other agrees.
A second machine on mobile data has real delay and no shared clock. Everything the two windows hid turns up in about ten seconds.
Build a multiplayer game as ONE self-contained .html file, using the
arcade.room API the site injects into every game. No frameworks, no
server of my own, no npm, no third party socket library.
GAME: up to 8 round players shoving each other on a flat arena seen from
above. Falling off the edge costs you the round. A point for the last
one left, then the arena resets.
JOINING: two buttons on the title screen.
- "Play now" calls arcade.room.join() with no code, which puts the
player in a room of this game that has space.
- "Private room" makes up a six character code from A to Z and 0 to 9,
calls arcade.room.join(CODE), and shows that code in large text.
A text box lets a player type a code somebody read out to them. Show
arcade.room.code on screen the whole time the game is running.
ALONE: the game must be completely playable with one person in the room.
One player is the host, the arena works, the controls work, and a line
says the room code and that it is waiting for other people. Nobody will
ever have a friend ready at the moment they first open the file.
AUTHORITY: arcade.room.isHost is true for exactly one player and it is
the same player on every machine. The host owns the world. Only the host
moves anybody, resolves a shove, decides who fell off and keeps score.
MESSAGES, which is the part to get right:
- every client sends only intent, about 20 times a second:
arcade.room.send({ t: "in", up: true, left: false, right: false })
- the host, and nobody else, sends the world about 10 times a second:
arcade.room.send({ t: "world", players: [...], scores: {...} })
- a client that is not the host never moves another player, and never
trusts its own position over the one the host sent.
- your own broadcast does not come back to you, so the host has to
apply its own input locally as well as everybody else's.
DRAWING: each machine draws its own player straight from its own keys,
so pressing a key feels instant. When a world message arrives, ease that
player towards the position the host gave it over a few frames rather
than snapping to it. Other players are drawn only from the host's world.
COMING AND GOING: handle arcade.room.on("join", ...) and
arcade.room.on("leave", ...) so people can arrive and quit mid round. If
the host leaves, the roster reorders and somebody else becomes the host,
so read arcade.room.isHost every frame instead of once at the start.
Base all movement on seconds elapsed between frames, capped at 0.05.
Give the canvas CSS of canvas { touch-action: none; } so a phone does
not scroll the page under a thumb. Put every tunable number in one
CONFIG object at the top, including both send rates, with a comment
on each.
The two message shapes are the whole design. Everything else in the prompt is a game you could swap out, and those eight lines are what stops it becoming two separate games running at once.
Why two computers cannot both be right#
A first build nearly always has every client run the whole game and broadcast its own position. On one laptop with two windows that works perfectly. Messages arrive in under a millisecond, nothing is ever contested, and the design looks sound.
Then the other player is on a train. Their position reaches you late, so your screen is showing where they used to be. You both go for the same gap. Your machine saw you get there first and theirs saw the opposite, and both of them are telling the truth about what they were shown.
If every client reports its own position, anybody can edit one number and stand wherever they like. If the host owns positions, the worst a player can send is a key they are not really holding. That is your entire security model, and you choose it in the first hour without noticing.
The host is not a trusted server, and it should not pretend to be. Whoever hosts can still cheat in their own favour. For a game between friends that is fine, and the alternative is a server you have to write, pay for and keep running.
Send intent, not results#
An intent message says what a player is trying to do. A result message says what happened. Clients only ever send the first kind, the host only ever sends the second kind, and the two directions never swap.
// every client, about 20 times a second
arcade.room.send({ t: "in", up: held.ArrowUp, left: held.ArrowLeft });
// the host only, about 10 times a second
if (arcade.room.isHost) {
arcade.room.send({ t: "world", p: positions, s: scores });
}
arcade.room.on("message", (m, from) => {
if (m.t === "in" && arcade.room.isHost) intent[from.id] = m;
if (m.t === "world" && !arcade.room.isHost) applyWorld(m);
});
Intent also ages better than position. A position from 200 milliseconds ago is simply wrong. A key that was being held 200 milliseconds ago is probably still held, so the host can carry on using it until it hears different. That is why a hosted game degrades into mild rubberiness on a bad connection instead of falling apart.
| It looks like | What is actually happening | Ask for |
|---|---|---|
| Players slide back a step | The host corrected a position you had drawn already | A correction eased over several frames, never a snap |
| Two people both win the round | Both machines scored it on their own copy | Only the host scores, and it sends the result out |
| Your own player feels heavy | You are waiting for the host before drawing yourself | Local input drawn at once, corrected when state lands |
| Everything stops when one person quits | The game is waiting for a player who has gone | A leave handler that drops them and carries on |
| It breaks when the first player leaves | The host was worked out once, at the start | isHost read every frame from the live roster |
Describe what you are seeing rather than guessing at the cause. Every one of these has the same two or three lines behind it.
Lag is not a bug you get to fix#
You cannot delete the distance between two houses. Light takes its time and a phone connection adds plenty more. The job is not removing the delay. It is keeping the delay off the one thing the player is actually watching, which is their own character.
- Draw yourself immediately. Apply your own input the moment the key goes down, then let the host correct you. Waiting for the round trip makes the controls feel broken even on a fast connection.
- Correct gently. When the host disagrees with where you drew yourself, move towards its answer over several frames. A small steady correction reads as nothing at all. A snap reads as a bug in your game.
- Send on a clock, not on every key press. Twenty intent messages a second is plenty, and it costs the same whether the player is standing still or sprinting across the map.
- Let the host update slower than the frame rate. Ten world messages a second is enough when clients draw in between them. A world every frame spends the budget on detail nobody can see.
- Design so delay does not decide the match. A game where a hit is settled in a single frame always punishes the slower connection. Shoving, territory and stacking all survive it.
Rooms, codes and what the site hosts#
A game published on Agent Games can put players together without a server of its own. The site holds the room, keeps the roster in order and carries messages between the people in it. It never reads them, because it does not know the rules of your game and never will.
arcade.room.join(); // a room with space, or join("ABC123")
arcade.room.on("open", (r) => {}); // r.self, r.room, r.players
arcade.room.on("join", (who, all) => {});
arcade.room.on("leave", (who, all) => {});
arcade.room.on("message", (data, from) => {}); // from.id, from.name
arcade.room.on("close", (reason) => {});
arcade.room.on("error", (message) => {});
arcade.room.send({ hold: "left" }); // everybody except you
arcade.room.send(world, someone.id); // one player
arcade.room.players; // [{id, name}] in join order
arcade.room.self; // { id, name }
arcade.room.code; // the room you are in
arcade.room.isHost; // true for players[0], and only for them
- players[0] is the same person for everyone. The roster is ordered by when each player arrived, and every client sorts it the same way. That is what makes isHost safe to build on.
- Your own broadcast does not come back to you. A message goes to everybody except the sender, so the host has to apply its own input locally alongside everyone else's.
- The sender is stamped by the site. Every message arrives naming the player it came from, taken from the connection rather than the contents, so one game cannot speak as another player.
- Say it plays with other people when you publish. Rooms are refused for a game that was not published as a multiplayer one, and the box is on the upload form.
The limits exist to stop a runaway game rather than to shape your design. A room holds up to 32 players. Each player may send 60 messages a second, and a single message may be up to 8KB. A room nobody is in is dropped after a few minutes, so a reload still finds it.
Calling join with no argument drops the player into a room of your game that has space, which is the right default once people are playing. On the first evening nobody is playing. Show the room code in large text, let a friend type it in, and test with somebody you can phone.
🌐Multiplayer games written by AI
Common questions#
Can AI make a multiplayer game?
Yes, if you tell it who is in charge. Say in the prompt that one player is the host, that everybody else sends only the keys they are holding, and that the host sends the world back. Left to decide for itself, a model will have every client run the whole game, which works on your laptop and nowhere else.
Why do the two screens slowly drift apart?
Both machines are running the game and neither is correcting the other. Small differences in timing compound until the two copies describe different matches. Pick one machine to be right, send intent to it, and draw what it sends back.
Do I need a server for an online game?
Not for a game published here. The site hosts the rooms, matches players and carries the messages, so a one file game needs nothing of its own. What the site cannot do is referee, because it does not know your rules. Your game still has to choose a host.
How do two people on different computers find each other?
Either the site puts them in a room of your game that has space, or one of them reads a room code out and the other types it in. For a first build, do the code. Matchmaking only helps once there are strangers to match you with.
How many players should a room hold?
Fewer than you think. The ceiling is 32, but a host sending the whole world to everybody gets expensive quickly, and a shoving match stops being readable long before that. Four to eight is a sensible first target.
Why does my own character feel sluggish online?
It is waiting for the host before it moves. Draw your own player from your own keys the instant they go down, then ease it towards whatever the host says a few frames later. Every other player can safely be drawn from the host alone.
Can players cheat in a hosted game?
The host can, and it is worth knowing that before you invite strangers. Everybody else can only lie about the keys they are pressing, which is a much smaller problem. A game you cannot cheat at needs a server that trusts nobody, which is a different project.
Where to go next#
When two people have played it without either of them saying the word lag, publish it. It gets a page recording the model, the prompt and the tokens behind it, and a friend can join from a link without installing anything.
For the method behind all of it, read how to make a game with AI. For the version two people play on one keyboard, read the 2 player guide. Prompts for common faults live in the prompt guide. The full room reference is in the agent docs, and the multiplayer games here are worth playing with somebody first.
Built something? Put it on Agent Games.
Publishing takes a file and a minute. Your game gets its own page listing the model, the prompt and the token count, and anyone can play it without signing up.
Read next