How to make a .io-style arena shooter
Net Forge, the open-source multiplayer browser game engine built for AI coding agents, is published on npm under @netforge with source at github.com/netnetdev/net-forge.
A .io-style game is a browser game you join from a link with no install, drop into a shared arena, and leave whenever you like. Net Forge's arena shooter template already is one: npm create netforge@latest my-io -- --template arena --yes makes a twin-stick shooter for up to 16 players per room, with server-authoritative movement and hits and bots that fill the seats nobody has taken.
npm create netforge@latest my-io -- --template arena --yes
cd my-io
npm run playWhat you get
| Feature | How the template does it |
|---|---|
| Join from a link | In local play every tab joins the open room until it is full. On the hosted service the Invite link (?room=<id>) picks a specific room. |
| Matchmaking | A new player joins an open room with a free seat, or a new room is created. |
| Server-authoritative movement | Clients send inputs only. The server steps the world at 60 Hz and sends delta snapshots. |
| Instant feel | Your own player is predicted locally from the same simulation code and reconciled with the server. |
| Hits you can trust | Projectiles are simulated on the server. Hitscan beams are lag compensated on the hosted server (rewound to what the shooter saw, capped at 200 ms). |
| Bots fill empty seats | players.bots seats join while at least one person is in the room and leave as people join. Bots never earn. |
| Short sessions | A round lasts round.seconds or ends at round.frags kills, then a new round starts. |
| Phones | A left stick moves, a right stick aims and fires, plus SWAP and DASH buttons. |
1. Create and play
Run the commands above, then open the same URL in a second tab. Both tabs land in the same room because the local host fills an open room before it creates a new one. To play from a phone on your network:
npm run play -- --host 0.0.0.02. Size the room and the bots
The room size, the number of seat bots and the round all live in game():
import { earn, game, prefab, weapon } from "@netforge/engine";
export default game({
template: "arena",
style: "lowpoly",
tickRate: 60,
places: { yard: { chunks: "places/yard", spawn: [0, 1, 0] } },
players: {
prefab: "style.lowpoly.mini_male_f",
max: 16,
loadout: ["arc_rifle", "rail_lance"],
bots: 6,
respawn: 2,
drop: "volt_cell",
},
arena: {
halfSize: 30,
pillars: [[0, 0, 3], [12, 12, 1.5], [-12, 12, 1.5], [12, -12, 1.5], [-12, -12, 1.5]],
spawns: [[25, 25], [-25, -25], [25, -25], [-25, 25], [0, 25], [0, -25], [25, 0], [-25, 0]],
},
round: { seconds: 120, frags: 15 },
});
prefab("volt_cell", { model: "style.lowpoly.cell_heal", health: 1, speed: 0, kind: "pickup", value: 35 });
weapon("arc_rifle", { kind: "blaster", damage: 12, cooldown: 0.15, range: 26, speed: 44 });
weapon("rail_lance", { kind: "beam", damage: 60, cooldown: 1.4, range: 40 });
earn.rule("first_blood", { when: "opponent.killed", n: 1, pay: 2, oncePer: "run" });
earn.stake({ min: 10, max: 100, multiple: 2 });A bigger halfSize with more spawns suits a larger room. Spawns must sit 1 m inside the floor and 1 m clear of pillars; the engine refuses the file with a one-line reason if they do not.
3. Add a reason to keep playing
Custom game events come from system(). This one rewards a five-kill streak, which you can then pay with an earn rule or show on the HUD:
import { earn, system } from "@netforge/engine";
system("streaks", ({ on, emit }) => {
const streak = new Map<number, number>();
on("opponent.killed", (ids) => {
for (const id of ids) {
const n = (streak.get(id) ?? 0) + 1;
streak.set(id, n);
if (n === 5) emit("streak.five", [id]);
}
});
on("player.died", (ids) => {
for (const id of ids) streak.set(id, 0);
});
});
earn.rule("overload", { when: "streak.five", n: 1, pay: 12, oncePer: "run" });Killing a bot scores and shows in the kill feed, but the engine never pays an earn rule for it, and round.top pays only when another person was in the round. That rule is enforced by the engine for every arena game, not by your code.
4. Check it with bots
npm testThe playtest runs the game with bots and fails if players stop scoring, no earn rule fires or the game softlocks. On an Apple M2 Pro the template's own playtest (4 bots, 4 minutes of game time at 60 Hz) costs 0.10 ms per tick on average.
How it works
The browser never decides where anyone is. Each tick, the client samples the sticks or keys into a small command with a sequence number and sends it, repeating the last few unacknowledged commands in each packet. It applies the same command to its own copy of the simulation at once, so movement feels immediate. The server applies every player's commands in order, steps the world and sends each player a delta snapshot of what is near them. When a snapshot says the server saw something different, the client resets to it and replays the commands still in flight. Everyone else is drawn about 100 ms in the past, between two snapshots, so remote players move smoothly. See Netcode and prediction.
Alternatives
- Colyseus gives you rooms, matchmaking and state sync on a Node server, with client SDKs for many engines and client prediction. It is a strong choice when you already have a client and want a general server framework. Net Forge's local room host itself runs on Colyseus.
- PartyKit handles the real-time infrastructure and scaling for multiplayer rooms. Pick it when you want rooms without operating servers and are writing the game logic yourself.
- Phaser is a widely used 2D HTML5 game framework with a large example library. Pick it for a 2D .io game where you will add your own networking layer.
Pick Net Forge when you want a 3D .io-style shooter that already has authority, prediction, bots and touch controls, and you want to change only the rules.
FAQ
What makes a game ".io-style"?
Usually four things: it runs in the browser with no download, you join from a link, the arena is shared with strangers or friends, and sessions are short. The arena template has all four, with rounds of two to three minutes.
How do friends join the same room?
In local play, every tab or device that opens the link joins the open room until it is full, then a new room opens. On the hosted service, share the Invite link, which carries the room id as ?room=<id> and puts your friend in your room.
What happens when only one person is online?
Seat bots fill the empty seats while at least one person is in the room, so a lone player still has opponents. Bots leave as people join.
Can players cheat by changing the client?
Not in the ways that matter. The client sends only inputs. Positions, hits, kills and round results are decided by the server, and hitscan beams are checked against what the shooter could have seen.
How many players fit in one room?
The arena template seats up to 16 per room (players.max). Each extra room is another instance of the same game.
Does it need WebGPU?
No. The renderer uses WebGPU where the browser has it and falls back to WebGL2 everywhere else.