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.