跳到正文
DEV Community· Tophe·· 3 小時前AI 評分50

pingpad:以手機作球拍,在瀏覽器玩乒乓球

pingpad: table tennis in the browser, with your phone as the paddle

AI 導讀

pingpad 是一款在瀏覽器運行的乒乓球遊戲,玩家用手機掃描 QR code,手機便成為球拍。手機與遊戲畫面透過 WebRTC data channel 點對點通訊;作者把 PeerJS signaling server 移植至 Cloudflare Workers 的 Durable Objects,只用來介紹兩部裝置。

正文

pingpad is a table tennis game that runs in the browser. You open pingpad.app on a laptop, a tablet or a TV, scan the QR code with your phone, and the phone becomes your paddle. There's nothing to install and no account to create. You can play the computer, play a friend while the next players wait on the bench, or play alone and chase a record. Badges and records stay on your phone.

One of the people who tried it summed it up: "The Scan'n Play side of it works really well. No install, no questions, no pesky app store. I love it."

This article is about how it's built: a phone and a screen talking peer to peer, a swing recognised on the phone rather than tracked, a lot of work spent on guiding the player between two devices, and the library it all sits on.

It started with a counter

In 2019 I wrote a WebRTC proof of concept: a counter shown on one screen and driven from a phone. You snapped a QR code and updated the counter in real time. The goal was to go a bit further than the usual WebRTC exercises.

In 2022 it became webrtc-remote-control, an open source library for what any app runs into once a phone drives another screen: reconnections, page reloads, phones going to sleep, a remote that has to find its screen again. I had demos, the counter and an accelerometer moving a 3D object, and they worked. What I lacked was a game idea.

At the end of September 2026 I started on pingpad with AI coding agents. The library already handled the most critical part, the link between the phone and the screen, so I could build the game on top of it. The React and Vue bindings each come with an llm.md guide, written so that a coding assistant knows how to use them.

Five weeks later, pingpad has had over 200 pull requests merged.

Phone and screen, peer to peer

The phone and the screen talk over a WebRTC data channel. No game server sits between them.

Two devices still need someone to introduce them. I ported the PeerJS signaling server to Cloudflare Workers, on Durable Objects. It introduces the phone and the screen, then carries nothing. I picked Workers because the alternatives fit badly. Vercel cuts a WebSocket function after 300 seconds and bills its memory as long as the socket is open, so a screen left open would have used up the monthly quota in about a week. Render's free tier goes to sleep after 15 minutes. Cloudflare doesn't bill a hibernated Durable Object socket, so a screen waiting for players costs nothing.

STUN finds a direct path when one exists, even between a phone on 5G and a laptop on home Wi-Fi. Some networks make that impossible, such as a symmetric NAT or a corporate firewall. For those, Cloudflare's TURN relays the traffic, still DTLS-encrypted, so the relay can't read it. The screen shows whether each paddle connected over the local network, across the internet through STUN, or through the relay, something WebRTC usually hides.

On the same Wi-Fi, the median latency from phone to screen is 3.5 ms.

Recognise the swing, don't track the paddle

A phone can't tell where it is in space. Its sensors give acceleration and orientation, not position, so you can't track a paddle the way a camera would. What you can do is recognise a swing.

So the phone doesn't stream its sensors. It runs the swing detector itself, at full sensor rate, 60 times a second on iOS, and sends one compact, timestamped event when it sees a swing. The screen converts that timestamp with a clock offset it measures by pings, measured again regularly and from scratch when a paddle reconnects. Then the screen decides whether the swing hit the ball, by comparing the time of the swing with the time the ball reached the paddle. A network hiccup delays the message, but it doesn't change your shot.

I don't have a ping-pong table at home, so I recorded myself hitting foam stress balls with the back of the phone. A recorder on the phone captured each take, sensors and microphone together, and I labelled it: forehands, backhands, serves. The detector's thresholds come from those recordings, never from tuning by hand. A bench replays every recording through the detector. The detector is a pure module with no DOM dependency, so the exact same function runs on the phone and in the bench.

The microphone was there to hear the contact, and it showed that the acceleration peak doesn't mark it. Depending on how you swing, the peak can come before the ball touches the phone as well as after. That's why the screen decides the hit with a window around the ball's arrival, instead of trusting the peak.

I tuned the detector on an iPhone. It's meant to work on any recent phone, and it has played fine on Android too.

Guiding the player between two devices

A good share of the pull requests went into guiding the player, because this isn't a game where you tap your screen. You hold your phone like a paddle while the game plays on another screen, and nobody has that reflex yet. At each step, the player has to be told what to do, on the device they're looking at.

Some of the situations the game handles:

  • A visitor clicks Play on their phone, when the phone is meant to be the paddle. The game explains that this device would become the screen, and offers to send the link to a computer.
  • The QR code is scanned from Instagram's or Messenger's built-in browser, which won't give access to the motion sensors. The paddle says so, and how to open the page in the real browser.
  • iOS only grants the motion sensors after DeviceMotionEvent.requestPermission(), called from a user gesture, on HTTPS. The paddle's button is that gesture.
  • Calibration shows a phone on the screen turning as the player turns theirs, so they can see what the game sees.
  • Once the ball is served, the paddle tells the player to watch the screen.
  • A phone locks, goes to the background or reloads mid-match. The game pauses instead of giving the point away, and the phone finds the game again when it comes back.
  • A third phone joins. It goes on the bench, in the order phones came in, and the screen calls it out by number. A player can hand over their seat, and a player who leaves keeps it until someone on the bench takes it.
  • A new version is deployed during a game. The protocol is versioned, so a phone and a screen on different versions notice it and say what to do.

Each of these has a message where the player is looking, phone or screen, and an end-to-end test that replays it.

Commands work the same everywhere. Pausing, choosing the match length or turning the sound off goes through one function on the screen, whether it comes from a button, a key, or a paddle. The screen sends each paddle the commands it offers, so any player can control the game from the phone in their hand. That was the point for TVs: once the game is open, nobody has to pick up the remote and steer a cursor through the TV's browser.

Testing a game that needs two devices

pingpad has 178 end-to-end tests, and 87 of them cover these player situations. Playwright opens the screen in one browser context and each paddle in another, emulating a phone, and sends the sensor events itself. A local PeerJS server starts with the tests.

Headless Chromium draws the 3D scene on the CPU. The screen and the paddles share that CPU, drawing the screen takes most of it, and the paddles fall behind. So in end-to-end runs, the screen doesn't draw, while its frame loop still advances the rally. The tests take less CPU and finish sooner.

The same fixtures film the demo videos for pull requests and the videos on the landing page. One setup drives the tests and the videos, so when the interface changes, the videos are filmed again with the same scenarios.

Those tests are what let me change the game without replaying every scenario by hand on two devices.

Working with agents

"A game in one prompt" makes for a good demo. pingpad wasn't one. What made agents useful on it was the setup around them:

  • the network layer was a library that already worked, documented for agents;
  • every change goes through a pull request, and every push runs format, lint, types, unit tests, build and end-to-end tests;
  • every pull request gets its own preview deployment, to try it on a real phone before it merges;
  • the dev server is reachable from a phone over HTTPS on the local network, thanks to portless, since iOS only gives motion sensors to a secure page;
  • a test that is written must fail when the condition it checks breaks;
  • an AGENTS.md file states the architecture and the rules, and the project records the decisions it lives with as ADRs;
  • every message between phone and screen is a schema, and the protocol is versioned, with each version frozen in a tested file.

I kept the design decisions, the tests on a real phone, and the approval of every merge. I wrote about how I work with AI agents on LinkedIn, in French.

What the game gave back to the library

Playing for real, with phones that lock, go to the background and change networks, turned up bugs the library's demos never showed. I fixed each one in the library rather than working around it in the game:

  • On iOS, a remote stayed listed after navigating away, because Safari skips beforeunload when it puts a page in the back/forward cache. The library now listens to pagehide (0.5.1).
  • A first connection whose ICE negotiation stalled waited forever (0.5.2).
  • Messages that arrived before the first listener was attached were lost, which affected the React bindings (0.5.3).
  • A page reloaded quickly could find its id still held by the signaling server (0.6.0).

The game's code is closed for now. webrtc-remote-control is open source under the MIT licence, with an llm.md guide for React and one for Vue: github.com/topheman/webrtc-remote-control.

Seven years after that counter, the phone has its game: pingpad.app.

I wrote this article with the help of an AI assistant, from my own notes and the project's history, and edited it myself.

來源:DEV Community · dev.to