NET FORGE DOCS

How to make a survivor-like with thousands of enemies in the browser

Last updated Checked against @netforge/engine 0.2.0View as Markdown

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.

The EMBERWATCH template holds 2,000 enemies with four players at 1.3 to 1.5 ms of server time per tick on average (p99 under 4.5 ms), against a 33 ms budget at 30 Hz, measured on an Apple M2 Pro. Run npm create netforge@latest my-horde -- --template survivor --yes and you have that working survivor-like: auto-firing weapons, XP gems, level-up cards, bosses and a swarm that grows to 2,000 enemies, with up to four players in one room. You change the game by editing the declarations in src/game.ts; the crowd rendering, the spawn director, the netcode and the bots are already done.

npm create netforge@latest my-horde -- --template survivor --yes
cd my-horde
npm run play

The hard part of this genre in a browser is not the rules. It is moving and drawing thousands of enemies every frame while a server stays in charge of every hit. The template solves both, and the playtest bots measure it.

What you get

PartWhereNotes
Spawn directordirector({...}) in src/game.tsSpawns just off screen on the run's clock, with curves for living count, rate, health and damage.
Enemies and rolesprefab({...})chase, runner, charger, ranged, bomber, shielder, healer, summoner, splitters and elites. Big hits are telegraphed on the ground.
Weaponsweapon({...})nova, aura, bolt, orbit, cone, chain, meteor, boomerang, 5 to 8 levels each, evolutions paired with passives.
Crowd renderingtemplate clientInstanced three.js crowds with procedural vertex animation, GPU particles, one draw per crowd.
Server costmeasured 2026-10-242,000 enemies with 4 bots: 1.32 to 1.49 ms per tick on average, p99 2.63 to 4.41 ms, over three 30-second runs; with every enemy role in the crowd, 1.70 to 2.14 ms, p99 up to 7.05 ms. The budget is 33 ms at 30 Hz (Apple M2 Pro, Node 22, engine.test.run).
Playersplayers.max4 per room, co-op revive, shared XP for teammates nearby.

1. Start the template

npm create netforge@latest my-horde -- --template survivor --yes
cd my-horde
npm run play -- --players 2

Two tabs open on http://127.0.0.1:4663, both in the same room. WASD or the arrows move, Space dashes, 1 2 3 pick a level-up card. The second tab is a second player against the same server.

2. Make the swarm bigger, later

The director decides how many enemies are alive at each minute. Curves are a number or [seconds, value] keyframes. Keep the opening quiet and push the late game:

src/game.ts
import { director } from "@netforge/engine";

director({
  start: 8,
  alive: [[0, 12], [60, 45], [180, 160], [300, 400], [540, 2000]],
  rate: [[0, 0.8], [60, 2], [180, 6], [300, 14], [540, 70]],
  health: [[0, 1], [60, 1.4], [180, 2.4], [540, 5.7]],
  damage: [[0, 1], [180, 1.45], [540, 2.3]],
  pool: [
    { prefab: "shade", weight: 10 },
    { prefab: "gnat", from: 45, weight: 2, group: [8, 12] },
  ],
});

More players raise alive and rate by 0.6 each. At the cap, new spawns get tougher instead of adding bodies, and enemies left far behind are moved back ahead of the players.

3. Add an enemy with a role

src/game.ts
import { prefab } from "@netforge/engine";

prefab("wisp", { model: "style.lowpoly.bat", health: 8, speed: 5, radius: 0.25, damage: 4, ai: "runner", drops: ["ember_small"] });
prefab("brood", { model: "style.lowpoly.slime_green", health: 40, speed: 3, split: { into: "shade", count: 3 }, drops: ["ember_small"] });

Then add { prefab: "wisp", from: 150, weight: 3 } to the director's pool. A runner aims at where you will be; a splitter bursts into three smaller enemies. Keep chasers at 3 to 4 m/s: players run at 7.

4. Add a weapon that clears crowds

src/game.ts
import { weapon } from "@netforge/engine";

weapon("frost_ring", {
  kind: "nova",
  damage: 6,
  cooldown: 0.8,
  range: 5,
  knockback: 8,
  levels: [{ damage: "+30%" }, { range: "+15%" }, { cooldown: "-15%" }, { damage: "+30%" }, { knockback: 3 }],
});

The level-up cards offer it automatically and show what each level changes. Knockback is physics: it is divided by the enemy's mass, so a brute (mass 6) barely moves while fodder slides into the crowd behind it.

5. Measure it with the bots

npm test
npm run check

The playtest plays the game headless with bots and reports kills, waves, levels, earn rules fired, softlocks and the server cost per tick. It also scores design rules: a quiet first minute, no spawns on screen and no death before the first minute. On the stock template, four bots playing three minutes clear 1,350 enemies, reach level 9 and nobody goes down in the first minute.

How it works

The room server is authoritative. It steps one shared simulation (@netforge/engine-sim, an ECS with Rapier physics) at a fixed 30 Hz and sends each player a delta snapshot of what is near them 20 times a second (the local host caps a survivor snapshot at 250 enemies within 40 m). Your own keeper is predicted in the browser from the same simulation code, so movement never waits for the network; enemies are interpolated. The client draws the crowd with instanced meshes, so 2,000 enemies are a handful of draw calls rather than 2,000. On slow devices the render stack drops shadows, post effects and resolution by itself to hold the frame rate.

See Simulation and ECS and Netcode and prediction for the details, and the survivor-like template page for every file.

Alternatives

  • Phaser is a free, open-source 2D framework with WebGL and Canvas rendering and a very large example library. It is a strong choice for a single-player 2D survivor-like with sprite art, where you write the game loop yourself. See Net Forge and Phaser.
  • Babylon.js is a full 3D engine with thin instances for large crowds and Havok physics. Pick it when you want an editor-style 3D toolset and plan to write your own networking.
  • Colyseus is an authoritative Node.js game server with rooms, matchmaking and binary delta state sync, and client SDKs for many engines. Pair it with any renderer when you want to own the server framework. For Phaser and Colyseus used together, see Net Forge vs Phaser + Colyseus.

Pick Net Forge when you want the multiplayer survivor-like already finished, with one simulation on both sides and a bot playtest your agent can run.

FAQ

How many enemies can a browser survivor-like handle?

The EMBERWATCH template holds 2,000 enemies with four players. On the server side, the simulation measured 1.32 to 1.49 ms per tick on average with 2,000 enemies and four bots on an Apple M2 Pro (three 30-second runs, 2026-10-24); the browser draws them as instanced crowds. The template caps the living crowd at 2,000, so that is the most it holds. To repeat the measurement from a clone of the public repository, run pnpm --filter @netforge/template-survivor bench -- --repeats 3 --seconds 30.

Is the game multiplayer out of the box?

Yes. Up to four players share one room on an authoritative server. Open the link in another tab, or run npm run play -- --host 0.0.0.0 so devices on your network can join.

Do I need to write a game loop or a renderer?

No. src/game.ts declares enemies, weapons, upgrades, the director and earn rules. The template's client and the engine do the loop, rendering, input and netcode.

Can I change the art style?

Yes. game({ style }) takes lowpoly, toon, ps1, pixel, voxel or handdrawn, and models are referenced by library id such as style.lowpoly.ghost.

Why do enemies never spawn on screen?

The director spawns every enemy just off screen by design, and the playtest fails a game where more than 2% of spawns land on screen. It is one of the design rules the bots score.

Does it pay real money?

No. Locally everything is play money: earn rules fire so you can see them and pay nothing. A token economy is an optional upgrade that runs only on the hosted Net Forge service.