Skip to content

Hits between players: tell the owner

Last updated October 6, 2026

From racecar, a real game built on GameRelay · the source

In a racing or fighting game, players hit each other. But each player's car is an entity that only its owner may move. So when your car hits someone else's, your screen can't push theirs. It tells their owner what happened, and their screen moves it. This is how racecar does bumps, takedowns and traffic hits.

Why "tell the owner"

Each screen runs physics for its own cars only (and the host for the AIs). If your screen also moved other players' cars, two screens would fight over the same car and it would jitter. One owner per car, and messages to that owner, keeps every car smooth and every decision in one place.

1. One name for each car

// Name every car the same way on every screen, and know who speaks for it.
// 'p:<player id>' for a player's car, 's:<seat>' for an AI (the host drives those).
const ownerOf = (name) => (name.startsWith('s:') ? 'host' : name.slice(2));
const speaksFor = (from, name) => (ownerOf(name) === 'host' ? from === room.hostId : from === ownerOf(name));

2. Bumps go to the car's owner

// After each step: your car touched another screen's car. Your physics pushed yours;
// tell their owner how hard theirs was pushed, and let them move it.
const BUMP_EVERY = 0.15; // s, per pair: a contact lasts a few steps
for (const hit of contactsThisStep) {
  if (!mine(hit.a) || mine(hit.b)) continue;                 // one of yours, one of theirs
  if (now - (lastSent.get(pair(hit)) ?? -Infinity) < BUMP_EVERY) continue;
  lastSent.set(pair(hit), now);
  room.emit('bump', { to: name(hit.b), by: name(hit.a), t: sim.time, dvx: hit.dvx, dvz: hit.dvz }, { to: ownerOf(name(hit.b)), echo: false });
}

// On the owner's screen: apply it, unless this screen saw the same contact itself.
room.on('bump', (d, from) => {
  const b = readBump(d);                                     // check the shape and the numbers first
  if (!b || !speaksFor(from, b.by)) return;                  // only the other car's owner may say it bumped you
  if (sawContact(b.to, b.by, b.t, 0.15)) return;             // within ±150 ms: already pushed here, once
  pushCar(b.to, clamp(b.dvx, -30, 30), clamp(b.dvz, -30, 30)); // their word, so capped
});

Often both screens see the same contact: both cars touched on both screens. Then each screen pushes its own car and ignores the other's message, so nothing is pushed twice. A contact only one screen saw (the network delay hid it on the other) still reaches the other car through the message.

3. Takedowns: the victim decides

// The victim's screen decides a wreck (its car, its call), and credits the attacker
// by telling the attacker's owner. That screen gives its own car the boost and the points.
room.emit('takedown', { victim: name(car), by: name(attacker), t: sim.time }, { to: ownerOf(name(attacker)), echo: false });

room.on('takedown', (d, from) => {
  const t = readTakedown(d);
  if (t && speaksFor(from, t.victim)) creditTakedown(t.by);  // only the victim's owner can hand out credit
});

Whose screen decides a wreck? The victim's: it's their car. The victim's screen then tells the attacker's owner, whose screen gives the credit. Without that message, a takedown would only count on the victim's screen.

4. Shared things: claim them

Traffic belongs to nobody. When two players hit the same truck at nearly the same time, a claim decides who hit it: the server gives it to exactly one player.

// Traffic is the same on every screen (a function of the race time), but a hit happens on
// one screen: the one whose car made it. Two players can hit the same truck at once, so claim it.
// Your screen has already wrecked it (your car hit it here), so tell everyone else when.
if (await room.claim(`traffic:${truck}`)) {
  room.emit('traffic_hit', { k: truck, t: sim.time }, { echo: false });
}
room.on('traffic_hit', ({ k, t }) => wreckTraffic(k, t)); // every other screen wrecks it from then

// When the truck is back on the road, let the claim go so it can be hit again.
room.release(`traffic:${truck}`);

racecar uses the same pattern for breakable things at the roadside.

Things to get right

  • Check every message. It came from another player's page. racecar checks the shape and ranges of everything (src/net/wire.ts) and caps how much a bump can change your speed.
  • Check who's speaking. Only the other car's owner may report a bump from it, and only the victim's owner may hand out a takedown.
  • Send to one player. emit(…, { to }) sends to that player only ('host' for the host). Fewer messages, and nobody else needs them.
  • The trust model. This is party-grade: a modified page could lie about its own car. That's fine among friends. Competitive games need the host (or a server you run) to referee.

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.