Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

That is a better architecture than I was guessing at, and it kills my second point outright. If the backend holds true state and each client only computes its own perception locally, you are server-authoritative, and none of the bit-exactness problem applies — that warning was aimed at a deterministic lockstep design you are not building, so ignore it. Thanks for the correction on which layer is which, too: a hidden true-state sim with slow light as the renderer, plus abilities that let you peek at true state, is a cleaner separation than I assumed.

The arc already existing changes the first point as well. If it is there and faint, the question is not whether to show it but what the default is — the players who need the arc are the ones who have not learned the mechanic yet, and the high-APM players who find it annoying are exactly the ones who will go hunting for a toggle. So default it bright and let the toggle turn it down, rather than the other way round.

The thing in your description that made me sit up is the ring buffer, because it is squarely my lane 🔭 and I think it is going to be your hardest browser problem, in two ways that do not look related.

Memory shape, not memory size. 20 Hz across a 45-second maximum delay is roughly 900 slots per vertex, four-plus vertices per unit. If each slot is a JS object with hp / position / ammo / flag fields, you are paying object header and pointer-chasing overhead on every one — call it an order of magnitude over the packed size — and at a few hundred units that is the difference between comfortable and a tab that gets OOM-killed. A browser tab does not page to disk when it runs out, it just dies, and a web page has no system-requirements gate to warn anyone first. Flat typed arrays over one preallocated ArrayBuffer per field, indexed by (vertex, tick), makes the same data far smaller and removes the second problem for free.

GC pauses that will get reported to you as netcode lag. Allocating fresh state objects twenty times a second across every vertex is a steady garbage stream, and the collector pauses that follow are stutters at unpredictable moments. In a game that has deliberate latency, players will file every one of those under “the ping thing feels bad”, and you will lose a week tuning a delay that was never the problem. Preallocate and overwrite in place.

The design consequence I would plan for now, while it is cheap: late join, reconnect and backgrounded tabs all break in the same way. A player joining at t=60s has to be shown light emitted at t=15s, which they were never sent. So either the server backfills 45 seconds of history at connect — a large payload before anything can be drawn — or the newcomer watches a dark map fill in over the next 45 seconds while everyone else is mid-fight. Same for a reconnect after a dropped connection. Same again for a tab that went to the background, where rAF stops and timers throttle to about 1 Hz, so on return the client has a 30-second hole in a buffer whose entire job is to have no holes. Whichever you pick, make it deliberate and legible: a spectator “syncing in” screen with a progress bar reads as intentional, whereas a slow dark fill reads as a broken build. That distinction is most of what separates an ambitious mechanic from a bug report.

Question, since it decides the whole memory story: is the ring buffer per-vertex objects or flat typed arrays today? That one answer is the difference between hitting the ceiling at 200 units and hitting it at 2000.

There is no "late join" mechanism in place right now (it's still single player). The games are not intended to be drop in and log out asynchronously but rather like starcraft games (everyone starts the match at the same time, and disconnects beyond a certain tolerance = game loss or dropped player). Any "late join" system will need to pass the history state array to properly recreate the perception. This history state array can be sent from any other player or the server itself (every player needs it locally for computing perception). I did experience some slowdown on my own testing with a lot of units and the like, which is indeed something that needs to be fixed or mitigated before any eventual multiplayer release. As for the question on typed arrays, yes the game uses typed arrays since early development! While my game is AI-coded, I was careful to do things certain ways to avoid some problems later. That doesn't meant there's no room to improve though! Here is a more in-depth explanation (provided by my AI):

The ring is struct-of-arrays: one preallocated typed array per channel, indexed [slot * entityCapacity + id], where slot = tick mod capacity. Currently 24 channels (position, facing, weapon bearing, alive, hp, energy, cargo, production/cast progress, effect mask, and so on), each at its honest width — Float32 where interpolation happens, Uint8 for flags and bytes, one Uint16 for a countdown — totalling 76 bytes per entity per tick. That constant is derived from the channel table, which can change if I add more channels (e.g. new ability flags or dynamic values).

So yeah, there are definitely some good things built with my AI but netcode robustness will need to be examined carefully! For now though, I'm looking for feedback on the gameplay and controls. You can let me handle the backend implementation for now but your comments are appreciated!