Small, Fast, Sharp, & Cross-Platform
In order to maximize our potential reach, we decided on some self-imposed constraints. The game had to be:
Small — low barrier to download, keeps download-to-play time short,
Fast — had to play smoothly on low-end mobile devices,
Sharp — make use of the high-dpi screens that saturate the market, and
Cross-Platform — at least playable in all major browsers/smartphones.
Many of these constraints are in direct tension, so the first technical decision required to make these goals even plausible is the following:
Appropriately scope the game
These constraints take on a different meaning if we were looking to make the next fortnite- instead, our target was something more along the lines of 2048 (AAA devs looking for tips will find no insight here). That means that, rather than employing every state-of-the-art trick to bend hardware to our will, we instead simply strove to remain diligent against unnecessary bloat (an achievable goal for a programming team of 1 working nights and weekends).
With that in mind, our next task was:
Choose a lean tech stack
That’s right: HTML5. (Alright, quiet in the back, please.) Remember, we’re not looking to break performance records here- just avoid unneeded pitfalls.
The strategy here was to err hard toward avoiding any plugins / libraries / frameworks if at all possible. As a result, the web version of the game is < 42k (before accounting for audio/image assets). Compare to 2048’s total of ~9k. Bottom line- still well below any threshold of concern.
However, this wasn’t going to fly with our goal of releasing cross-platform (aka “native mobile”). So rather than adhere to the “no-plugins” principle with arbitrary strength, we took the (necessary) step to use Apache’s Cordova to wrap our HTML5 page into native iOS and Android apps. The downside here is that an empty Cordova app is already 1.6M. Certainly still worth it.
Some immediate benefits from this choice are:
- The drawing API is an absolute dream to work with. It takes seconds to get rudimentary shapes up and running in code without needing to interface with an artist or modeler (or context switching to build placeholder programmer-art yourself).
- I have a decent amount of experience with it.
In hindsight though- this may not have been the best choice for architecture. The alternative of building in the DOM likely would have been its own can of worms, so that’s out. But a third option - WebGL - likely would have offered out-of-the-box performance gains that were hard-won with Canvas 2D.
Speaking of hard-won performance gains- this is where the rubber hits the road in terms of our constraints. We have Cross-Platform locked down with our choices thus far, but in doing so, have we committed ourselves to deadlock between Small, Fast, and Sharp?
Let’s start with “Sharp”- it’s common practice to include multiple assets in your game for the different resolutions at which it might be played. Assuming this includes “very high iPhone X resolution”, we’re already committed to large image asset sizes. Well, there goes “Small”.
Aha! But we can instead use SVG! That will scale with perfect precision at any resolution, and given our simple geometry, will result in microscopic file sizes! “Small” achieved!
Almost. Remember, we’re using the Canvas 2D rendering context. And while most browsers support SVG, not all do (looking at you, firefox…). And even on the browsers that do, there are many implementations where realtime shape-drawing is sssllllooooowwww (looking at you, whatever implementation of android/chrome my crappy old LG 10 is running). Well there goes “Fast”…
Here’s a trick (er, more of a bludgeon…): we can play with lowering the resolution and blending modes! Ok, I’ll admit, that’s just giving up on “Sharp”… or is it?
I noticed that when I turned off “Image Smoothing” in the canvas context (essentially changing bilinear filtering to nearest neighbor)- it was significantly more performant. Still not enough to allow for real-time dynamic SVG-like drawing commands, and it also didn’t look great. But there’s something to work with here.
A Neato Trick
A bilinearly-filtered image, when blit to an identical-resolution destination via nearest-neighbor looks like… a bilinearly-filtered image.
With some additional constraints to the rendering code, we could do the high-resolution, nicely-blended dynamic drawing (using Canvas 2D’s delightful drawing API) to a cached atlas, and just be sure to blit at pixel-perfect scaling and pixel-perfect boundaries!
This really requires pixel-perfection though- any change of resolution (including initialization) is a complete invalidation and re-rendering of every asset. This means that every asset in your game needs to be dynamically constructible. A bit of extra work, but results in the best of all three worlds!
Previously, I mentioned the game sans-assets comes in at < 42k. Well, if you add the image assets on top of that, we have: an additional whopping 24k! (This is for SVG images for the background, the logo, and some medal images that were too detailed to try to render dynamically. In firefox, we fall back to pngs, totaling 290k). Throw the font in there at a painful 85k and I’m not going to do the math the point is: we’re fine!
So fine, in fact, that we had the freedom to be pretty loose with audio files (including music!) coming in around 1.5M. To be honest though, I’m not sure what could be done if we did need to further compress those… ¯\_(ツ)_/¯
This wasn’t the result of perfect planning, and we certainly spun our tires coming to the functional conclusions we did. But at the end of the day I think we struck a really nice balance giving our players fast, low-bandwidth download times, a smooth and juicy play experience, and the ability to exploit the limits of whatever display hardware they happen to own.
I hope you’ll check out the little game we worked hard to make (and maybe download the app too!)
Bonus: Here’s the spritesheet rendered for a 660x900 game resolution-