Multiplayer without networking. You write game logic; playertwo handles sync, transports, and engine glue. Built for fast prototyping, repeatable multiplayer tests, and production-ready host-authoritative sync.
Website • Docs • Live demos • GitHub
- State-first API: mutate state, dispatch actions; no packet plumbing.
- Host-authoritative by default: deterministic, conflict-safe, cheat-resistant.
- Swap transports in one line: local (shared memory), WebRTC, WebSocket, Colyseus bridge.
- Engine adapters: Phaser today; adapters are pluggable/DIY without touching game code.
- Single-tab multiplayer: run host + clients in one tab for tests/demos/CI, zero servers.
- Built-in observability: action timeline, state diffs, network monitor, replay-friendly data.
- Browser IDE: embeddable dual-pane playground to teach, demo, and test live.
pnpm add @playertwo/core @playertwo/phaser @playertwo/transport-local phaserimport { defineGame, GameRuntime } from '@playertwo/core';
import { PhaserAdapter } from '@playertwo/phaser';
import { LocalTransport } from '@playertwo/transport-local';
// 1) Game logic (no networking code)
const game = defineGame({
setup: ({ playerIds }) => ({
players: Object.fromEntries(playerIds.map(id => [id, { x: 0, y: 0, hp: 100 }]))
}),
actions: {
move: (state, { playerId, dx, dy }) => {
state.players[playerId].x += dx;
state.players[playerId].y += dy;
},
hit: (state, { playerId, amount }) => {
state.players[playerId].hp -= amount;
}
}
});
// 2) Transport + runtime (host-authoritative)
const transport = new LocalTransport({ roomId: 'dev-room' });
const runtime = new GameRuntime(game, transport, { isHost: true });
await runtime.start();
// 3) Engine glue (Phaser)
const adapter = new PhaserAdapter(runtime, this);
adapter.trackSprite(playerSprite, `player-${myId}`); // auto-sync position/rotation/velocity
// 4) Dispatch actions anywhere
runtime.dispatchAction('move', { playerId: myId, dx: 10, dy: 0 });Swap transports without touching game code:
// P2P
const transport = new TrysteroTransport({ roomId: 'p2p-demo' });
// WebSocket
const transport = new WebSocketTransport({ url: 'wss://game.com/socket' });
// Colyseus bridge
const transport = new ColyseusTransport({ room: colyseusRoom });actions: {
collectCoin: (state, { playerId, coinId }) => {
const coin = state.coins.find(c => c.id === coinId);
if (coin && !coin.collected) {
coin.collected = true;
state.players[playerId].score += 10;
}
}
}Host applies the action → computes a minimal patch → broadcasts. Clients patch and render. No packet formats, no manual broadcasts.
- One peer (or server) is the host; it is the source of truth.
- Clients send inputs; host applies, resolves conflicts, sends diffs.
- Deterministic RNG available to keep simulations aligned.
- Optional server/back-end can still own matchmaking/auth/storage.
const host = new LocalTransport({ roomId: 'test', isHost: true });
const client = new LocalTransport({ roomId: 'test', isHost: false });Both sides share memory, so you can:
- Unit test multiplayer logic synchronously.
- Run CI without spinning up signaling/relay servers.
- Demo multiplayer interactions offline.
const adapter = new PhaserAdapter(runtime, scene);
adapter.trackSprite(sprite, `player-${playerId}`); // syncs pos/vel/flip/anim hooks
// Physics stays native:
sprite.setVelocity(200, 0); // host simulates, clients mirrorAdapters are thin glue: your engine code stays idiomatic; playertwo syncs state.
@playertwo/transport-local: tests, CI, offline demos.@playertwo/transport-trystero(WebRTC): P2P demos, zero server cost.@playertwo/transport-ws: centralized relay/prod.@playertwo/transport-colyseus: reuse Colyseus rooms/matchmaking.- Write-your-own: any protocol that can broadcast playertwo messages.
- Embeddable
<PlayerTwoIDE>(Svelte) for docs/playgrounds. - Dual-pane host/client, live code editing, run/reset.
- State inspector and action timeline from
@playertwo/devtools.
- Seeded RNG helper for reproducible simulations.
- Host-only side effects; clients display-only by default.
- Use
LocalTransportin tests to assert on state without network flake.
Traditional networking: invent N message types, serialize, order, dedupe, replay.
playertwo: mutate state → diff → patch. Less code, fewer edge cases.
if (runtime.isHost) {
this.physics.add.collider(players, walls, () => {
runtime.dispatchAction('hit', { playerId, amount: 10 });
});
}Clients never run the collision; they just receive patched state. Speed hacks and bogus collisions are ignored.
Snapshot interpolation is always on for clients: everything renders ~32ms in the past for jitter-free motion. No modes to pick—raise snapshotBufferSize on the adapter if you want extra smoothing.
- Keep your existing backend for auth/matchmaking/metrics.
- Let playertwo own only the game-state wire payloads.
- Move from P2P to WebSocket for scale without rewriting game logic.
- Diff-based patches keep bandwidth low; unchanged fields are free.
- Batch multiple mutations per tick.
- Choose transport for cost/observability: WebRTC for cheap demos; WebSocket/Colyseus for prod.
- Keep host authoritative; seed RNG for spawning; record action timelines to debug desyncs.
| Package | Description |
|---|---|
@playertwo/core |
Runtime, actions, state diffing, host loop |
@playertwo/phaser |
Phaser adapter: sprite/physics sync, interpolation |
@playertwo/transport-local |
Shared-memory transport for tests/demos |
@playertwo/transport-trystero |
WebRTC P2P transport |
@playertwo/transport-ws |
WebSocket transport |
@playertwo/transport-colyseus |
Colyseus room bridge |
@playertwo/devtools |
Action/state inspector, timeline |
@playertwo/ide |
Embeddable browser IDE |
pnpm install
pnpm dev
pnpm build
pnpm test- Docs & guides: https://playertwo.com/docs
- Live playground: https://playertwo.com/preview
- Issues / discussions: https://github.com/BlueprintLabIO/playertwo
- npm (core): https://www.npmjs.com/package/@playertwo/core
Apache-2.0 © Blueprint Lab