Getting Started
Build the gameplay module first, then choose the runtime host.
1. Write a defineGame Module
defineGame is the current game-authoring entry point. The module is host-agnostic: no Trystero imports, no worker imports, no DOM ownership in simulation code.
import { defineGame } from './defineGame.js';
export default defineGame({
initialState: () => ({
ball: { x: 50, y: 50, vx: 1, vy: 1 },
paddles: {}
}),
sampleInput: () => ({
dy: (keys.ArrowDown ? 1 : 0) - (keys.ArrowUp ? 1 : 0)
}),
step(state, ctx) {
for (const id of ctx.participants || []) {
const input = ctx.input?.(id) || {};
state.paddles[id] ??= { y: 50 };
state.paddles[id].y = Math.max(0, Math.min(100, state.paddles[id].y + (input.dy || 0)));
}
state.ball.x += state.ball.vx;
state.ball.y += state.ball.vy;
},
draw(state, ctx) { /* draw from read-only state */ }
});
2. Move to Schema-Backed Performance Mode When Needed
For V0.2/V0.3 performance mode, declare canonical state with schema helpers. Tables contain scalar fields only; wrapper classes are authoring handles over plain rows.
import { schemaScalar, schemaRecord, schemaTable } from '../experimental/performance-mode/state-backend.js';
class Ball { static table = 'balls'; }
class Paddle { static table = 'paddles'; }
export const SCHEMA = {
balls: schemaTable(schemaRecord({
x: schemaScalar('number'),
y: schemaScalar('number'),
vx: schemaScalar('number'),
vy: schemaScalar('number')
}), { id: 'string', order: 'id' }),
paddles: schemaTable(schemaRecord({
y: schemaScalar('number'),
score: schemaScalar('number')
}), { id: 'string', order: 'id' })
};
3. Author Deterministic step(ctx)
In schema-generated mode the engine hands your step a PerformanceModeEngineContext. Use ctx.world() as the release-facing authoring surface.
export function step(ctx) {
const world = ctx.world();
for (const playerId of world.participants()) {
const input = world.input(playerId) || {};
let paddle = world.get(Paddle, `paddle:${playerId}`);
if (!paddle) paddle = world.spawn(Paddle, `paddle:${playerId}`, { y: 50, score: 0 });
paddle.s.y = Math.max(0, Math.min(100, paddle.s.y + (input.dy || 0) * world.dt));
}
const ball = world.get(Ball, 'ball:main');
if (ball) {
ball.s.x += ball.s.vx * world.dt;
ball.s.y += ball.s.vy * world.dt;
}
}
step(ctx) must produce the same state from the same previous state, tick, and routed inputs. Do not read Date.now(), Math.random(), DOM state, renderer state, or transport state inside simulation.
4. Render From the Mirror
Performance-mode rendering is deliberately separate. Simulation publishes em.bus.anchor and em.bus.mirror frames; the renderer reads RenderMirror tables and owns only presentation objects.
export function drawPongMirror(mirror, canvasContext) {
const balls = mirror.tryTable('balls');
if (balls) for (const ball of balls.values()) {
canvasContext.fillRect(ball.x - 2, ball.y - 2, 4, 4);
}
}
5. Choose the Runtime Host
Standalone browser demos use TrysteroTransport for peer-to-peer rooms. Tests and embedded hosts inject their own transport and clock. V0.3 buses, schedulers, runtime adapters, and workers are runtime infrastructure that can host the same game model; they are not a separate authoring API.
Copy-Paste: Launch a Simple Trystero Room
For a standalone browser page, omit transport. EasyMultiplayer.start() lazily creates a TrysteroTransport for the room.
import { EasyMultiplayer } from './EasyMultiplayer.js';
import gameModule from './my-game.js';
const room = new URLSearchParams(location.search).get('room') || 'demo-room';
const em = new EasyMultiplayer({
room,
game: gameModule,
tickRate: 20,
maxPlayers: 4,
getLocalInputs() {
return {
discoverable: true,
input: gameModule.sampleInput?.() || {}
};
}
});
em.start();
Next Steps
- API Reference -
defineGame,ctx.world(), schema helpers, mirror rendering, and runtime boundaries - Examples - current Pac-Man V0.2/V0.3 demos and a Ball/Pong-sized slice
- V0.2 Game Authoring - deeper schema-backed guide
- Performance Mode Developer API - full lower-level details