Authoritative shooter
Competitive action is the top rung: the server simulates the world, clients only
send inputs, and hit detection happens where no one can fake it. irt.io gives
you a fixed-rate tick and per-entity prediction to keep it responsive.
Inputs as RPCs
export const rpc = defineRpc({
move: { args: { ax: number(), ay: number() } },
fire: { args: { angle: number(), power: number() } },
}); The server tick
export default defineRoom(schema, {
rpc: {
move(state, { ax, ay }, ctx) { state.players[ctx.clientId].input = { ax, ay }; },
fire(state, { angle, power }, ctx) {
state.bullets[uid()] = spawnBullet(state.players[ctx.clientId], angle, power);
},
},
tick(state, dt) {
for (const p of Object.values(state.players)) integrate(p, dt);
for (const b of Object.values(state.bullets)) {
advance(b, dt);
const hit = firstHit(state.players, b);
if (hit) { hit.hp -= b.damage; delete state.bullets[b.id]; }
}
},
}); Client prediction
Mark the local player predicted so your own movement responds instantly, then reconciles against the server:
const room = await joinRoom(schema, { predict: ['players'] });
function frame(dt: number) {
room.rpc.move(readStick());
integrateLocal(room.state.players[room.me], dt); // shown immediately
} Small server corrections are smoothed; large ones snap. Tune the threshold per entity.
Next steps
Placeholder guide.