Launching Pad blog
How we made Boots run more smoothly using cacheAsBitmap and object pools
Run Boots Run was created partly to help us make the transition to Flash and ActionScript from some other game toolkits that we’ve used in the past. There was a lot to learn even for such a simple game, and some of it has only become obvious now, a month after the initial release. These optimisation hints might be obvious to Flash experts, but read on if you’re an ActionScript newbie like me.
cacheAsBitmap
The most important key to smoothness in the game is a DisplayObject property called cacheAsBitmap. Lots of display-oriented classes in Flash subclass DisplayObject, including the all important MovieClip and Sprite.
Why is cacheAsBitmap important? Vector graphics are very expensive to draw (in CPU terms) and as we all know, Flash works primarily with vector graphics. All of the graphics in Run Boots Run are described as combinations of vector lines and shapes rather than as a bitmap bunch of dots.
Fortunately for Flash, vector graphics can look amazingly cool, and can zoom to any size without losing quality. Unfortunately for Flash, bunches of dots are much quicker to draw than lines and shapes. This is because the computer can copy a whole clump of memory (containing the dots) wholesale rather than painstakingly calculating what all those lines and shapes are supposed to look like.
Setting cacheAsBitmap to true makes Flash keep a copy of what your DisplayObject (e.g. Sprite or MovieClip) looks like onscreen in terms of a bitmap image. As long as you don’t change the DisplayObject’s contents, scale it or rotate it, Flash knows that it can keep using those same bitmap dots, even if the DisplayObject is moved around. It doesn’t have to draw all those lines and shapes from scratch each time.
Now, this property is no good for Boots herself, because she is constantly animated as she runs and jumps. Her bitmap representation would need to be constantly changing, so there is no point keeping a cached representation. Level blocks and collectable diamonds, however, are perfect candidates — all they ever do is scroll across the screen. Blocks and Collectables are both subclasses of LevelElement, which begins like so:
public function LevelElement() {
cacheAsBitmap = true
}
(Yes, I don’t use semi-colons in my ActionScript. Horrors! I might cover the reasons for this in a later post, but if you just threw up in your mouth a little… er, get a glass of water?)
Easy, eh? Just making this tiny change took the framerate from around 40 right up to about 58 on my MacBook Pro. I also bitmap-cached the layers in the parallax background for good measure:
public function Layer(child) {
this.speed = child.speed
childPrototype = child.clone()
addChild(child)
addLoops()
cacheAsBitmap = true
}
Note that I didn’t cache either the level itself or the entire parallax background. Both of these are constantly changing: diamonds are being collected from the level, and the background’s various layers are moving around within it. Their cached bitmaps would need to be constantly re-updated. Luckily, it’s more than good enough to just have caches of their individual ingredients: LevelElements and Layers.
Check out this good introduction from Adobe to learn more.
Unfortunately, our massive improvement in framerate uncovered a second problem, which hadn’t been so obvious before. Every few seconds, the framerate would drop noticeably, making everything stop, then skip forward nastily. (We use time-based rather than frame-based animation in Run Boots Run. This may be another topic for another post.)
If you’ve done much game coding in ActionScript — or most other object-oriented languages — you can probably guess what the problem was.
Object pools
The continuous, never-ending level in the game is put together on the fly from level ‘chunks’. Initially, I wanted chunks to be able to be an arbitrary width and be able to be snapped together in any order: even the same one twice, if required. This lead to a system whereby chunks were instantiated on the right side of the screen and deleted as they finished moving off the left.
Now, there’s nothing wrong with this from an object-oriented programming point of view. You need an object somewhere? You instantiate it. Finished with it? You delete it. Unfortunately, in the speed-critical world of games, sometimes this isn’t a good idea. Instantiating a great big level chunk takes time, and deleting it means that memory will need to be reclaimed by the garbage collector, which takes more time. All of this time adds up to big fat framerate drops.
So what’s the solution? As it turns out, Tristan created all of our level chunks to be wider than the screen, and we decided that we definitely didn’t want chunks repeating immediately. This opened the door for the use of a simple object pool. (It’d still be possible with the original constraints, but we would have to make sure we had more than one instance of each chunk in the object pool and keep track of each instance more carefully.)
An object pool is created at the start of the game (or level, or whatever is appropriate for your game) and contains instantiated versions of the live objects you’ll be using. Rather than creating and deleting these objects, you simply change their parameters and continually reuse the same objects.
Here’s how it works for us. At the start of the game, we instantiate all the chunks we’ll be using:
private var classes:Array
private var chunks:Array
public function ChunkController() {
classes = new Array(Chunk1, Chunk2, Chunk3, Chunk4, Chunk5, Chunk6)
chunks = new Array()
for each (var klass in classes) {
chunks.push(new klass())
}
}
Okay, it’s a little clumsy. It’s got some suggestions of the old method still in that code, and chunk class names are un-prettily added by hand. It gets the job done, though. Anyway, you can see that chunks is our object pool for level chunks.
The main LevelController continually asks ChunkController for more level chunks from its pool. LevelController adds these offscreen to the right (with addChild(chunk)), and removes them once they’ve passed off the left side (with removeChildAt(0)). Note that once a chunk has been removed from the LevelController, it is still instantiated and a reference to it exists in the object pool. (In any more complex situation, it’d be a good idea to remove the reference from the pool just before adding the child, and re-append it to the pool just after deleting it.)
If the LevelController asks for that same chunk again later, it simply needs to ‘reset’ to its original configuration and it’s ready to go again. For a level chunk in Run Boots Run, reseting mainly means setting its x position to zero again, as this is where the LevelController expects it to be.
It’s not quite enough to just reset the position, though. Since diamonds get collected during gameplay, and since those diamonds are children (in Flash display terms) of level chunks, we need to reinstate the original diamonds. Here is the code for a chunk reset. Note that startingCollectables holds an array of the diamonds in their starting positions, and collectables holds an array of the chunk’s current diamonds:
public function reset() {
x = 0
for each (var collectable in _collectables) {
removeChild(collectable)
}
_collectables = new Array()
for each (collectable in _startingCollectables) {
_collectables.push(collectable)
addChild(collectable)
}
}
As you’ve no doubt inferred, our code could further optimised. However, these two tricks have given us a mighty speed boost all on their own.
