What this project is
Snake is a solved game. Everyone's built it. So I asked: what if it was 3D? What if it was multiplayer? What if your score actually persisted and competed against everyone who'd ever played?
The result is a 3D Snake game where players join via room codes, compete on a Three.js-rendered 3D grid, authenticate with Google OAuth, and post scores to a global Supabase leaderboard. It's the kind of thing that starts as a weekend project and turns into three weeks of "just one more feature."
It's also the project that made me properly understand real-time systems. Not "I read about WebSockets" understand — "I watched two browser tabs desync and had to figure out why" understand.
How I built it
The architecture splits into three layers:
Three.js for the 3D grid was the part I was most nervous about. I'd never done 3D in the browser before. The learning curve is real — Three.js has its own concepts (scene, camera, renderer, geometry, material, mesh) that don't map cleanly to anything in React-land. Once you internalize that the render loop is separate from the React component lifecycle, it starts to click.
The snake segments are boxes (BoxGeometry) with an emissive material so they glow slightly. The food is a small sphere that pulses using a simple sine-wave animation in the render loop. The camera is positioned isometrically and pans smoothly when the snake approaches the edge of the grid.
The WebSocket server runs on Node.js and is the single source of truth for game state. Every player sends their input (direction changes) to the server. The server runs the game tick at 100ms intervals, resolves collisions, updates positions, and broadcasts the new state to all clients in the room. Clients just render what the server tells them.
The hardest bug: Two players in the same room would occasionally see different food positions. Root cause — I was initializing food position on the client side with Math.random() instead of having the server generate and broadcast it. Classic distributed state mistake. Always let the server own the state.
Room codes are just random 4-character strings (e.g. "X7KP") generated on the server when a player creates a room. The server maintains a Map of room code → game state. When a second player enters the same code, they join the existing room. Simple, but it makes the game feel like a real product.
Supabase handles two things: Google OAuth (via Supabase Auth) and the leaderboard table. The leaderboard is a simple Postgres table with player_id, display_name, score, and timestamp. At game over, the client posts the score to a Supabase edge function that validates it server-side (you can't trust clients with scores) before writing to the DB.
What's powering the game
What I'd do differently
Real-time multiplayer is a discipline, not a feature. The naive approach (sync everything, all the time) doesn't scale. Even at 2 players the latency was noticeable when I wasn't running the server locally. I'd invest more time upfront in a proper game loop with client-side prediction.
Three.js + React is friction. React wants to own the DOM. Three.js wants to own a canvas and run its own render loop. Getting them to coexist gracefully took more effort than expected. If I built this again, I'd look at react-three-fiber which is designed to solve exactly this integration problem.
The OAuth flow with Supabase was genuinely painless. Google OAuth + Supabase = about 45 minutes of work including reading the docs. Highly recommend Supabase Auth for any project that needs social login without running your own auth server.
Room codes were a late addition but might be the best feature. They made the game immediately shareable — you can just text someone a 4-letter code and they're in your game. No account required just to join. Simple beats clever every time.