How to make a multiplayer 3D platformer for the web
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.
Run npm create netforge@latest my-climb -- --template platformer --yes and you have a multiplayer platformer race: up to eight climbers run, jump, dash, ride moving platforms and bounce off springs to reach the flag first, with seat bots so one person still has a race. The jump feel is a handful of numbers in src/game.ts, and a route planner proves every course can be finished.
npm create netforge@latest my-climb -- --template platformer --yes
cd my-climb
npm run playWhat you get
| Part | Where | Notes |
|---|---|---|
| Jump feel | game({ motor }) | Run speed, jump height, time to apex, fall gravity, jump cut, coyote time (0.1 s), jump buffer (0.13 s), air jumps, dash speed. |
| Courses | game({ levels }) | Platforms, moving platforms, springs, thorns, coins and checkpoints. One course per round. |
| Race rounds | engine RaceRunner | Countdown, race, results, next course. |
| Seat bots | players.bots | Run routes found by the route planner; never earn. |
| Softlock check | route planner | The playtest fails a course with no route to the flag. |
| Server cost | measured | Four playtest climbers finish seven rounds with a p99 of 0.033 ms per tick (Apple M2 Pro, Node 22). |
1. Start it
npm create netforge@latest my-climb -- --template platformer --yes
cd my-climb
npm run play -- --players 22. Tune the feel
The movement numbers sit in the motor field of game({...}). Change a few at a time:
export default game({
template: "platformer",
// ...the rest of the template's game({...}) stays as it is
motor: { runSpeed: 8, jumpHeight: 3.6, airJumps: 1 },
});Accepted ranges: runSpeed 3 to 14, jumpHeight 1 to 8, timeToApex 0.2 to 0.7, fallGravity 1 to 3, jumpCut 0.2 to 1, coyoteSeconds and bufferSeconds 0 to 0.25, airJumps 0 to 2, dashSpeed 6 to 30. The route planner re-checks every course with the new numbers, so a jump made too low to clear a gap fails the playtest instead of shipping.
3. Reward a clean climb
import { earn, system } from "@netforge/engine";
system("clean_climb", ({ on, emit }) => {
const falls = new Map<number, number>();
on("player.fell", (ids) => {
for (const id of ids) falls.set(id, (falls.get(id) ?? 0) + 1);
});
on("player.finished", (ids) => {
const clean = ids.filter((id) => !falls.get(id));
if (clean.length) emit("climb.clean", clean);
});
on("run.started", (ids) => {
for (const id of ids) falls.delete(id);
});
});
earn.rule("speedy", { when: "player.finished", n: 1, pay: 6, oncePer: "run" });Race events include player.finished, race.won, race.podium, coin.collected, checkpoint.reached, player.fell and spring.bounced. Winning against a field of bots pays nothing extra.
4. Test it
npm testThe playtest first asks the planner for a route to the flag on every course, then races four climbers through the rounds and checks that they finish and that earn rules fire.
How it works
The platform sim is pure TypeScript: swept AABB movement on a 1/1024 m grid at 60 Hz in @netforge/engine-sim's platform mode. Because it is deterministic, the browser predicts your own climber and replays it bit for bit when a snapshot arrives. Climbers pass through each other and coins are per climber, so nothing another player does can make your prediction wrong, which is why the race feels local even online. See Netcode and prediction and the platformer template page.
Alternatives
- Phaser is a free, open-source 2D framework with WebGL and Canvas rendering. Pick it for a single-player 2D platformer with tile maps and sprite art.
- Godot has a scene editor and a character body for platformer movement, and exports to the web as WebAssembly with WebGL 2. Pick it for a level-editor-first workflow and a native build.
Pick Net Forge when you want an online race with prediction and seat bots, and courses that a planner verifies after every change.
FAQ
What is coyote time and does the template have it?
Coyote time lets a jump still work for a moment after you run off a ledge. The template has 0.1 s of it, plus a 0.13 s jump buffer, variable jump height and a lighter apex while Jump is held.
Can players knock each other off?
No. Climbers are ghosts to each other, which keeps every player's movement predicted exactly.
How does the game know a course can be finished?
A route planner searches each course with the current movement numbers. If it finds no route to the flag, the playtest fails, so a broken course never reaches players.
Is the platformer 3D?
The courses are 3D scenes in every art style, played from a side view.
How many players can race?
Up to eight per room, with seat bots filling empty places.