Hang by a Thread - Post Mortem
The process of making Hang by a Thread under 13KB for the annual js13kgames jam in 2022. The theme was "Death". I'm using verlet integration to simulate rope physics, and the goal is to collect all hearts, and lead the skull to the goal.
Updated: Aug 4, 2023
Published: Jul 24, 2023
The process of making Hang by a Thread under 13KB
You're a lost soul. Your thread is your lifeline. Use it wisely to find eternal peace, and collect hearts for a better afterlife.
Hang by a thread is my fourth game for the annual js13kgames competition. This is the first time I felt the game was fun to play, and I do hope to finally be in the top 100. I've always wanted to make a rope physics game. Implementing the game following the theme was only secondary, and I admit the theme isn't that well interpreted. During brainstorming, my first idea was to create a "Worms ninja rope"-type of game. I wanted the rope to consists of bone-segments, and play through an obstacle course. However, I've made several game design changes during development, and the game turned out to be something completely different from what I had initially thought. I don't think that's a bad thing, because adapting the game based on continous testing and keeping track of the space has been crucial to end up where I did. Which is a game I'm truly satisfied with. There's still a lot of things I wanted to add to make the game more interesting, but time and space didn't allow me to do that.
In this post mortem, I'll go through
- What I've done and my process
- What I've learned
- What I would have done differently
- What I will do post-jam
- Thoughts and final notes
- Results
What I've done and my process
I created a rope physics game, using a technique called Verlet Integration. I will not go through the technique in detail in this post mortem, because I think this article, and this series on YouTube explain it very well. In short, it's a technique that uses a point's old position and current position to determine it's new position (the object's speed.) We also have a link (or stick) between two points with a constraint, making sure that the two points over several iterations will eventually rest, meeting the constraint. This may sound confusing, but I highly encourage the reader to take a look at the article and YouTube video for more details. Going through the code in my repository may help too.
Using this technique, verlet integration, we can create a rope consisting of several points, and each pair of point is linked together. We can apply gravity to each point to give the rope some life.
After playing around with the first prototype, I started to think of new ideas. It looked similar to "cut the rope", but I didn't want to make a clone, so I was thinking of something else that could be fun to experiment with. Adding some more control to the movement of the rope itself could maybe lead to some interseting game play. Since the theme was "DEATH", I wanted the character to be a head skull.
The first element I added was a shuriken (ninja star.) I just needed something that could cut the rope. I didn't want the player to be able to cut the rope herself in the game, but instead use elements within the game.
I started exploring some new abilites for the player, and thought about a "jetpack" ability and a "climb the rope" ability. Climbing the rope is just shortening the rope, while jetpack is the ability to give the skull a boost upwards.
I've received feedback from previous game jams that the pixel art looked too small. Well, that's true. The skull is 8x8 pixels. I need to save space, so I made small pixel art. Instead of creating new art, I kept my sprites to 8x8 pixels, but scled them by 4 in the game to make them look bigger. (Remember to disable image smoothing on the canvas for crisp pixels)
I also experimented with different gravity settings, and I really liked the way the rope moved in lover gravity. It almost felt like the skull was in space or under water
I spent a lot of time working on the collision detection on the boxes. I ended up with a hack that only worked for boxes without rotation. I just didn't have time or the motivation to try a better approach (vector projection, and find the shortest path from point to line.) I want to improve this post-jam.
I also added some hearts, which doesn't to anything special, but I added them as extra challenges to each level, making the game replayable, and also give skilled players some extra challenge without hurting the beginners with too difficult levels. I wanted to make 1 heart in each level easy to collect, and 1 heart to be difficult to collect.
"DEATH" was the theme of the competition, but there were also some juicy prices for two other categories, decentralized and web monitization. I've had some experience with this already from previous competitions, so I wanted to submit my game for those categories as well, in the hopes of getting some prices. There were three sub-categories to the decentralized challenges:
- NEAR Protocol Challenge - build your game entry using NEAR Protocol technology
- OP Games Challenge - follow any of the Decentralize The Metaverse article suggestions
- Arcadia Challenge - align with the arcade theme, use Arcadians NFTs, or implement Arcadia SDK
I integrated the Arcadian API, and made it possible to select some headgear for our player.
I integrated NEAR to be able to buy extra levels through the use of NFTs. I created a NFT collection for my game on Paras.id (testnet for now), and each series in a collection will contain NFTs that players can buy to unlock bonus levels. 1 NFT consists of an image of the level, and the json file of the level itself, meaning that the level cannot change! The NFTs are visible in your NEAR wallet, and you can also re-sell them on Paras.id's marketplace (testnet). There are currently only 100 editions of each NFT.
This integration also made it possible for me to align with OP Games' challenge.
I also added some extra behaviors, making it possible to move boxes, shurikens, and the rope left and right. With this, I had all the building blocks I needed to create some levels. After adding sound effects and music, I wasn't able to fit much more in the game. Even with tree-shaking enabled in my build process, I still had to remove some code that I didn't need, and remove code from the game engine, kontra.js. More about this in the next section.
I added a few more effects to polish the game. I never had the time nor space to align with the underwater theme I was going for, but I added some bubble particle effect from the skull as I we move it. I also added a really short and subtle stop motion effect when the rope is cut. We stop the rendering in less than a second, add a short flash for additional impact, and I personally think it feels really satisfying.
What I've learned
I want to seperate my learning points into three different section
- Techniques used to reduce space
- Game design and controls
- Support from friends and community
Techniques used to reduce space
This is the first time I've been pressing for space for a js13kgames entry. I had to do some absurd changes to make the game fit within 13kb. First, let's go over the main parts to reduce space.
Rely on tools to minify and compress code
I used parcelto automate my build and development process. I chose parcel because it didn't require a lot of setup to get started, and everything works more or less out of the box. Parcel also builds the JavaScript files ready for production, meaning that we minify the footprint.
Parcel also uses a technique called tree-shaking, which means that it will not include code that's unused. Let's say we are using Kontrajs as our game engine. The engine supports tree-shaking really well, because it consists mostly of pure functions and independent classes. If you don't use everything in the tool, Parcel will not include it in the prodction bundle.
In the js13kgames community, many developers are using a tool to compress the build even further. The tool is called Roadroller. This tool has helped developers save at least 1kb of space, and it helped me save that much space as well.
In order to make the most of compression libraries, e.g. Mac's default zip compression tool, it's important not to use a nested folder structure for your code. It will create additinoal overhead. RoadRoller also expects one JavaScript file to compress. Make sure the production code is one JavaScript file, and one index.html file to make the most of the compression tools.
What if you have images? Use tools to reduce image size (e.g. TinyPNG), and Inline them as well. The compression tools do a good job of compressing images even further. Using Parcel, we can use data-url, to inline images. For example importskullfrom'data-url:./assets/img/skull.png';
Still pressing for space?
If you still find yourself pressing for space after following all of the good practices above, you may have to do some absurd things to reduce space. This is not something you normally want to do, but you really need those extra bytes to submit your game. This article about Byte saving techniques contains an exhaustive list of tips to reduce space.
Some examples are
Prefer ==
over ===
(save a byte)
- Prefer
!=
over==
(save a byte) - Prefer
| 0
overMath.floor()
- Prefer
let
overconst
(save a byte) - Prefer
Array.map()
overArray.forEach()
To help enforce these byte-saving (bad) practices, we can use eslint in the development process.
To save some bytes in the DOM, we can use one character for ids and classes:
Before: <div id="level-dialog" class="overlay hide"></div>
After: <div id="l" class="o h"></div>
If you are sending custom events in the JavaScript code, use variables and assign them to one character strings:let GOAL_COLLISION = 'g'; // using let instead of const to save a byte
emit(GOAL_COLLISION, {}); // Using Kontra's emit function to send the event
.
Even though Kontra is built with tre-shaking in mind, there are still ways to reduce the space even more. Let's say we are using Kontra's Vector class, but we don't use all of the functions in the class. Maybe we don't use normalize
or dot
. Well, tree-shaking is not going to help us here. We need to remove them manually. Download the library, and start removing stuff you don't need. We only have to remove the functions and attributes we don't need in the class. It's not necessary to manually remove the Pool class if we're not using it, tree-shaking handles that for us.
Game design
We are creating a small game within 1 month and under 13kb. The most important thing I've learned is to limit the scope of the game as much as possible. Focus on one thing only, and polish that one thing as much as you can. I look at the competition as an opportunity to learn new things, experiment with something you have thought about for a long time, or try something novel. In my case, I really wanted to learn about rope physics.
One month may sound like a lot of time, but if you have a full-time job and a family, you're only able to work on the game a few hours every day. Keeping that up for 1 month is tough. It drains a lot of energy, and I wanted to quit many times. Don't let the feature creep get you, and don't be afraid to trash ideas if they take too long to implement. I've trashed my ideas many times. I struggled with the collision detection of the rope, and abandoned the idea of using vectors and projection to solve the problem for all shapes and orientations. I did a simple solution that worked for boxes only (not rotated.) I saved alot of headaches and some bytes becase of it.
Support from friends and community
Let friends and family play your game. Show them what you've made. They'll encourage you and give you advice if something is really strange. Ask the community for play testing or technical advice. I've gotten important feedback before I submitted the game. When you work on your own game for hours, it's easy to miss obvious things. You will be surprised to see how good the feedback is from other game developers. Some comments are trivial to fix, and will improve the game tremendously. I didn't auto-restart a level after you died. I had a "restart"-function by pressing 'z', but having that auto respawn keeps the player engaged instead of frustrated. I also changed something in the main game mechanic. I got a comment that the rope felt too heavy, and that the trajectory of the skull felt wrong. I fixed it partly by cutting the rope half way instead of from the top. It was still a little heavy, but it made the game play better.
What I would have done differently
Instead of using (small) images, I would use vector data instead. Even a small image of 8x8 px takes around 200 bytes. And that is for an image limited to 4 colors plus an alpha channel. For exmaple this image of my skull. The skull looks blurry because it's an 8x8px wide image, and anti-aliasing is making the skull look blurry when we scale it up.
My game uses a mix of images and vector data, but I think I could save some bytes by using vector data only.
I thought about manually creating the sprite in JavaScript by using an array of integers, where each integer represents a color. I don't know if this will save me some space or not, but I will experiment for next year's gamejam. Here's an illustration of what I'm thinking about.
What I will do post-jam
I want to continue on the game, polish it, and create a water theme atmosphere. I got some comments about the controls being difficult to use, so I will try to improve that as well.
I want to improve the collision detection part of the rope to work for any oriented box and circles. Doing that will allow me to create more interesting levels.
I will also adjust the movement of the boxes to ease in-out instead of moving linear, meaning that we get smoother animations without abruptly changing direction.
NFT Mint Game
The people at OP Games reached out to me, asking me to use the game as a minting experience for their Arcadian Reloaded Free Mint campaign. More about that in this Blog Post.
Thoughts and final notes
I've had great fun creating my fourth entry. I feel like I'm learning new things each year, and it's fun to see that I'm climbing the overall rank ladder for each year. I didn't make top 100 last year, but I hope this is the year. There are a lot of good entries this year. I'm blown away with a few of them, so the competition is tough.
What I've enjoyed the most with the game jam is the joy I see in my five year old son when he plays my game. He saw it by accident when I was testing one of the final levels, and he wanted to play it. He likes to play it before he goes to bed, and it's fun to see that he gets better at the game each time he plays. It's his first game to play alone and I feel really proud. It's a memory I will cherish forever.
He was able to catch both hearts on the first level the other day, and we both did a "high five" and shouted with joy. His body suspens when he almost reaches the goal, or almost capture a heart, or almost gets cut by a shuriken (the spinning saw blades.) I think this is really fun, because I remember the feeling myself.
Results
The voting period is over, and I was thrilled to see the results this year. My game placed first in the Web Monetization category, 6th in the Decentralized category and 31st in the Overall ranking!
It's a giant leap from my 136th overall position from last year. Not only that, but the quality of the games this year has been exceptional. The competition was firece, and after rating almost all the entries during the voting period, I didn't expect much of my own game. There were just an abundance of great games.
The overwhelmingly positive results made me more confidence, and it helps me stay motivated and continue working on the game. It will be difficult to beat this next year.
Previous entries:
- js13kgames2021 - Kurve Space (ranked 136/204)
- js13kgames2020 - Black Jack 404 (ranked 157/200)
- js13kgames2019 - Circle Back (ranked 194/200)
If you liked this blog post, or want me to elaborate on other topics, consider following me on Twitter, @ReitGames, and mention me in a Tweet. Happy coding!