september 6, 2020

JS13k 2020-Searching for 04

JS13k 2020-Searching for 04

I have lost count on how many Ludum dares I have done but about 15 now so I'm very used to making a game in just a few days and wanted a new challenge. I have seen JS13k popping up every year in my Twitterfeed but I have always thought it was for real pros optimizing the code into unreadable code with lots of tricks and I thought I never would be able to make a game in just 13kb.

This year when I saw the announcement of the start of JS13k I thought I would give it a try. If I failed I just wouldn't have to submit it and nobody would know.

The game: Searching for 04

Inspiration

Growing up with a C64 and Amiga in the late 80s and early 90s introduced me to games like Bards tale that I spent endless hours playing. I also spent many hours playing Eye of the beholder so I really enjoy dungeon crawlers and have always wanted to make one for Ludum dare but so far it hasn't happend.

A more modern game I have borrowed inspiration and graphic style from is Delver which mixes a 3D world with 2D billboard sprites and I can't deny that parts of the inspiration also comes from Notch game Prelude of the chambered done for Ludum dare which kind of inspired me to start with game developement in general almost ten years ago.

Misreading the rules.. twice

So I read in the rules that the 13kb didn't count the inclusion of ThreeJS into the zipfile which I thought sounded strange but I started to work getting ThreeJS up and running and after two days I had some basic levels and camera movements. Then suddenly I realised I was right, it did sound strange that ThreeJS was allowed to use because it was only allowed when doing a VR-game.

So that was my first failure. I did look into doing 3D with the famous raycast and psuedeo-3D tricks but since I never understood the math behind it I couldn't really use it. So the only way I could think of was doing WebGL myself, but I was afraid I would fill those 13kb with just the rendering code.

Fast forward to the second time of misreading the rules. While doing the basic engine I did use 7zip to benchmark the size of my code but I learned that only basic zip was allowed and suddenly I lost some space.

The engine

So I put together my own little engine and I spent the first 1.5 week working on it before I even could start to add content. WebGL itself doesn't take up too much space as I thought. Many hours was spent on "Why doesn't the sprite show up on the screen?" "Why doesn't the camera rotate correctly?" "Why doesn't I move in the direction of the camera.. bloody math".

garden.png

But slowly the core engine was getting better and better. It could generate optimized walls not creating unnesceray meshes. I could create Sprites in the world. I could do simple animations. I had simple faked physics in place with collision detection. And I'm amazed how fast Javascript is executing all this code 60 times per second.

I have separate posts about parts of the engine here:

Lights
Levels-Not-Written-Yet
Physics-Not-Written-Yet

ES6 and minification

As a Java developer not used to Javascript I started to write the game in ES6 using classes and static variables. When I started to look into how to minify the code I run into some issues. First of all I had to figure out how to collect all files into a single file that could be minified. I read about using Babel to generate ES5-code but my atempts did result in a bigger file in the end. Finally I found rollup and I could do this with my code:

rollup m.js --format cjs --file ../dist/bundle.js

Now I could finally try to minifiy the code. First I tried was using Google Clojure Compiler which spit out an error after a while. Turns out it doesn't support static class variables.

After that I tried to use Terser instead which supports ES6 and in the end I'm running it with these options:

terser bundle.js -o m.js --compress --mangle --mangle-props
reserved=["movementX","imageSmoothingEnabled"]
--timings --toplevel --module

It did a really good job keeping the variable names and function calls for the public API calls. The only two it was missing is movementX for locked mouse cursor input and the imageSmoothingEnabled variable I needed to have disabled on the UI-canvas.

Zip and minification are smarter than you

Yeah. I spent some time going trough the code and optimizing it with method calls and what I tought was clever ways to save bytes. In the end the minification file become bigger and I had to back the changes and start do them one by one to see which ones actually helped to make the code smaller. The more I worked with the code the less I understand how zip works. In one place I removed a variable initalization I didn't use anymore just to see the zipped file become larger.

Assets minification

a-1.png

I started out with a big asset texture atlas which by time became smaller and smaller. The gates was a 16x16 texture but is now just a tiny part that is repeated. For the roof I had a noise texture but in the end I'm just using parts of the floor texture for the roof and coloring some texture with a tint instead of having seperate textures saving some extra bytes!

Unforunatly I did run into a classic OpenGL/WebGL issue with UVs not being rounded correctly which creates flickering from bordering textures so I had to waste some space to make the borders clean.

The big saver of space has been TinyPNG especially when I noticed I could run the assetfile 3-4 times without too much issues in the final texture.

UI

At first I was gonna render the UI with WebGL using a seperate orthographic camera but after an evening not being able to render anything decent on the screen I did fallback on an easier way of just having two canvas on top of each other with differnt Z-index. Then I'm just using the same textureatlas and canvas.drawImage() calls.

I had a custom font but in the end I removed it to get an extra 250-300 bytes. Instead I'm using canvas.fillText with a monospace font.

Audio

So I removed the custom font and started to play around with audio to create some simple sound effects. After reading up on the APIs and found a nice way to create some small "beep"-sounds that sounded ok. Then I took the game for a try in Firefox and no!! it sounded horrible. But after some help on twitter from @killedbyapixel and @TrezyCode I managed to get it work in both browsers.

I discovered a bug while looking for memory usage and discovered I had lots of Audio objects and also noticed after playing for a while the audio would start to sound strange. The issue had to do with starting the audiocontext with a click eventlistener on the canvas but that method never checked if audio already had been created so I just created new audiocontexts everytime I clicked in the game.. ooops!

Summary

This has been one of the most fun experiences I have had and I really really enjoyed both the process and how the game ended up. I have learned alot how to save bytes and I'm amazed for all the times I have optimized a few things and in the end said "woho! another 50 bytes saved \o/".

There is lot of stuff I wanted to have in the game that never ended up. Like fire, torches, other kind of enemies, traps.. but 13kb is just 13kb and I'm satisfied with how much I could put into the game in the end.

I guess I will participate again but maybe just do 2D next time instead of wasting so much time on WebGL and rendering code :)

Now it's just a few weeks to the next Ludum dare so I need some programing rest now :)