Ninja vs EVILCORP postmortem
Js13k 2020 is now over (sadly), and the results are out.
After chasing the symbolical first place for seven years, Ninja vs EVILCORP did unexpectedly well, and I now feel obligated to share some of the things I learnt along the way.
Bear with me, as there is a lot I want to share, while I have very little clue how to structure it.
Designing the game
Fake 3D racing game?
When the 404 theme dropped, I was actually in the middle of a road trip through Québec, but I somewhat benefited from it: no laptop to work on my entry, but lots of time to doodle, write algorithms, think about the story and gameplay.
My initial idea was to expand on my 2019 entry, [SWAGSHOT], and leverage the fake 3D tricks to make a racing game. Imagine Wipeout’s score attack mode, but a lot less cool. You would keep going faster, having to clear gates without hitting the edges, with a million effects to hide the simplicity of the game.
The issue with that idea was that is was never going to fit the theme. I could add 404 gates, make the timer four minutes and four seconds, try to reach 404 kph, but really nothing more than a cheap grab.
Building a platformer
So I decided to go back to my notebook, and to one of my favorite genres: platforming games.
Every year, I try to build something new, something that hasn’t been properly done within js13k.
Except for 2020.
I had already built two platformers: It’s raining… boxes?! (4th in 2015) and Glitch Buster (2nd in 2016). Three if you count Virtual War (8th in 2014). But Glitch Buster was the only “true” platformer, and I wasn’t super happy with it (I’ll talk about why in the user testing session).
This time, I wanted to make a platformer with excellent controls. Everything had to be snappy. Everything had to be fast, yet feel natural. You press jump, your character jumps. You hold jump longer, your character jumps higher. You jump near a wall, your character does a wall jump. It had to be responsive. Inspiration came from Super Meat Boy.
There had been many platforming games made for js13k, but I wanted to take the risk and make something that I would play myself.
I wanted to make it a speed game. The goal would be to finish the levels as fast as possible. I love watching speedruns, and I wanted to encourage people to “break” the game and beat it as fast as possible. In order to achieve that, I’d need obstacles. Jumping alone isn’t fun, jumping between traps is more exciting, so I decided to make it a sneaking game. Guards and cameras were introduced. Inspiration came from Stealth Bastard Deluxe, which I’d highly recommend trying.
Because I wanted to make a speed running game, I’d need it to be predictable. No RNG. This is actually something I had always wanted to do. I’ve always built games with highscore mechanics. You keep doing the same thing over and over again, your goal is to have a high score. This is an easy way to make games, as you can generate things procedurally. This time, I’d have to build levels myself. Maybe even make them good and call it level design.
Another goal I really wanted to achieve was that I wanted players to be able to complete the game. I wanted the game to last 5 to 10 minutes, and help players get to the end, which resulted in the different difficulty settings. I’d rather have someone finish the game in easy difficulty than let them close their tab.
Adding a story
From all these ideas, I came up with a story. If it’s a sneaking game with crazy jumping mechanics, let’s make it about a Ninja.
With guards and cameras, it became pretty obvious that it had to be an office setting. From there came the idea of scaling a tower. Each level would be a floor, and I would transition between levels by raising the camera. A lot of the inspiration came from Mark of the Ninja.
Now why would a ninja be sneaking into an office tower? What would they be looking for? This is when the story and the theme came together: the tower would be an evil corporation’s, say EVILCORP, and the ninja would be looking for a file on the computers. Each level would end with a 404 — PLANS NOT FOUND, until the very last level, where the plans would be found.
For the style, given my lack of artistic talent and the constraints, I decided to go back to Glitch Buster’s blocky style.
Code things
Collision management
Whenever I make a new game, one of the difficult things I want to get right is collision management.
Typically, you can handle collisions with a simple algorithm:
const collidingBlock = character.checkCollision()
if (collidingBlock) collidingBlock.pushAway(character)
This works in most cases, but it causes a few issues with some specific layouts.
- You can push the character away so that it is closer to its “expected” position. If you do that, there is a risk of snapping through corners.
- You can also push the character away so that it is closer to its previous position. If you do that, there is a risk of making it harder to go past corners.
- You can also take more blocks into account, but it’s a bit more tedious and requires a lot of cases, so I decided to be lazy and cheat.
Ninja vs EVILCORP handles collisions by:
- Computing all valid (non colliding) positions and snapping to the position closest to the expected position (optimizing for movement). Relevant code can be found here.
- Capping the framerate. Relevant code can be found here.
Yep, this is highly inefficient, as I compute a lot of positions, and I also prevent teleportation by making sure that the character is never updated less than 60 frames per second. If you ran the game at 30 frames per second, the character would still compute 60 frames every second.
The game then makes sure that frames don’t have too much delay between them. If you were to run this on a toaster CPU, at 10 SPF (seconds per frame), the game wouldn’t compute 600 frames.
Jumping
Because I wanted the game to be consistent and predictable, I needed all jumps to have the same maximum height.
Typically, to get a good jump curve, you would write something like this:
jump() {
velocityY = -200
}update(timeDelta) {
velocityY += timeDelta * 100
y += velocityY
}
At the time of the jump, you add vertical momentum, which gets reduced over time.
There are a few issues with this:
- It becomes a bit difficult to predict the maximum jump height.
- It is slightly (but noticeably) framerate-dependent. Different frame rates give different peak heights.
- If you want the player to be able to press the jump button longer to jump higher, the curve becomes unnatural.
So I decided to “cheat” again. I would separate the upward momentum from the downward momentum:
update(timeDelta) {
if (goingUp) followEasingCurve()
else {
velocityY += timeDelta * 100
y += velocityY
}
}
This way I would be able to control the time to peak, the height of the peak, and have a natural curve regardless of how long the player holds the jump key. Relevant code can be found here.
Compression
I don’t have much to say about compression, but I thought I could share a few of the tricks I’ve been using for js13k.
Please do not use any of these tricks for actual projects. This is only good in the context of js13k where the constraints are unreasonable :)
It is worth noting that I use a custom compiler for js13k. I could have probably achieved the same thing with Gulp, Grunt or Webpack, but why would I do anything sane? After all, we’re building tiny unmaintainable games.
Don’t sacrifice readability
The 13kb constraint is a rather strong one. However after all these years I’ve noticed that you don’t need to sacrifice your code’s readability.
I use ES6 classes, have decent variable names and functions. I consider my code somewhat readable, but I’m able to compress it because of separate tricks.
What you sacrifice in readability will hurt you in the long run. If a variable is poorly named, you might spend more time debugging than you’ll save space.
Hardcoded constants require less characters
Let’s look at this code snippet:
const Constants = {
width: 800,
height: 600
}canvas.width = Constants.width;
canvas.height = Constants.height;
And let’s compare it to this one:
canvas.width = 800;
canvas.height = 600;
Regardless of the minifier you use, the second example will always be smaller.
Now, hardcoding these things does sacrifice code quality, which I don’t want, so I made it so that my compiler would replace constants before minifying the code. That way, I can write this code:
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
CANVAS_WIDTH
and CANVAS_HEIGHT
simply get replaced when compiling.
You can find these exact lines here, and the constants definitions here. And this can be applied to a lot of constants: minimum framerate, jump height, gravity, colors…
one is truthy, zero is falsy
Now, we all know Javascript is a weird language. But we can leverage this weirdness. In Javascript, true
and 1
are kind of the same thing, and the same goes for false
and 0
.
Let’s replace them all!
In Ninja vs EVILCORP, all true
/false
references are replaced with 1
s and 0
s.
And you can do the same thing with null
, which is almost the same thing as 0
.
Let’s push it even further. const
is five characters long, but let
is three characters. Let’s also replace that.
And the best part? It doesn’t affect the code you write! You can keep writing good code. You can find this trickery here.
Again, when applied to the entire code, small savings like this make a significant difference.
Mangle everything!
So now that all our constants are hardcoded, let’s shrink our symbol names.
Javascript minifiers can easily make variable name smallers. Classes and functions in the global scope, not so much. Same for properties.
But this is js13k, and we can take extra risks by mangling everything. My custom compiler actually renames every symbol it can find, and gives it the shortest possible name.
So, before my code is even minified, it is already highly unreadable.
For instance, here is what it’d look like:
cz() {
for (let i = 0 ; i < 100 ; i++) {
this.B._q({
'_f': [10, -10],
'_J': '#000',
'O': N(1, 2),
'x': [this.x + N(-15, 15) * 1.5, N(-20, 20)],
'y': [this.y + N(-15, 15) * 1.5, N(-20, 20)]
});
}
}
Note: This trick might be out of date since Terser has property mangling, but I felt like it was worth sharing, as I am still probably more aggressive than what Terser can do.
Throw window out the window
Fun fact: in Javascript, if something is defined on the window
object (which is the global scope in your browser), you don’t need to explicitly get it from it. And if you define something without var, let, const or function, it gets defined on the window object. That saves you a few characters every time.
This is super practical for global variables or utilities. For instance, all my math utilities are on the global scope:
limit = (a, b, c) => b < a ? a : (b > c ? c : b);
between = (a, b, c) => b >= a && b <= c;
rnd = (min, max) => random() * (max - min) + min;
distP = (x1, y1, x2, y2) => sqrt((x1 - x2)**2 + (y1 - y2)**2);
dist = (a, b) => distP(a.x, a.y, b.x, b.y);
normalize = x => moduloWithNegative(x, PI);
angleBetween = (a, b) => atan2(b.y - a.y, b.x - a.x);
roundToNearest = (x, precision) => round(x / precision) * precision;
pick = a => a[~~(random() * a.length)];
Make some objects global
When you build an HTML5 game, there are some objects that you are going to make references to all the time.
For instance, what if instead of writing:
const enemyPosition = Math.random()
… we could write:
const enemyPosition = random()
It doesn’t seem like much, but at the end of the project, it saves you a lot of symbols that can’t be mangled.
In my js13k entries, I always expose Math
and my main canvas context on the global scope. You can find the code here and here.
Compress level matrices
Typically, what takes up a lot of space isn’t the code itself. It’s the content: strings, and in my game’s case, levels.
While strings are difficult to optimize, levels aren’t.
In Ninja vs EVILCORP, every level is made of a 20x20 matrix. With a two-dimensional array, that’s 842 characters for the matrix alone. Way too many.
The trick was to notice that the structure of the levels would be mostly empty space, and a few sets of cells in a row (vertical or horizontal) to form the platforms and walls.
So instead of storing the matrix, I stored the list of lines for each level. Each level would be a list of 4-element vector:
- Index #0: row at which the line starts
- Index #1: col at which the line starts
- Index #2: vertical length
- Index #3: horizontal length
Obviously I’d need a function to encode matrices (at compile time), and another to decode them (at run time). Both can be found here.
I also figured all levels would be “boxes” with four walls: top, bottom, left and right. If every level has that structure, no need to store these walls. They can be added when decoding.
This resulted in a size reduction between 60% to 80% per matrix. Given the game has 17 levels, I was able to save a lot of bytes to add more features (or more levels).
And again, levels are still somewhat readable.
User testing
One of the things that I did differently from any other game I’ve built in the past, is that I really tried to get as much user testing as possible.
When building a game, it’s easy to forget that:
- You know all the mechanics
- You know what the game wants you to do
- You’re better with the controls than anyone else because you played every version of it. A lot.
It’s really difficult to take a step back and look at the game through the eyes of someone who hasn’t spent so many hours building and playing it.
Roasting my past entries
In order to demonstrate that, I’m going to look at my own past entries and point out things that I now consider bad:
- Bad Luck Brian: graphics could be improved. Splash particles on the floor, on the umbrellas. It’s a bit hard to understand that you’re supposed to do because it’s only communicated through text. Could use more “cutscenes”. No sound is bad.
- Virtual War: I honestly really don’t like this game. I originally wanted to make a jumpy tank game, ended up making a worse version of stickman jump. I still kinda like the screen grid, but the effects feel a bit cheap. Gameplay is far too basic and doesn’t favor replayability. Bad sound effects.
- It’s Raining… Boxes?!: I still really like the concept of the game, but the theme was barely taken into account. Jumping feels floaty and the character could be improved. Mediocre sound effects.
- Taxi Drift: rushed entry in about a week, theme was not even taken into account. Pedestrians look terrible, graphics are inconsistent (pixelated world, blurry characters and destination, blocky cars). No sound effects.
- Glitch Buster: I’m still quite proud of this game, but the controls could be improved. Jumping feels too fast, double jump is not great, holding the jump key would have felt more natural. Needs more colors, especially for health items that should not be red (what was I thinking???). It’s also quite repetitive, and levels aren’t super fun. Sound effects are okay.
- Lost Beacons: unfinished entry due to bad mental state that year. No way to select a level, game doesn’t scale too well after a few levels where AI gets too good, and could be optimized. Heard some people spent hours on it though, which I consider a huge success. Sound would have been nice.
- Everyone’s Sky: one of my worst failures. The game was ambitious as it was trying to be a 2D no man’s sky, with various missions and the ability to make allies. Ended up being a boring and repetitive asteroids-like game, with difficult controls.
- [SWAGSHOT]: theme was barely followed. Game is good technically but there is no story, and nothing except a high scoring goal. Sensitivity issues on some people’s devices, and apparently some people didn’t realize they had to use their mouse. Sounds are okay but soundtrack is decent.
So based on all that, my aim was to leverage user testing to…
- … better communicate the controls and goals to the user
- … have decent (not annoying) sound effects
- … allow users to do what they expect games to offer: do they expect a level selection screen, as pause menu
- … have decent graphics: make sure everyone understands the story and setting
- … make sure controls were good
Beyond all that, I wanted to make sure that everyone wanted to finish the game. In js13k, a lot of games don’t catch the player’s attention. If I can get the player’s attention and interest for a few minutes, then my goal is achieved.
How I conducted user testing
One thing I have learned in the past is that sending a link to a group of people you know and waiting for them to give you feedback is actually terrible. No one will dare give you constructive feedback. Most people will just click the link, try it, and go about their day.
In order to address that, I decided not to do any testing with close friends, nor coworkers. I leveraged a gaming community I’m part of, a hobby community, and the js13k slack. I was able to get help from players of all ages, gaming background, people I knew, people I had never talked to before, making it super valuable.
I also asked people who wanted to test to jump on a voice call on Discord, and share their screen before I even sent them the link. This allowed me to look over their shoulder during the entire process.
Outcomes and takeaways
Results were extremely useful for each testing session. I was able to make adjustments every time, and each session was smoother than the previous.
I can’t thank the people who helped test the game enough. What the game was in the first session, while very similar in appearance, was very different from what it is now.
Here are some of the things that resulted from the sessions:
- Coyote time had to be added. Coyote time consists in allowing the player to jump after they left a platform for a small amount of time. This allows players to jump off platforms a bit more easily without requiring insane reflexes. While I originally was against it since it could allow for exploits, it turned out to be necessary, given even skilled players would struggle with it.
- Many players did not realize they could hold the jump key longer. This resulted in an explicit message on the first level, and different layouts.
- Wall jumps were really hard to balance. Some players press a directional key when sticking to a wall, while others don’t. I had to add coyote time and make sure players wouldn’t stick to walls unexpectedly.
- Difficulty is hard to balance. Some levels were too easy, some too hard. Some of them were too easy towards the end of the game, some were too difficult towards the beginnings. I spent countless hours tweaking levels, and adjusting the orders. Some players could finish the game in about 15 minutes the first time they played it, some of them couldn’t in over 45 minutes.
- Supporting different control schemes is important. Some players are gamers that are used to WASD controls, while others prefer arrow keys. Some of them are okay with pressing the space bar to jump, others prefer to press the up arrow.
- Most players didn’t realize that they could change the difficulty during the game. This is something that I added because I wanted everyone to be able to finish the game, so I’d rather have people switch to easy mode in the middle than have them give up entirely. The problem is that it’s hard to communicate. I had to add a message when dying, as well as a dialog when you fail the same level three times in a row.
- Similarly, a lot of players didn’t expect to be able to use their gamepad. This resulted in adding a message at the bottom of the screen. Still, a lot of players just don’t expect browser games to be playable with a gamepad and decide not to use it.
- A lot of players did not want to take the easy (intended) routes at all. Some of them even insisted on playing in nightmare mode. Few players were interested in the easy or super easy modes.
- Some players really wanted to go back to the main menu, some others wanted to be able to reset a level faster, or without dying. This resulted in adding the R and ESC shortcuts.
- Most players run straight into the first camera, despite the clear message saying to avoid them. I actually think this is okay, as it helps them learn what not to do.
- No one actually struggled to understand what to do. It’s probably due to the halo effect and god rays behind the levels’ exits, but everyone naturally tried to reach them. However, since most levels have their exit at the top right, sometimes people would not even realize that a level’s exit was in a different spot. Not a huge issue in my opinion though, I did not make any adjustment.
I roughly spent a week developing the game, and another week doing user testing and making adjustments.
Conclusion
This year’s js13k has motivated me to work on games more regularly (at least more than once a year). I’m really happy with what I was able to achieve, but my biggest takeaway is how valuable user testing is. I’m hoping I’ll be able to do something similar for my next projects.
See you all next year for js13k!