AirHockey
A real-time multiplayer air hockey game for the browser, networking built from scratch. It won me a teddy bear and a banana.
AirHockey is a real-time multiplayer air hockey game you play in the browser, built by a team of four students at Carnegie Mellon University, a mix of graduate and senior-year undergrads. The assignment was simple enough: build a web app with Python and Django in five weeks. I had just wrapped Hygiene Hero Cup and was feeling ambitious (the polite word for cocky), so a boring CRUD app that nobody would enjoy building or playing felt like a waste of five weeks. I was already deep in NOMU at the same time, so the obviously reasonable move was to add a from-scratch multiplayer game to the pile. I pitched the idea to the team and we were off. I owned the idea, the art, the design, and effectively all of the game and networking code.
Note: There's no live link to play because a running server costs money, and my bank account and I are not currently on speaking terms. Happy to walk through it or share it on request, and sponsorships are warmly welcomed.
Choosing the stack and resisting bloat
The first week went to deciding the tech stack and setting up the project. We picked the Phaser game framework for the gameplay itself. Since a big part of the point was to learn, I decided against any prebuilt networking library and committed to writing the networking layer from scratch, both to understand what actually happens under the hood in a multiplayer game and to brush up my async programming. For the same reason, and because we already had a game framework doing the heavy lifting, I skipped frontend frameworks entirely and wrote the game frontend in plain HTML, CSS, and JavaScript. I didn't want the project puffing up like a pufferfish. The website around the game stayed in Django, since that was the assignment's requirement.
The first prototype
The second week I finally got my hands on Phaser, which I had never touched before, and stood up a working prototype using WebSockets to carry the game updates. Alongside the code I drew most of the art from scratch in Figma, and I genuinely miss those days: nudging a single point on a bezier curve until it sat just right, agonizing over the perfect color for three hours, actually making art that has a soul instead of pulling it from some machine trained on a pile of data that makes everything look the same. I learned an enormous amount that week: opening and holding a TCP connection, synchronizing state between clients, keeping paddle positions, puck position, and Phaser's puck physics in agreement across the network, and drawing clean vector art in Figma while wrestling it into the exact export format I wanted. It was one of the most rewarding weeks during my Master's, and one I plan to write about in more detail in a future post.
The latency problem
Playing the prototype surfaced an obvious problem: WebSockets run over TCP, and TCP is too slow for a game this fast. TCP is built for reliability, so it guarantees that packets arrive in order and retransmits anything that goes missing. That is exactly what you want for a chat message and exactly what you don't want for a puck flying across the table many times a second. All that bookkeeping added enough latency that syncing positions and physics felt sluggish, and for a fast-paced hockey game, sluggish is fatal.
Going low-latency with WebRTC
So I went looking for something faster and landed on WebRTC, which can send data over UDP and trade most of TCP's guarantees for speed. I moved the latency-sensitive updates (paddle positions, puck state, physics) onto WebRTC and left everything else on WebSockets. Getting a UDP connection running between two browsers turned out to be a small odyssey. I learned how peer-to-peer connections are actually established: STUN servers to discover each peer's public address, TURN servers to relay traffic when a direct connection isn't possible, ICE to negotiate the best route between the peers, and a signaling channel to exchange all of that in the first place. The end result was a hybrid: WebRTC carrying the fast game state, WebSockets handling scores, the clock, and chat, and a fallback path so the game keeps working if the WebRTC connection drops.
Polish, pucks, and shipping
The final week was polish: sound effects, background music, a leaderboard, and the smaller features that make a game feel finished. I also added extra pucks that pile on as the clock runs down, up to ten of them once the final ten seconds hit, a direct lift from the arcade air hockey tables I loved as a kid. As a bonus, ten pucks flying around at once was a good stress test: it proved the WebRTC approach held up fine under much heavier packet updates. For deployment we swapped WSGI for ASGI so the server could handle WebSockets, and put it on an AWS instance to demo in front of more than two hundred classmates and professors. It held up: we ran around ten concurrent rooms without trouble, earned an A+, and took home the best project award. The prize was a teddy bear and a banana, which I still consider a fair trade for five weeks of work.
Reflection
Looking back, AirHockey is one of the most rewarding things I've built. It stretched me well beyond programming into design, art, and the less glamorous work of managing scope and a timeline. If I did it again, I'd aim smaller at the start: we originally planned both 2-player and 4-player modes and only shipped 2-player, which is the kind of ambition that's better caught in week one than in week four. Even so, I'm proud of what four people pulled off in five weeks, and it left me convinced that stepping outside your comfort zone is where most of the real growth happens.