3D multiplayer in the browser
For a 3D game the bandwidth question is the transform stream: position and rotation for every player, many times a second. Routing that through a server is what makes realtime backends expensive. Here it goes peer-to-peer over WebRTC, so the server only sees a presence call every five seconds per player once connections are up.
What must not be spoofed — hit points, kills, scores — goes through shared state, where your declared rules bound every write. Our FPS demo is built exactly this way, in Three.js, and its source is readable in the browser.
A complete Three.js integration
import * as THREE from 'three';
import { Multiplayer } from 'https://multiplayerapi.com/sdk/mp.esm.min.js';
const mp = new Multiplayer({ key: 'mp_pk_...' });
await mp.login();
const room = await mp.quickmatch({ mode: 'fps', max_players: 12 });
const remotes = new Map(); // account_id -> { mesh, from, to, t0 }
room.on('peer', (p, from) => {
let r = remotes.get(from);
if (!r) {
const mesh = new THREE.Mesh(new THREE.CapsuleGeometry(0.4, 1), new THREE.MeshStandardMaterial());
scene.add(mesh);
r = { mesh, to: p };
remotes.set(from, r);
}
r.from = r.to; r.to = p; r.t0 = mp.now(); // interpolate between the last two packets
});
room.on('members', (m) => {
const alive = new Set(m.map((x) => x.account_id));
for (const [id, r] of remotes) if (!alive.has(id)) { scene.remove(r.mesh); remotes.delete(id); }
});
room.mine('hp', 100);
room.state.watch('players.*.hp', (hp, path) => hud.setHp(path.split('.')[1], hp));
setInterval(() => {
const p = camera.position, q = camera.quaternion;
room.send({ x: p.x, y: p.y, z: p.z, qx: q.x, qy: q.y, qz: q.z, qw: q.w });
}, 50);
function animate() {
const now = mp.now();
for (const r of remotes.values()) {
const k = Math.min(1, (now - r.t0) / 50);
r.mesh.position.set(
r.from.x + (r.to.x - r.from.x) * k,
r.from.y + (r.to.y - r.from.y) * k,
r.from.z + (r.to.z - r.from.z) * k);
r.mesh.quaternion.set(r.to.qx, r.to.qy, r.to.qz, r.to.qw);
}
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();What goes where
| Channel | Call | Delivery | Use it for |
|---|---|---|---|
| Direct | room.send(data) | peer-to-peer, best effort, zero server cost | positions, aim, animation |
| State | room.set(path, value) | server-validated, kept for the match, reaches late joiners | health, score, whose turn |
| Events | room.emitEvent(topic, data) | ordered, guaranteed, not persisted | chat, rematch, one-shots |
Three.js-specific tips
- Interpolate on
mp.now(), the server-aligned clock, notDate.now(): client clocks drift by seconds. - Render others ~120 ms in the past. It leaves room for the next packet and turns jumps into motion.
- Hits: the shooter reports a hit, the victim's client writes its own
hp, and a rule such as{"write":"self","min":0,"max":100}bounds it. Kills that count for a leaderboard go through match attestation. - Quantize positions to centimeters before sending if you have many players: smaller packets, same feel.
Can I build a multiplayer FPS with Three.js and no game server?
Yes — our FPS demo is exactly that. Transforms and shots go peer-to-peer; health and kills are arbitrated in shared state.
What happens when WebRTC cannot connect two players?
That peer pair falls back to a server relay automatically. The rest of the room stays direct, so one player on a hostile network does not slow everyone down.
Does it work with React Three Fiber?
Yes. The SDK is plain JavaScript; create the client once, keep the room in a ref or store, and read remote transforms in useFrame.
Make your game online today
Free sandbox keys in one call, no account and no card. $19.99/month when you ship, for every game you make.
Free sandbox · $19.99/mo to go live · cancel anytime