From racecar, a real game built on GameRelay · the source
In racecar, up to 7 AI drivers race alongside the players. Every screen must see the same AIs in the same places, so one browser drives them: the room's host. The host sends each AI car as a host entity, and everyone else draws it like another player's car. When the host leaves, the next one carries on driving them from where they are.
Why the host drives them
If every screen ran its own AIs, they'd drift apart within seconds: a tiny timing difference changes every overtake. One driver means one truth. The AI keeps no memory beyond the car's position and speed (and a little more, below), so any player can take over the driving at any moment.
1. One entity kind for the AIs
// One kind for the AI cars: a car's fields, plus which lobby seat it's in and which race.
const rivals = room.define('rival', { x: 'number', z: 'number', h: 'angle', speed: 'number', seat: 'number', race: 'text' }, { rate: 30 }); The race field matters because host entities outlive a race: the room stays up between races, and the last race's AIs belong to that race, not this one.
2. The host writes them
// After each step of the race, on the host: write every AI car as it is now.
function afterStep() {
if (!room.isHost) return;
const mine = rivals.mine(); // host entities count as yours while you're the host
for (const seat of aiSeats) {
const car = sim.carInSeat(seat);
let e = mine.find((r) => r.seat === seat && r.race === raceId);
if (!e) e = rivals.spawn({ seat, race: raceId }, { owner: 'host' }); // the first time
const jumped = Math.hypot(car.x - e.x, car.z - e.z) > 20;
Object.assign(e, { x: car.x, z: car.z, h: car.h, speed: car.speed });
if (jumped) e.teleport(); // a respawn: others see a jump, not a slide across the map
}
}3. Everyone else follows
// Before each step, on everyone else: put each AI car where the host says.
function beforeStep() {
if (room.isHost) {
for (const seat of aiSeats) sim.carInSeat(seat).remote = false; // yours to drive now
return;
}
// Only the host's count: any player could spawn a 'rival', but it isn't theirs to drive.
const theirs = rivals.all().filter((e) => e.owner.id === room.hostId && e.race === raceId);
for (const seat of aiSeats) {
const car = sim.carInSeat(seat);
const e = theirs.find((r) => r.seat === seat);
if (!e) {
car.remote = false; // not sent yet: drive it here, from the same grid, as the host would
continue;
}
car.remote = true;
car.setPose(e.x, e.z, e.h, e.speed);
}
}Until the host's AIs arrive (the race just started, or nobody has connected yet), each screen drives them itself from the same starting grid. Nothing waits on the network.
4. When the host changes
The SDK moves host entities to the new host by itself. On the next step, room.isHost is true on the new host, and its beforeStep takes over driving from the positions the old host last sent. racecar also sends what the next driver needs beyond the position: a wreck in progress (how long it has left, and the tumble), the boost meter, and the last safe spot on the road for a respawn.
// For a moment, two players can both think they're host and both spawn a seat's AI.
// Keep one per seat, and remove the last race's.
const keep = new Map();
for (const e of rivals.mine()) {
if (e.race !== raceId || keep.has(e.seat)) e.remove();
else keep.set(e.seat, e);
}Things to get right
- Keep the AI's memory in the entity. Anything only in the old host's variables is lost at a handover. If the next host needs it, send it.
- Check who owns it. Read the host's entities (
e.owner.id === room.hostId), not any entity of that kind. - Hidden tabs. A host in a background tab must keep stepping. racecar steps with the SDK's
relay.tick, which keeps running when the browser pauses animation frames. - Smoothing and prediction. Entities are drawn about 100 ms in the past. racecar turns smoothing off for cars and predicts them forward from
room.renderTimeinstead, which suits fast cars. Most games can keep the default smoothing.
Try it
GameRelay has a free plan, no card needed. Get a public key, then follow the quickstart, or point your AI coding agent at llms.txt.