What Inspired Us Growing up playing arcade fighters like Street Fighter and Tekken, we always wanted to recreate that experience — two players, one screen, pure competition. The twist: what if you could do it entirely in a browser, with a real stranger on the other side of the internet? That question became this project. How We Built It The architecture is a hybrid of two worlds — a modern React app and a classic game engine working in tandem. The handoff pattern:

TanStack Start handles routing and the lobby UI Once a match is confirmed, React hands rendering control entirely to Phaser 3 During the fight, Phaser runs local physics and animation at full speed

The multiplayer layer: Each client simulates its own physics locally and broadcasts its state to a lightweight Express + Socket.io relay server. The opponent's position is then interpolated client-side to smooth out any network jitter. The sync formula we use for interpolation: Prender=Pprev+(Pnext−Pprev)×tnow−tprevtnext−tprevP_{render} = P_{prev} + (P_{next} - P_{prev}) \times \frac{t_{now} - t_{prev}}{t_{next} - t_{prev}}Prender​=Pprev​+(Pnext​−Pprev​)×tnext​−tprev​tnow​−tprev​​ Where ( P ) is position and ( t ) is the timestamp of each received snapshot. What We Learned

How to hand off rendering control between React and a Phaser canvas mid-session without tearing or memory leaks Building a rollback-friendly game loop — keeping local state authoritative while reconciling opponent data The difference between a relay server (what we built) and a true authoritative server (what a production game would need) Socket.io room management for matchmaking and graceful disconnect handling

Challenges We Faced Latency hiding was the hardest problem. Raw socket updates arrive unevenly — sometimes 3 in a row, then a gap. Interpolating between the last two received snapshots, rather than the latest one, made movement feel smooth even under poor network conditions. Phaser + React coexistence was trickier than expected. React wants to own the DOM; Phaser wants a raw canvas. Getting them to share a page without React re-renders destroying the game canvas required careful use of refs and lifecycle management. State synchronization on disconnect — if one player drops mid-fight, the other client needed to detect this cleanly and return to the lobby without crashing the game scene.

Built With

Share this project:

Updates