The thoughts and opinions expressed are those of the writer and not Gamasutra or its parent company.
[This article is a technical summary of the curves, ratios and dimensions in the Flash version of Canabalt. Reader beware - contains spoilers and lots of numbers!]
A friend asked recently if I'd thought about putting the source code to my Flash game Canabalt up on the web for people to pick apart and study. He thought it would be handy for people to see just how the player physics were set up, etc. I still feel a little leery about putting all the source up for a couple of reasons, but mainly this: source code for a game written in 5 days is a hellish wasteland of spaghetti balls that isn't gonna help anybody. So, in honor of Canabalt's first birthday (back on September 1st), why not write a guided tour of how everything works instead?
One of my goals with Canabalt was to make a game that really felt fast, like blazing, sweaty-palms fast. It turns out this isn't that easy to do in 2D, because the horizon is so close. 3D games get to cheat a little, by letting you stare off into the distance in a way that 2D games tend to struggle with. You can zoom the camera out, so that player can see more of the world, but if you do that then objects appear to be moving more slowly. It's a really tricky balance.
Our view of Canabalt is 480 pixels wide by 160 pixels tall. The normal web version of Canabalt displays at twice that resolution (960x320) while the MEGA build displays at three times that size. But the game itself is running at 480x160 pixels, and it can't hurt to keep that in mind as all sizes and speeds mentioned throughout this article will be in pixels as well.
Why 480x160? Well, it helps us get around this annoying problem with apparent speed of objects in the world. It's just the right ratio to let the players look pretty far ahead (helping them react to oncoming challenges more effectively) and keep the viewing window nice and tight, for that intense sense of velocity. But more on speeds later!
The view may be 480x160, but the world of Canabalt goes from 0 to essentially infinity, and it's 320 pixels tall. I really wanted it to have a little bit of vertical scrolling because it makes the parallax buildings in the background stand out a little more - instead of just moving faster or slower, you can see them shift up and down and it helps it feel like there's a lot more depth there. Obviously there's more vertical range for the player to explore as well - you can have bigger drops and larger facades with that extra space. Big drops are one of the most show-off-y stunts in the world of parkour/free-running, so I definitely wanted those in there.
The runner is a 24x24 sprite, but his bounding box is smaller than that (more on him in the next section). The buildings are all more or less on a 16x16 pixel grid, but I'll cover them in more depth later as well. The scrolling backgrounds are created from three 480x320 images. I used two images for the closest background layer, effectively making it a 960px wide image (for extra variety). I used just one image (with two instances) for the farther, slower moving, less noticeable background layer.
In Flixel (the Flash library that powers Canabalt), objects can be assigned a "scroll factor", which refers to how much the object should move in comparison to everything else. Essentially, foreground objects get a scroll factor of (1,1) - meaning if the object's position increases by 20 pixels horizontally, that object will change position on the screen by 20 pixels. If the scroll factor was (0.5, 1) then it would only move by 10 pixels. The scroll factors for the background layers in Canabalt are (0.15,0.1) for the farthest layer, and (0.4,0.2) for the closest layer. Partly this puts some distance between the player and the alien invaders in the background, but it also makes sure that the buildings are never whizzing by TOO fast, even when the player is reaching supersonic speeds.
One of my goals when I was building Canabalt was to emulate the animation from step platforming games I grew up with, like Flashback or Prince of Persia. These games had realistic, rotoscoped animations with a lot of frames, and have a very distinctive, "smooth" look. In Canabalt, the runner only has 4 animations (running, rolling, jumping up, and falling down) but he's packing 38 individual frames. About half of those frames are devoted just to the running animation.
As we mentioned before, the runner in Canabalt is a 24x24 pixel sprite. For comparison, this is a little taller than regular Mario in Super Mario Bros., but a little shorter than super Mario - and a little bit wider than both. Another way to look at it is the character is about 15% as tall as our view on the world. As you can see, the character himself doesn't use up that whole 24x24 box - a lot of that space is to let him tumble and swing his arms around during his jumps. Also, note the runner's "hitbox" ("real size" as far as the game engine is concerned) - it's actually quite a bit smaller, just 12x14. And, as you can see in the image, this "real size" doesn't just float in the middle of the character, it is snapped to the bottom of the image and then offset quite a bit to the left.
Hitbox tuning is a really fundamental part of game feel, even if your game is using circles and not strictly boxes. Tweaking the hitbox can help make your game more forgiving or more challenging, depending on what you're going for. In the case of the player, I wanted to make it more forgiving - but that doesn't just mean making it smaller or bigger.
Since the runner spends most of his time jumping from rooftop to rooftop and hopping over boxes (or trying to anyways), I tried to modify his hitbox to make those activities as easy and natural as possible. There isn't much hitbox out in front of the player, so you can get awfully close to the boxes without actually triggering a collision and stumbling. Likewise, with the box hanging off the player's left side quite a bit, you can pretty well run right off the side of a building and still leap at the last moment. All of this just provides a little extra cushioning for people's reaction times, which gives me more latitude to add more exciting challenges without frustrating people too badly.
Oh one last thing - the obstacles themselves have funny looking hitboxes too. They're just 2 pixels tall! This way if the player is pretty much jumping at all, they'll easily clear the box. It's all about making things "feel right", and getting rid of those moments when the player says, "What!! I SO MADE THAT, what the f-".
The Player's Motion
Like we talked about earlier, the screen is 480 pixels wide by 160 pixels tall. The fastest possible speed for the player is 800 pixels per second, which in game scale translates to something like 80 meters per second, or 286 kilometers per hour! That's pretty fast. But with a 480 pixel wide view, with the player on the extreme left, the player still has more than half a second (480/800) to react to whatever the game throws at them. This is a pretty good window, the limits of human reflexes for unexpected events is somewhere in the neighborhood of 0.3 seconds I think? According to hardcore Street Fighter players anyways. So even at it's most intense, Canabalt is, technically, playable by humans.
But you don't just run 800 pps (pixels per second) straight out of the box. Initially, the runner only moves at 100 pps. He also has a tiered acceleration curve - between 0 pps and 100 pps, the runner accelerates at 50 pps. Between 100 and 250, he accelerates at 30 pps. Between 250 and 400, 20, and between 400 and 800 he accelerates at just 10 pps. Basically, the faster you go, the slower you speed up. This means it takes a while to get up to top speed, but after stumbling you can get back up to medium speed pretty quickly. Obstacles in Canabalt (chairs, boxes, etc) reduce the players speed by 30%. If you were running at say 300 pps (accelerating at 20 pps) when you stumble over an obstacle, your speed is reduced instantly to just 210 pps, and your acceleration is bumped up to 30 pps.
The runner also has some fairly finicky rules that govern how he jumps, too. In Canabalt, the longer you hold the jump button, the higher you can jump. The way this is implemented is we start a timer when the player first presses the jump button, and as long as the timer hasn't timed out, and the button is still pressed, we assign the runner a vertical velocity of 300 pps (technically, -300, since negative is up in 2D computer imaging coordinates, but whatevers).
But that's not all! The length of the timer varies based on your running speed. The slower you're running, the faster the timer runs out, regardless of how long you are pressing the button. The effect we get from that is the fast the player runs, the longer that constant vertical speed is applied, and the longer they'll stay in the air. The maximum value the timer can have is 0.35 seconds. I also added a slight modifier - if the timer is pretty much brand new (less than 0.08 seconds have passed since the button was pressed) then the runner's vertical velocity is set to just 200 pps, instead of 300. This helps a lot with letting players pull off the hop when they tap the button lightly, but isn't enough to stand out or look weird then the button is pressed and held for a normal leap.
Even when the prototype was still pretty new, I was fairly certain that it would involve rooftops. That fantasy just suited the mechanics perfectly; the very earliest prototypes were just big green boxes with gaps between them. It took very little imagination to see the little white box as a dude, and the big green boxes as buildings, the sizes were just right. I ended up working out a system of roof, wall, and "filler" 16x16 tiles that could be arranged and repeated and combined in different ways to satisfy the graphical requirements of that setting.
(For active Flixel users: I created a custom object based on the FlxTileblock object that would allow me to randomly tile the interior sections of the buildings, whilst specifying specific tiles to use on the endcaps of each block. Most buildings were rendered using just two custom FlxTileblocks each, not counting the roof silhouettes. Buildings with hallways used I think seven, instead of two).
There are a lot of different systems at work generating the city though, and I think the graphical part is the easiest to reverse engineer, so I'm not going to dwell on that. The key thing that happens in the city generation is that the algorithm is dependent on player velocity. That means that the game decides what the next building should be like based on how fast you're going. This is why there can be obstacles you stumble over without just losing the game right away. And it turns out this is pretty simple to implement; the tricky part was figuring out all the right sort of sizes for things.
Some of the sizes and parameters for the structures are based on the size of the building tiles, some are based on the size of the game window, and some are based on the player velocity. For example, the maximum size of gap between this building and the next one is roughly two thirds the horizontal speed of the player. This means that most of the time, players can safely hit one crate (a 30% speed reduction) and still make the jump, even if the game generates the maximum amount of space between rooftops.
The height of the (accursed) windows, and subsequent hallways, is based on player speed too. Depending on the player's speed, the hallway (and thusly the windows) can be anywhere from 48 (under 320 pps) to 96 (over 640 pps) pixels tall. The minimum width of a building at any point is initially calculated to be the width of the screen minus the size of the gap, though it can't go below 96. The maximum possible width of the next building is twice the minimum width, and the game randomly selects a dimension from within those bounds. Likewise, there are bounds on the maximum "drop" from one roof to another (160 pixels - the height of the screen).
Of course, not all buildings are equal - at various intervals, Canabalt will insert bombs, cranes, windows, and collapsing buildings to keep things exciting. Not only do these things add visual variety and atmosphere, but they keep players asking a really important question: "What's the next building going to look like?" Behind the scenes, any time a "specialty" building is inserted, we pick when the next one will appear - anywhere from 3 up to 8 normal buildings will be generated before the next special event appears. This was easy to do and seemed to strike a nice balance: if each building averages say one and a half screenwidths, and we're averaging 6 normal buildings per special building, then that's an average of over 4000 pixels between each special event. At a median speed of 400 pixels per second, that ends up being something like an average of 10 seconds of "rest", or normal buildings, between each panic/crisis rooftop.
Finally, like so many other crucial elements in Canabalt, buildings have hitbox tweaks too. Buildings are just big boxes, but they have a little bit (8 pixels, to be exact) of "Merry Melodies" space hanging off the right hand side. That is, the "real size" of the building, or the way the game sees the building, is just a little wider than it looks. This is just another form of forgiveness for the human player who happens to tap the button just a split-second late. We're giving them the benefit of the doubt here: they wanted to make that jump, they were just the tiniest bit late. Getting rid of those cases, the times where you really feel like you should have made it, can make a world of difference in how "fun" a game feels. Especially when the punishment for missing a jump is instant death!
Hopefully you guys find this interesting or useful! I wanted to add that Canabalt was not engineered in a deliberately analytical way; I didn't actually calculate the runner's ideal speed based on the aspect ratio when I was prototyping the thing. I just did what we usually do, which is keep trying different numbers until it "feels right". What I am hoping to do here is to expose at least some of the reasons why certain things in Canabalt "feel right." If I have managed to accomplish that then huzzah!