跳到正文
DEV Community· Superevh·· 3 小時前AI 評分54

構建多人瀏覽器遊戲時遇到的三個 bug,其中一個令流量來源全數消失

Three bugs from building multiplayer browser games (one hid all my traffic)

AI 導讀

作者分享構建多人瀏覽器遊戲時遇到的三個 bug,分別涉及題圖短暫完整顯示、回合計時接手,以及流量來源統計失準。React 重用題圖元素造成答案短暫曝光,按題目 ID 設定 `key` 可避免舊狀態動畫;計時採用錯開觸發,主持人或繪圖者失聯時,其他玩家在 3.5 秒後接手,並以 `turnId` 忽略不適用的重複動作。

正文

I run a lot of remote meetings, and the first five minutes are usually silence and black camera tiles. So I built a set of small multiplayer games that run in the browser: Draw & Guess, Two Truths and a Lie, Emoji Charades, Zoom In and a few more. Everyone joins from one link, with no accounts.

The idea was simple. The bugs were more interesting, so here are three of them.

1. The game kept showing the answer

Zoom In shows a mystery picture as an extreme close-up, then slowly zooms out. Whoever names it first scores the most.

Playing it, I noticed that each round flashed the whole picture for a moment before zooming in. That ruins the game.

The cause was React doing its job. The picture element was reused from one round to the next. At the end of a round it was fully zoomed out, and a 0.25-second CSS transition animated it back up to full zoom when the next picture arrived. For a quarter of a second, you could see the new picture at its full size.

The fix is one prop. Key the element by puzzle, so each picture mounts fresh instead of animating from the old state:

<div className="zi-view" aria-hidden="true" key={c.id}>

If a CSS transition ever shows you something it shouldn't between two states, ask whether those should be the same element at all.

2. Nobody owned the clock

A party game has to keep moving on its own, and the obvious design has a flaw: if only the host's browser moves the game on and the host's Wi-Fi drops, the round just sits there.

The game state is a pure reducer over an action log. Time comes from action.at and randomness from action.nonce, so every client that replays the same log reaches the same state. That means any client can safely push the game forward. The problem becomes who should do it, and when.

The answer is a staggered timer. The host and the current drawer fire on time, and everyone else waits a few seconds:

const driver = isDrawer || me === room.ownerId
const late = driver ? 0 : 3500

const fire = (key, at, action) => {
  if (now < at + late) return
  const prev = fired.current[key]
  if (prev && now - prev < 6000) return
  fired.current[key] = now
  act(action)
}

Normally the host or drawer fires first. If they vanish, everyone else steps in 3.5 seconds later. The room may send the same action more than once, so each action carries a turnId and the reducer ignores any that no longer apply.

3. My analytics had been wrong from day one

This one isn't about games, but it cost me more than the others.

I wrote a tiny cookie-free page counter, with the endpoint in a constant:

const URL = 'https://…'

Further down, the code called new URL(document.referrer) to read where a visitor came from. That line was now trying to construct a string. It threw, a catch returned an empty value, and every visit was recorded with no referrer. 217 page views later I noticed that not one of them had a source.

So I spent two days making decisions about traffic with no idea where it came from. Renaming the constant took ten seconds. The lesson I took is to test your measurement with a known input on the day you ship it, because a counter that goes up looks like a counter that works.

Try it, or tell me what's broken

The games are free with no sign-up, and they run on React, Vite, Vercel and Supabase realtime channels: fun-retro.com/games.

I'd especially like to hear about lag or sync glitches with five or more people in a room. How do you handle timers when any client can disappear?

I build these with AI as a pair-programming partner, and this post was drafted with AI assistance from the real code and bug history.

來源:DEV Community · dev.to