Lessons Learned
Backcountry was a huge project for us. We prepared for it for a few months prior to the competition, and we learned a lot about game design and architectures, JavaScript, WebGL and 3D maths. We also made many mistakes. Following are some lessons we learned.
What Went Well
- We prepared prior to the competition. We experimented with new technologies and built prototypes to validate ECS as a new code architecture.
- We spent some time in June working on an unreleased test project. It was a voxel 3D platformer and it gave us an opportunity to blaze many paths for Good Luck, and later on, for Backcountry.
- We experimented with ECS for code architecture, WebGL's instanced drawing for fast rendering of voxels, and with Web Audio for sound effects. All of these ended up being core technologies in Backcountry, too.
- Writing size-constrained code requires a special mindset. It often goes against the established practices of clean code. It felt wrong to us at first, but we quickly embraced it and started to enjoy it, too.
- We didn't pull any dependencies.
- We vendored
gl-matrix
into our codebase and rewrote it to TypeScript. - We controlled every byte of the codebase. This approach allowed heavy modifications in the name of size optimizations.
- OTOH, we had to write all the code that we needed.
- The code isn't split into a generic engine and a specialized game code. Instead, the entire codebase is a monolith designed to holistically solve Backcountry's problems, and no other problems.
- For instance, we knew that the game would be only using two shaders and a single mesh: the voxel cube. The rendering pipeline is designed specifically to support this scenario.
- We vendored
- We avoided abstractions and favored simple and repetitive code. The code written in this manner is easy to reason about, tends to have fewer surprises, and performs and compresses great.
- We usually started with a fairly generic version of some code. Once feature-complete, we optimized it just for the cases present in Backcountry.
- For example, our particle renderer used to take a few more uniforms to control the initial and the end sizes and colors of particles. After optimizations, the end size is always
1
and the end color is alwaysvec4(1.0, 1.0, 0.0, 1.0)
, i.e. yellow.
- For example, our particle renderer used to take a few more uniforms to control the initial and the end sizes and colors of particles. After optimizations, the end size is always
- Generalizations cost bytes. The less generic the code is, the smaller it usually is.
- Generalizations have a cognitive cost, too.
- It's easy to get side-tracked by thinking about how to scale abstractions up, and in the end it often turns out that requirements change and the abstraction doesn't work as well as intended. YAGNI. When things keep changing, don’t waste time modeling problems which will become outdated the next day.
- We usually started with a fairly generic version of some code. Once feature-complete, we optimized it just for the cases present in Backcountry.
- This approach was made possible thanks to clearly defined maintenance and extensibility requirements for Backcountry. We knew it was going to be a short-lived project, released on September 13, with very few changes to the code after it ships, and no changes to the game design.
- Given these constraints, we were able to make decisions about hardcoding certain parameters and about using a closed typing architecture to lay out the code.
- Open typing systems allow easy extension through inheritance and dynamic dispatch. They make it easy to add new subtypes but harder to add new logic to the existing ones.
- Closed typing systems assume that all the variety is known up-front. They commonly use algebraic sum types to define unions of possible types. In these systems adding new types is hard, but adding new logic is easy: a new function must only support all known types and doesn't require any polymorphism.
- It's tempting sometimes to want to improve the game after the competition ends, to add all the features that didn't make it at first, etc. OTOH, it's liberating to not do it. The version of the game which we shipped is the final one, complete with all its faults and mistakes.
- In his 2016 book Spelunky, Derek Yu gives the following advice about finishing a game:
Save it for the next game. Do you have a brilliant new idea that requires extensive changes or pushes back your deadline? Save it for another game.
- In his 2016 book Spelunky, Derek Yu gives the following advice about finishing a game:
- Given these constraints, we were able to make decisions about hardcoding certain parameters and about using a closed typing architecture to lay out the code.
- We didn't pull any dependencies.
- ECS is a very lean and scalable architecture for writing games.
- The focus on composition makes the code easy to reason about, and maintainable in the long run.
- Even without any optimizations, ECS performs very well on a wide spectrum of computers and phones.
- It's a viable architecture for writing games for js13kGames. The maximum of 32 components is enough for most projects; a 13KB game will necessarily be rather small in scope.
- ECS made programming fun again for us.
- In gamedev, good performance is a feature. We tested and monitored performance all the time, using FPS counters and recording profiles.
- At 60 FPS, 16 ms is the maximum budget available per frame smooth animation to run smoothly; your average should be lower so that you have a buffer for outliers.
- A GC pause, a resize event, a DOM reflow, or something else might happen unexpectedly and make the animation jittery if an outlier frame takes more than 16 ms to render.
- In size-constrained projects, the friction between size and performance is unavoidable.
- When deciding which one to sacrifice, we usually went the third way: we cut scope.
- Good tools are important. They made us more productive by allowing us to not even think about certain problems.
- Thanks to Prettier, we literally had zero discussions about code style, ever.
- Yes, sometimes we weren't happy with the result of auto-formatting.
- But we were much happier with the fact that we weren't spending any time on formatting manually, or trying to format code while typing it.
- For someone who suffers from obsessing about code style (Staś), this was a bliss.
- With TypeScript and VS Code, refactoring was a breeze. Knowing that a piece code would be easy to refactor later on allowed us to keep momentum while adding features.
- Not sure what to call a property on a interface? Call it whatever, finish the feature, and then maybe rename it using Rename symbol (F2).
- We also loved VS Code's Organize imports feature, which rearranges imports alphabetically and removes the obsolete ones. It completely automates what usually amounts to being tedious bookkeeping.
- Here's our
.vscode/settings.json
in its entirety:{ "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": true }, "[html]": { "editor.formatOnSave": false } }
- Thanks to Prettier, we literally had zero discussions about code style, ever.
- We focused on making a feature-complete product first, and we optimized for size later.
- We resisted many temptations to micro-optimize code as it landed.
- Given how the DEFLATE compression works, manual micro-optimizations are mostly irrelevant. In the worse case scenario, they can actually inflate the final build size.
- DEFLATE cares about code's low entropy, not its size in bytes.
- DEFLATE considers the entire codebase; any code you land after optimizing for size changes the impact of the optimizations.
- The minifier rewrites most of the code anyways. Spending time on deciding whether to use
for
orwhile
in the source code is pointless.
- Given how the DEFLATE compression works, manual micro-optimizations are mostly irrelevant. In the worse case scenario, they can actually inflate the final build size.
- Instead, we focused on the big picture:
- How much new code does a feature add?
- How much of it can be reused for other purposes?
- Can we design it slightly different in order to reuse existing code?
- How generic the code should be? Can we make it less generic?
- How much of the code can be hardcoded vs. how much of it must be parameterized?
- OTOH, due to this approach we were over the size limit for most of the competition. It added a bit of stress as we never knew how much we could keep adding.
- We resisted many temptations to micro-optimize code as it landed.
- We didn’t hesitate to cut scope and remove large existing features.
- The hardest part of js13kGames isn’t trying to come up with a smart way to write code that takes fewer bytes than the alternative.
- It’s deciding which code doesn’t even get written.
- Removing features which don't bring too much to the final product is the best size optimization.
- We shipped with sound effects and a soundtrack.
- In our previous js13kGames attempts, we neglected sound design. We would leave it "for later" to implement and compose, and that later never came, or when it did, we were already looking for features to cut and bytes to save.
- Yet, sound design is such an integral part of game design!
- In 2019, we studied the topic of audio synthesis before js13kGames started. We created our tiny custom synthesizer, and we landed it in Backcountry very early on. We also included a number of sample sounds, so that they take up space from the beginning.
- Backcountry was for us a great motivation and excuse to study a new field of knowledge: the music theory and the sound synthesis. Staś even gave a talk about it (in Polish) at a local meetup in November 2019.
What Could Be Improved
- We made a cool technical demo rather than a unique and original game.
- To some extent this was a result of a conscious decision. In previous years we focused on making the core mechanic unique and original. With Backcountry, we tried a different approach: start with a proven and common idea and make a polished and technically impressive, yet not very original game.
- When evaluating game ideas, we used a simple three dimensional evaluation framework. The three axes were: innovation, fun, and execution. An idea is considered good if it scores well on at least two axes. For Backcountry, we picked fun and execution.
- Perhaps due to the changes to the voting system, this strategy didn’t pay off in 2019.
- Backcountry scored max points in the Graphics and Technical categories, but didn’t do as well in other categories.
- To some extent this was a result of a conscious decision. In previous years we focused on making the core mechanic unique and original. With Backcountry, we tried a different approach: start with a proven and common idea and make a polished and technically impressive, yet not very original game.
- The idea for the core gameplay loop came too late. A few days before the deadline we were still trying to find the fun.
- Instead, we should have started the project by spending a week exploring and prototyping gameplay loop ideas.
- We set out to make a fun hack and slash in the browser, and we only got 37 points out of 50 in the Fun category. Hack and slash games are supposed to be fun!
- We should have noticed that there was a problem during user testing.
- We had set milestones (and hit them all!), but they were the wrong milestones.
- The milestone were roughly:
- August 19: 3D projection, voxel rendering, mouse picking.
- August 26: Path finding, simple enemy AI.
- September 2: Terrain generation (town, desert, mine),
- September 9: Feature complete; start optimizing for size.
- September 13: Submit.
- We spent too much time on the technical side of the project. Many techniques were new to us, and to be fair, we had a blast studying and implementing them.
- Not enough time for prototyping the game idea and designing the complete gameplay loop. For most of the time, we just vaguely assumed we knew what an ARPG should play like.
- Not enough time for user testing. Had we done more of it, we would have learned there were problems with the gameplay.
- Not enough time for size optimizations. Despite best intentions the project ended with crunch as we were looking for last bytes to shave. We went to sleep at 6 AM on the morning of September 13.
- The milestone were roughly:
- We should have added more variety in character models to convey meaning and mechanics.
- Friendly NPCs in the town look the same as bandits out in the desert.
- It’s not clear to first-time players that the boss can be found outside the town.
- We saw testers look for the boss by walking around the town and never making it outside into the desert.
- At one occasion, the first bandit you got a bounty for looked almost the same as the sheriff!
- Both characters are randomly generated from the daily seed. It was pure coincidence.
- We tried to mitigate this problem last minute by placing a gold bar outside the town gate, hinting to the player that they need to leave the town.
- We didn't communicate to the players that the right mouse button can be used to shoot without moving.
- We wanted to make a game that plays instantly without a tutorial.
- We could have at least mention it in the entry description.
- We assumed many players would be familiar with the Shift+Click from Diablo or Right Click from Torchlight, and that they would discover the Right Click in Backcountry on their own.
- Right Clicking is an important skill at higher levels. It's a difference between shooting at enemies from safe distance and walking straight into them for a certain death.
- We wanted to make a game that plays instantly without a tutorial.
- We didn’t do enough play testing.
- Many gameplay issues discussed above would have been easily caught.
- The final gameplay loop wasn’t ready until 3 days before the deadline. We didn't allow enough time for play tests.
- We should have showed test builds to people outside the circle of close friends.
- Friends tend to be too nice :)
- During play tests, we sometimes couldn’t stop ourselves from explaining the game mechanic to testers who struggled.
- It’s really hard to stay silent in those cases.
- A missed opportunity to realize that there were issues with discovering what the game was about.
-
We shipped a WebGL2 build which doesn’t work on iOS.
- It saved us about 100 bytes.
- iOS has much less market share than Android, but it’s the mobile platform of choice of many people we wanted to play our game.
- It’s a real shame iOS doesn’t support WebGL2, however.
- Next time we’ll try to look for those 100 bytes elsewhere.
- The compression measurements we relied on during the optimization process were not precise.
- The day-to-day measurements taken after each commit used gzip while our release pipeline used the more effective 7z.
- Because of differences in compression quality, it’s best to use the same tool that we use for creating the release builds.
- Changes with reported savings of 10 or 20 bytes were likely meaningless.
- We didn’t check the final build size for each change.
- Producing release builds was tedious and required a number of manual edits.
- We should automate more of this process next time, including merging the
master
branch torelease
, and building production builds in CI.
- We missed a few opportunities for size optimizations.
- We should have used Chrome’s Coverage tool more rigorously.
- The day-to-day measurements taken after each commit used gzip while our release pipeline used the more effective 7z.
- This documentation took over 4 months to write.
- Rather than write just a post mortem, we inflated the scope and set out to describe in depth the technical and design details of making Backcountry.
- This turned out to be a herculean task. Much bigger than creating the game itself!
- We should have taken notes during the development process.
Afterword
This book is a testament of all the fascinating things we learned thanks to js13kGames. It's a lot! We hope the learnings we have shared will be helpful to many other developers creating web games and size-constrained demos.
We also wrote it for our present and future selves. When memory fails, we want to able to return to a detailed written record of how everything works. It can serve as a reminder that things that look trivial on the outside can hide a lot of complexity, both in terms of design and implementation. As players of other games, we often only experience the end result of thousands of decisions, each of which represents a small universe of requirements, constraints, processes, timelines, interests, and emotions.
The Lessons Learned chapter was particularly challenging to write. Admitting to mistakes is already no easy task; putting them in writing is harder still. It was a therapeutic experience which gave us a closure after shipping Backcountry, and equipped us with a better understanding of what to improve going forward.
Thank you for reading.