# How to make a multiplayer 3D platformer for the web

> Start the SKYLEDGE platformer with one command: up to eight climbers race cliffside courses with coyote time, jump buffering, variable jump height, dashes, springs and moving platforms. The movement is predicted bit for bit, and a route planner checks every course. Tune the feel in src/game.ts.

Net Forge, the open-source multiplayer browser game engine built for AI coding agents, is published on npm under `@netforge` with source at https://github.com/netnetdev/net-forge.

Source: https://forge.netnet.capital/docs/guides/3d-platformer-web  
Last updated: 2026-10-23. Checked against @netforge/engine 0.2.0.

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.

```sh
npm create netforge@latest my-climb -- --template platformer --yes
cd my-climb
npm run play
```

## What 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

```sh
npm create netforge@latest my-climb -- --template platformer --yes
cd my-climb
npm run play -- --players 2
```

## 2. Tune the feel

The movement numbers sit in the `motor` field of `game({...})`. Change a few at a time:

```ts title=src/game.ts check=skip
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

```ts title=src/game.ts
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

```sh
npm test
```

The 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](https://forge.netnet.capital/docs/concepts/netcode-prediction.md) and the [platformer template page](https://forge.netnet.capital/docs/templates/platformer.md).

## Alternatives

- **[Phaser](https://docs.phaser.io/)** 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](https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_web.html)** 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.
