ALIENFALL.IO

Devlog

How you build a real-time multiplayer arena game that has to run in a browser, hold up to 70 players per arena, and stay pleasant on a mobile connection. This log describes the techniques in place, what they cost, and what they returned — with the measurements.

The stack is deliberately plain: TypeScript end to end, PixiJS for rendering, Node and Colyseus for the authoritative server, SQLite for the data that matters, Redis for what has to be shared between processes. No in-house engine, no exotic dependency. The interest is not in the stack — it is in what you do with it when the margin is thin.


The network and how the game feels

25 July 2026 — Authoritative server, client-side prediction

The server simulates the world at 60 Hz and broadcasts state 20 times a second. The client does not simply wait: it predicts its own actions immediately and interpolates other players between snapshots. That is the reference design, publicly documented for a long time — Valve's Source Multiplayer Networking, the GDC talks on Overwatch's network architecture, Glenn Fiedler's articles on snapshot interpolation.

The structural choice is elsewhere: client and server literally run the same simulation code. Physics, collisions and rules live in a shared module imported by both. Two implementations of one rule always drift apart; a single one cannot.

31 July 2026 — A protocol that counts its bytes

At 20 snapshots a second with dozens of visible entities, the wire format is the network budget. Three techniques stack up:

All three rest on a strictly frozen field order: insert a field in the middle and the reader on the other side shifts everything after it, reading perfectly plausible — and wrong — numbers. The contract therefore carries a version number, and fields are only ever appended.

5 August 2026 — Interest management: send only what is visible

Each client declares a view rectangle sized on its screen, and receives only the entities and tiles inside it. An entity leaving the view is forgotten client-side and comes back complete on its next entry — no delta applied to a state you no longer hold.

That is what keeps a crowded arena readable on the wire: network cost follows what the player sees, not what exists.

15 August 2026 — Telegraphed attacks, judged in the present

For dodging to exist, two things are needed, and they are coupled:

The second point deserves spelling out, because it runs against common practice. Many shooters rewind state to judge the hit at the instant the shooter aimed — Valve has documented that trade-off since 2001 under the name favor the shooter. It is excellent when precision is the heart of the game. In an arena where movement is the game, it amounts to punishing a target who reacted correctly: by the time she sees the shot leave, the server has already ruled on a past frame.

The principle we settled on is not original, and that is a strength: Riot publicly documents its counterplay doctrine — an attack the opponent can answer. Telegraph durations in mainstream games give a reference: on the order of a second for Clash Royale, a few seconds for Bomberman. We sit deliberately below that: the arena is fast and the floor gives way, a one-second warning would make every attack useless. The departure is knowing and written down, and the chosen value is treated as a floor that is not lowered without fresh measurement.

16 August 2026 — Freeze and catch-up, tuned as one system

When the predicted position drifts too far from the server-confirmed one, the client brakes the local character: that is what stops you running into empty space and being snapped back.

The non-obvious part, and the heart of the tuning: that brake itself widens the gap reconciliation must then absorb. Freeze and catch-up are not independent parameters — tuning them separately does not converge.

What is in place, as one whole: a tolerance before freezing, a gradual resume ramp, a speed-capped catch-up, a soft re-join always armed after a slide, and extrapolation bounded in distance as well as duration. Without that last bound, an extrapolated shove throws a ghost three tiles into a hole.

Validated on a gamepad over a 4G tethered connection, not only on the bench.

16 August 2026 — Connection indicators everyone can read

Three bars show connection quality. The design choice is deliberate: the shape carries the information, not the colour alone. Roughly 8% of men do not distinguish the green / amber / red trio; an indicator resting on it alone does not inform everyone.

24 August 2026 — Predicted falls, and a shadow state

The floor wears out and gives way. Until this date the client did not predict its own fall: you visually landed on emptiness, and death arrived "retroactively" half a ping later.

The technique in place: the client replays the server's support test identically. The function deciding whether a character is held up is exported from the shared module and called on both sides. The visuals follow immediately, while death itself stays server-side — the prediction decides nothing, it displays.

On top of that sits a shadow state for tiles: the client tests support against the freshest state it has received, rather than the dated one it is currently interpolating. One implementation of the rule, two readers: if the support rule ever changes, prediction follows on its own.

25–26 August 2026 — Leeway: promise less to the shooter, deliver exactly to the target

A player who gets hit wants to understand why. That is what leeway is for: a bonus to the judged body size granted to the target, computed from the attacker's compensated display delay and bounded to a narrow range. It applies identically to the shove and to the three weapon fronts — one implementation, all readers.

The elegant part is how the information is split:

That is a publicly documented design rule (Riot, WildStar): better a hit that lands when you had given up on it, than a telegraphed hit that does not.


Performance and machine cost

12 August 2026 — Measure before optimising

A measurement campaign was set up before any optimisation, with simulated clients and server instrumentation. Verdict: cost did not grow linearly with player count but with its square, and a single loop carried more than half of it.

The result is not the point here — the order of operations is. Without that campaign the effort would have gone elsewhere: perceived cost and measured cost did not point at the same code.

14 August 2026 — Spatial indexing of the broadcast

Broadcasting naturally compares every player to every entity. Entities are now indexed by row of the hexagonal grid, and the broadcast sweeps only the rows covered by each client's view rectangle. Cost follows the area being looked at, not the total population.

14 August 2026 — An allocation-free simulation path

The path executed 60 times a second was made 22% lighter, measured, by textbook techniques applied systematically:

That last point pays the most in JavaScript: what you don't allocate is what the garbage collector won't come back for — and it always comes back at the worst moment, which is during a heavy frame.

14 August 2026 — A byte budget on the critical path

The project holds itself to a weight ceiling on the critical path: what must be downloaded before the menu appears. Three techniques hold it:

The budget itself was recalibrated to measure the real critical path — the entry graph actually loaded — rather than the whole shipped folder, which also holds things that never reach a given player.

20 August 2026 — Cadences counted in time, never in frames

Saving work by refreshing some animations "every other frame" is tempting. At 60 frames per second that is 30 Hz: imperceptible. At 30 frames per second it is 15 Hz — while the local character stays perfectly smooth.

The rule applied since: if what you skip carries time, the guard is counted in milliseconds. The frame counter is still useful for spreading load across entities; it no longer decides on its own to skip an update.

Two settings of the same nature landed at the same time: the playback clock no longer advances on a delta capped by the render engine (it fell behind real time until a hard re-sync), and a frame rate cap stops a 144 Hz screen making the client manufacture the saturation it then suffers.

20 August 2026 — Database writes leave the hot path

Statistics and leaderboard writes used to run inside the simulation step. They are now batched in a transaction and moved out of it, with short lock timeouts on non-critical databases — a lock taken on a shared database can no longer delay an arena.


Server architecture and scaling

31 July 2026 — One process per core, and a shared directory

Node is single-threaded. Running one process per core is not enough on its own: arenas have to know about each other across processes, or players scatter into worlds that ignore one another.

The central piece is a shared directory in Redis, in three inseparable parts: presence, routing, public address. The point of vigilance, raised during the preliminary research and confirmed since: if one of those three is missing, the framework silently falls back to its in-memory versions. Nothing fails, nothing warns.

Hence a design rule applied everywhere since: in production, a missing dependency fails the startup. Never a silent fallback.

31 July 2026 — Warm sessions

Creating an arena costs a few milliseconds. Paying them while a player waits is free latency. An arena is therefore prepared in advance, ready to receive on arrival.

The detail that matters is a security one: the "warm session" flag cannot be requested by a client. It is only accepted alongside a token derived at startup and never transmitted — otherwise anyone could spawn immortal arenas.

31 July 2026 — A measured capacity model, and its counter-check

Rather than a capacity quoted by feel, it is measured: cost of a playing player, cost of an empty arena, cost of an arrival. The resulting model is then checked against an independent measurement point0.2% apart.

Measurement also corrected an over-pessimistic estimate: the test clients spent their lives reconnecting, which loads a server far more than a real population. We had been measuring the bench, not the game.

31 July 2026 — Checks live where the decision is made

The per-arena player cap is enforced where the framework allocates the seat, not in the join handler that runs afterwards. A guard placed after the decision guards nothing: it is a general rule, and it is verified by a dedicated bench that starts dozens of players at once.


Data, economy and integrity

26 July 2026 — A ledger, not a counter

The game's currency is not a number stored somewhere. Every movement writes a line to the ledger: round earnings, purchase, reward, adjustment. The balance is their sum.

That costs more than a counter, and that is exactly the point: a discrepancy becomes detectable. A check verifies that the ledger sum equals the total of all balances; a difference is not an approximation to tolerate, it is a defect to find.

31 July 2026 — Idempotence guaranteed by the database, not by code

Every currency attracts double-spends: two simultaneous requests pass the same check before either has credited.

The guarantee therefore does not rest on a variable in memory — that survives neither a restart nor the neighbouring process. Each round gets an identifier when it starts, and a partial unique index makes a second credit impossible at the database level. Check and credit sit in a single transaction.

Proven against real concurrent processes: six simultaneous requests, one credit, five refusals. And proven by removing the guard: three requests out of six credited.

22 August 2026 — An outbox that survives outages

The game's emails do not leave from the routes that trigger them. They are deposited in a database, and a single process sends — the one holding a lease arbitrated by the database, which expires on its own. Three reasons: a route must answer immediately; several processes talking to the same service multiply duplicates; and an outage of that service must lose nothing.

Retries are spaced out, then abandoned loudly. With no transport configured in production, mail waits and the log complains — it does not vanish.


Security

This section deliberately stays at the level of principles: describing your own thresholds precisely is handing them over.

21 August 2026 — Argon2id, and a measurement before choosing

Passwords are hashed with Argon2id, parameters above the OWASP floor, a per-password salt and a pepper stored outside the database. A stolen database is therefore not enough.

Two points were settled by measurement rather than habit:

A dedicated bench measured the impact on the game loop: synchronously, hashing costs enough to lose a frame and a half for the whole arena; asynchronously, the block drops below the noise floor. The game therefore did not need a dedicated process — a decision taken on a measurement, not an intuition.

21 August 2026 — Not saying who exists

An honest login page answers faster when the username does not exist, because it has nothing to hash. That gap alone is enough to enumerate accounts before testing them against leaked passwords.

The countermeasure is a decoy hash: an unknown username costs exactly the same work as a known one. The check is timed and automated, as a median over several alternating series — a protection of this kind is lost at the first refactor if nobody watches it.

22 August 2026 — What holds everywhere else


Interface, content and accessibility

2 August 2026 — A design system, and a single source of colour

Every colour in the game comes from one file of tokens. CSS reads them directly; the renderer reads the same tokens converted to numbers. Static pages — help, legal notice, this log — load a theme built from that same source.

Four semantic families are frozen and never reassigned: bonus, activatable, danger, currency. The action green lives deliberately outside those four: it marks what can be clicked, and nothing else.

6–7 August 2026 — Sound, cut at the audible attack

A recording never starts exactly at zero. Measured on our own files: up to 125 ms of silence before the sound's attack — more than seven frames at 60 Hz. You don't hear it — you feel it: the sound seems soft, behind the picture.

The processing chain therefore trims automatically, and the choice of cut point is the whole subject: cutting at the first non-zero sample gains nothing (a sound starts with an inaudible rise); cutting at the peak beheads the transient and produces a click. It cuts at the audible attack, keeps a few milliseconds ahead of it, and applies a very short fade. It refuses the file if the cut would land in solid sound.

8 August 2026 — A check that refuses hard-coded colours

The "no hard-coded colour" rule was stated everywhere and verified nowhere. An automatic check now enforces it at every commit: any colour literal in code or a stylesheet is refused.

It is calibrated, not blunt: it explicitly tolerates neutrals — grey is not palette, it is light — and a handful of declared sources, each with its reason written inside the tool itself.

26–27 August 2026 — The game's numbers, derived from the code

The values shown in the help pages and the FAQ — ranges, radii, durations, caps — are not copied. They are derived from the code when the pages are generated: a rebalance propagates on its own.

The mechanism is fail-closed: an unknown measurement name fails the build, and the pre-commit hook regenerates the pages as soon as a source of numbers moves. The same holds for text: everything a player reads comes from a bilingual catalogue, and an unknown key fails the build — never a gap in production, never an English sentence forgotten inside a French interface.


Method and tooling

What follows is not a preference: it is what makes the decisions above verifiable.

29 July 2026 — Guards that refuse, and are proven by breaking them

The project does not rely on vigilance. A set of automatic checks refuses a commit that breaks a rule: compilation, module boundaries, palette coherence, hard-coded colours, dead documentation links.

Each has been proven by deliberately breaking it, and the result of that counter-test is written next to the check. The reason is lived experience: an analysis tool once proudly reported "no violations" — after analysing zero files. Hence a simple rule: always look at the number of items analysed, never only at the verdict.

12 August 2026 — Measure the dips, not the average

A stutter cancels out in an average. A game averaging 59 frames per second can feel unpleasant if the dip falls to 12 for a tenth of a second — and the dip is what the player feels.

Measurements therefore keep the minimum and the number of episodes below a threshold. On the GPU, time is read through a hardware timer query rather than estimated from the frame loop: a long frame can be waiting on the GPU without the GPU doing any work.

20 August 2026 — Try to refute, not to confirm

Every analysis finding goes before a counter-review whose instruction is to demolish it. What survives is kept; the rest falls — and some does.

Two recent examples. A documentary sweep concluded, with sources, that a network coalescing algorithm was adding latency to our connections: false, verified inside the dependency's code, which disables it explicitly. And a GPU cost quoted at around 70% turned out to be nearer 27% the same day, once measured in the right place.

An audit that confirms everything it goes looking for has audited nothing.

Ongoing — Write the trap once, pay for it once

Every defect found is recorded where the next person will look for it, with its date, its symptom, and the wrong reasoning that led there. The project carries several dozen, filed by area. This is not comfort documentation: several of those notes have prevented the repeat of a costly defect, weeks later.


What's next

The game is playable and it keeps moving. Open work covers the mobile experience, progression, and continuous balance tuning from player feedback.

A technical question, a proposal, or an interest in working together? The contact page is there for that.