← All articles

What Building a Game Taught Me About Real-Time Systems, Latency, and Product Evolution

July 26, 2026 · Browser Games, TypeScript, WebSockets, Game Development, Distributed Systems, Preact

What if starting a real-time multiplayer game were as simple as opening a link?

That question became Tanks, a fast, top-down battle game that runs entirely in the browser. There is no app to install, account to create, or lobby to navigate. Open it on a desktop or mobile phone, choose a mode, generate a battlefield, and start playing.

Tanks may look like a small browser game, but underneath it is a practical study in distributed systems, network performance, and product iteration. Making distant players feel as though they share one battlefield requires a trusted source of truth, responsive controls, careful physics, efficient communication, and graceful recovery.

Three ways to play

Invitation without friction

For a two-player match, the first player creates a game and receives a private link backed by an unguessable token. The link can be shared with a second player, who opens it and claims the remaining tank. Separate tokens map to isolated sessions, allowing many unrelated games to run concurrently.

The client is written in TypeScript using Preact and Vite. Preact manages the menus, invitation flow, status display, rules, sound, and responsive controls. A Canvas 2D renderer draws the battlefield, walls, tanks, flags, shields, and projectiles at the browser’s display rate.

Fingerprint-named assets are cached across a global edge network. Players are routed through a nearby point of presence, which can serve files close to the device. Live game traffic uses secure WebSockets.

High-level architecture diagram for Tanks

At the center is a State Object named GameSession. Each invitation token receives one, giving the match a strongly consistent authority over players, connections, positions, projectiles, lives, flags, countdowns, and victory.

Lite database checkpoints restore a session after it leaves memory. When nothing moves or flies, it stops scheduling physics work. Many games can coexist without a global game loop or one match blocking another.

A shared, authoritative physics engine

Browsers never announce “I moved here” or “my shot hit.” They send intent: movement axes, ordered inputs, and fire requests. The session decides positions, collisions, damage, flag state, and victory while advancing the world at 30 fixed steps per second.

The shared TypeScript physics engine generates symmetrical arenas with safe spawns, cover, and a verified route between sides. It resolves tank and wall collisions and applies mode rules. Swept tests prevent fast projectiles from jumping through objects between steps.

This shared engine made expansion easier. Capture the Flag and Single Player reuse the same simulation, and the computer opponent produces ordinary input under the same rules as a person.

Making network latency feel smaller

Distance still creates network delay. The goal is to stop it from controlling the player’s hands.

The browser predicts movement immediately, so a drag or keypress moves the local tank on the next animation frame. Authoritative snapshots smoothly correct small differences and snap only when disagreement is large. The network exchanges compact state, not display frames.

Joystick data is coalesced. Shots and releases are immediate, while rapid movement events are combined and capped at 20 messages per second. This reduces exchanged data, prevents high-refresh-rate phones from flooding the connection, and preserves responsive control.

Short input leases stop stale commands: if held movement is not refreshed, the server treats it as released. Ping-and-pong messages measure round-trip latency, while reconnect attempts use exponential backoff with jitter.

From driver controls to direct movement

The first version used the driver’s point of view: forward and backward followed the tank’s facing direction while left and right rotated it. On a phone, players had to judge heading, accelerate, track an opponent, and manage fire on a small screen.

The first improvement replaced separate movement and rotation buttons with a virtual joystick. Testing then exposed a deeper mismatch: a modern drag control feels as though it should move directly toward the finger.

The next iteration introduced absolute movement. Up means north, down means south, and left and right mean west and east regardless of the tank’s previous orientation. The tank faces its latest movement direction, keeping firing predictable without adding a second aiming control.

The joystick gained analog direction, proportional speed, a dead zone, and a neutral position. Its side preference survives reloads. Desktop players can use WASD, arrows, and spacebar or drag the joystick with a mouse.

Pause, recover, and continue

If a player reloads or disconnects during a two-player round, the server neutralizes input, clears queued and in-flight projectiles, and pauses the match. When the player reconnects, both browsers receive the authoritative state and a fresh countdown begins. Old sockets cannot replay delayed commands, and a normal refresh never costs a life.

The lesson from Tanks is that responsiveness is not one optimization. It emerges from nearby asset delivery, persistent WebSockets, local prediction, compact state, bounded message rates, fixed-step physics, safe reconnection, and controls refined through real use.

What began as a browser game became a reminder that the most satisfying technology often disappears into the experience. When the architecture works, two people simply open a link, move their tanks, and feel as though distance is no longer part of the game.

Play Tanks and explore the code

Play Tanks or browse the source code on GitHub.