Not so long ago, Habré already wrote about the 10K Apart contest
I want to introduce your attention to our work
for this contest, which we did in the evening with a private_face
in the evenings for two weeks: a dungeon-crawler
style adventure game called “Fontanero” ( Spanish
- Random connected generator.
- Disclosure of the map as it passes, the disclosure of rooms.
- 13 monsters c primitive AI and the algorithm for finding the path.
- Food, drink, 4 different spellbooks: vision, heal, cure and genocide.
- The effects of poisoning and blinding.
- Mini-game with the rotation and repair of pipes
- And even a monstrous boss at the end.
All this variety in three files with a total weight of 10230 bytes. If you ever find a 1.44MB diskette on the mezzanine, then you can write 144 such games to it.
After we decided on the nature of the application (adventure), genre (dungeon crawler in the style of nethack) and the setting (the plumber goes down to the basement to fix the pipe and goes to hell), it’s time to figure out keep within 10K.
The first code sketches (card generator and playing field pattern) showed that 10K is not enough even for a third of the game. It was necessary either to drop everything or to somehow increase the available space.
As a low-level programmer, I was immediately very interested in the possibility of clamping js in a zip, in order to later unpack it at runtime. My first thought was to write my own LZSS, but a little more simple idea was born a bit later: put js in PNG, because the data in it is compressed with the same zip. (As it turned out later - we were not the first to whom this idea occurred
Having studied the technology, we checked the ability to load arbitrary code through the Canvas from the palette png, by encoding the symbols in one of the color components. The test was successfully executed in all required browsers, including IE 9 preview (which now also supports Canvas). It was a success. The eight-bit palette can store 256 colors, but only characters with codes 32-94, and \ n (10) remained after the obfuscator. Encoding them to 64 values, and not to 256, also gave significant savings (Strange, but png does not know how to compress the palette).
Zip increased the actual amount of code we could afford, but this was still not enough. Therefore, the next step in optimizing the size was the choice of obfuscator for JS.
From the free solutions on the market, we chose between the yui compressor and the google closure compiler. In the competition, of course, won the google closure compiler (hereinafter gcc), which, hand on heart, is by far the best in terms of compression, plus, it displays warnings, mostly useful.
We used gcc in advanced mode, in which it throws out unused code, renames all identifiers to one-two-letter, rebuilds branching, inline functions, and in every possible way smokes code, in pursuit of saving bytes.
However, even when using gcc in advanced mode, there is still room for optimization. First, gcc is afraid of renaming fields with standard, in his opinion, names:
left, right, top, bottom, name, type, width, height
, etc. Therefore, we preprocess our code before running gcc by renaming such names ourselves. Secondly, after obfuscation, a lot of frequently repeated words “function” and “this” remain in the code. To save money, we replaced their @ and `characters. At the same time, the loader grew a bit for reverse replacement:
replace(/@/g, 'function').replace(/\`/g. 'this')
and the code after such processing looked quite monstrous:
but it was worth it: in general, with each replacement it was a byte of 50.
When we were determined with the appearance of the game, our opinions diverged from private_face
. I offered a classic 2D view:
Vova-2 insisted on an isometry of the following type:
Therefore, then I, having scribbled a simple old-school render (everything was drawn with text inside the textarea tag), continued to write the game logic, and the map generator, and began to tinker with my isometry. He did not yet know what surprise IE9 was preparing for him. Ahahahahahahaha: ((
The implementation of the isometry was assumed as follows: rectangular blocks with textures superimposed on them were translated into an isometric projection by the CSS transformations skew and scale, after which they were absolutely positioned at the right place.
This solution required the creation of a separate DOM element for each tile, but it looked simpler and more compact (and, most importantly, more productive) than drawing on the canvas.
In addition, the use of DOM elements made it possible not only to impose textures, but also to easily write any text on the walls, which could become a killer feature for our game.
and gave quite a good picture (although it significantly slowed down in Firefox 3.6):
Life seemed beautiful and cloudless. However, igniting the idea of isometry, I forgot to do one important thing: to make sure that the CSS transformations are supported by the new Internet Explorer 9. This error cost me meaningless time spent and tons of innocently killed nerve cells in the final. Because it turned out that the only option to implement the transformation in IE9 is the Matrix filter. But the performance of such a decision was simply none.
In general, the isometry had to be abandoned.
Back to basics
After the failure of the 3D version, we decided to return to the simple tile version.
All tiles were drawn in the usual GIMP and were combined into a PNG sprite with a 4-bit (16 colors) palette, after which the latter was clamped by the pngcrush utility with the -brute key.
We managed to impart some volume to the picture by setting the box-shadow property of wall and floor tiles. However, this unexpectedly negatively affected the performance of Internet Explorer 9, while other browsers worked at normal speed. In order not to upset IE9 users with brakes, we had to add the option “Enable shadows” to the game interface, which is disabled by default in this browser. Hopefully, the release of box-shadow performance in IE9 will be improved.
The transition to 2D had a good side: the total volume of the drawing code was significantly reduced compared to the isometric version, and we used the space we finally used to add a mini-game about repairing pipes to the gameplay.
This is a brief history of how we made this game. Starting to work on this project, none of us had imagined that it would be so interesting - to work in strict constraints and saving precious bytes. We got a huge amount of fun, and 10k did an excellent job with their task - reinspire the web.
There are many other tasks left overboard of the narration that we had to solve during the work on this project. Such, for example, as a compact level generation algorithm with connectivity testing, an ultracompact algorithm for moving monsters that could walk along the corridors and would not look too stupid.
And at the end of this topic, I will cite once again a short list of optimizations that allowed us to cram almost 40K of code into 10K.
- Wrapping CSS YUI Compressor into a string, adding the result to the original .js file.
- Remove optional semicolons at the end of each rule block.
- Inserting the resulting style line to JS (I mean, packing css + js in one archive block).
- Before obfuscation: replacing fields with the names left, right, name, type, etc. to others (since GCC does not rename them even in advanced mode).
- Obfuscating GCC code in advanced mode.
- After obfuscation: replacing the function and this keywords with single-letter abbreviations ('@' and '' ').
Encapsulation in a proton container. Packaging in PNG.
A small update:
- All images were stored in one sprite.
- The number of colors in the palette was limited to 16.
- After any change, the file was pinched by pngcrush with the -brute option to ensure the best compression.
Today, due to numerous requests, we fixed the display of the mini-game in the opera, although this is not in the requirements of the competition. Patch on the way to 10K Apart.
Thank you very much for your attention, we will be very happy if you rate our game on the contest page
PS We press further, continued.
PPS Contest is over, thank you all!