This article is a technical discussion of how the 2024 remake was built. You can play the game online, or read an older article about the 2004 version.
In 2022, I found an archive DVD with the C++ source code, sprites, sounds and music of my 2004 video game Darklaga Cannonball Symphony. It could no longer be built (due to missing proprietary dependencies), so I decided to re-implement it as a late evening hobby project. As an additional challenge to porting a game I wrote when I was 20 years younger, I decided on two additional constraints:
- Only use technologies that can be expected to still be present (and backwards-compatible) 20 years from now.
- The game should be quick to download and easy to install on as many open platforms as possible.
The complete source code of the remake can be found in this GitHub repository, and can be played online here.
Easy to install
The remake is a web page that runs directly in the browser. It consists of a single HTML page darklaga.html
which downloads a single binary blob darklaga.bin
containing the JavaScript code, the sprites and the sounds.
Only two permissions are requested:
-
To run in full-screen mode with
requestFullscreen()
. -
To play audio with an
AudioContext
.
Both are non-essential (the game can be played without full-screen mode and without audio), and on most platforms the two follow the same logic of being denied until the user first interacts with the page, after which they are automatically granted. For this reason, the initial loading screen remains visible until the first interaction, at which point the game begins with both permissions granted.
The only essential dependency that might not be 100% portable is WebGL, which is used for performance reasons. To ensure the widest browser support, I avoided using anything outside of WebGL 1.0 (from 2011), and steered clear of the recent but unsupported WebGPU.
Quick to download
When publishing anything online, it is a question of respect to have as few HTTP requests as possible, and to keep the downloaded data as small as possible.
Like the rest of my website, the game page does not make any third-party requests, show any ads, or run any tracking code. This should be the standard.
The entire remake weighs 1433 KiB (1042 KiB compressed), divided as follows (one dot is 4 KiB):
2.04 KiB — the HTML and CSS of darklaga.html
.
1.79 KiB — the JavaScript bootstrap code inside darklaga.html
. This code initiates the retrieval of darklaga.bin
, then extracts the JavaScript code contained inside and evaluates it.
36.30 KiB — the “loading” image data, a 240×320 PNG image embedded as a base64 data-URL inside darklaga.html
. The base64 encoding significantly increases the file size (from 26.50 KiB), but most of it is clawed back by the compression.
327.15 KiB — the complete JavaScript code of the video game, found at the beginning of darklaga.bin
. This code is not minified ; while using a modern minification library would bring the size of this section down to 139 KiB (a reduction of over 50%), the entire darklaga.bin
blob is compressed, and JavaScript compresses very well: 54 KiB compressed, 37 KiB minified and compressed. Increasing the complexity of the build system for 17 KiB was not worth it.
Even worse, several minifiers I have tested actually broke my JavaScript code! Bugs in minifiers happen, and the industry standard for dealing with those is to include the minifier in CI, to detect if a change to the code, or a migration to a newer minifier version, breaks the project. I am fine with this approach in my day job, but it does not satisfy my constraint for the remake to still be usable 20 years from now.
415.36 KiB — all the sprites, textures and fonts in the game, combined into one large 1024×1024 texture atlas, encoded as a lossless PNG image file, at the end of darklaga.bin
.
Ironically, the above 512×415 image, which is a shrunk down version of the atlas, weighs 443 KiB! This is because the choice of palette, as well as the absence of partially-transparent pixels, greatly help the PNG compression, but did not survive the downsizing.
18.09 KiB — mapping information for the texture atlas. There are a total of 772 items in the texture atlas, and each of them has six associated float32 values: top, left, right and bottom normalized coordinates within the atlas, and width and height (in pixels) for rendering.
13.70 KiB — all the levels in the game, taken as-is from the 2004 game, and included in darklaga.bin
. These use a very simple binary format with 6 bytes for every enemy that spawns in that level (meaning, Darklaga Cannonball Symphony contains 2353 enemies across 16 levels).
584.99 KiB — all the sounds and music in the game, compressed as MP3 (from 1.29 MiB of original WAV files) and concatenated together in darklaga.bin
. Surprisingly, the largest asset category in the game is actually the audio!
Anecdote! The entire darklaga.bin
is mapped as an ArrayBuffer
, and views over subsets of that buffer are then passed to the various APIs (TextDecoder
for extracting the JavaScript, URL.createObjectUrl()
for the WebGL texture atlas, etc.) but the APIs used for sound, AudioContext.decodeAudioData()
, detaches the buffer passed as argument! Which, being a view over the entire darklaga.bin
, caused all of it to be detached as well, making it unusable for any operation. This is a tiny detail that is not documented outside of the specification. The behavior is in fact sensible—if you don’t need the buffer anymore, it allows
.slice(0)
) and let the clone be detached instead—but it will confuse people, and it takes much prodding to get ChatGPT to acknowledge that it is even happening. I may one day be replaced by LLMs, but today is not that day. Thank you, obscure JavaScript APIs!
Like the rest of nicollet.net, the game is served as two static files from nginx. To save on CPU usage, I use option gzip_static on
, so that if the browser indicates support for compressed transfers (by including header accept-encoding: gzip
in the request), nginx will respond with the already-compressed darklaga.html.gz
and darklaga.bin.gz
files, instead of compressing darklaga.html
and darklaga.bin
on the fly. This also means that I can upload compressed versions with a stronger but more time-consuming compression level.
I do not plan to include a CDN in front of my server, despite the obvious immediate benefits for myself. To keep the free web alive, I have a duty to make your browser connect to my server without a huge organization in the middle having the power to treat either of us as a robot, a spammer, or a criminal.
Still running 20 years from now
I’ll bet that current web technology—HTML, CSS, JavaScript—will still be available in 2044. It’s possible that some JavaScript features will be deprecated, and so I decided to minimize the number of features I use. Aside from standard JavaScript (functions, arrays, strings, numbers and classes), the unusual features are:
-
The
ArrayBuffer
and the typed arrays (Int32Array
,Float32Array
, etc). Given that these types are now used to interoperate with web requests and WebAssembly, they should be safe to use. - WebGL, specifically the WebGL 1.0 features only. Whether WebGL as a whole will be replaced by WebGPU remains to be seen, but the 1.0 fea