22 February 2010
You should have a basic understanding of the Flash authoring interface, be able to work with movie clips, and know how to create a basic ActionScript 3 project. You should also be familiar with the topics covered in Rendering game assets in ActionScript using blitting techniques and Flash Builder 4.
Intermediate
Blitting is a higher-performance alternative to using the built-in display list in Adobe Flash for drawing objects on the Stage. This technique involves copying the individual pixels of an existing image directly on to the screen—a bit like painting all of your game's spaceships and monsters onto a canvas.
Working manually with pixels is more complicated than using the display list, but the improvement in performance more than makes up for the extra effort. For situations where there are many objects moving around the Stage, you must usually choose either a smooth frame rate or a small memory footprint. With blitting you can have both.
The basic concepts of blitting are covered in Rendering game assets in ActionScript using blitting techniques and Flash Builder 4 by Renaun Erickson. In this tutorial, you'll learn how to cache an animated movie clip symbol as an array of bitmaps and then blit it to the screen, all within the Flash authoring environment.
Note: Throughout this tutorial, I use the term global to refer to any public instance variable that is defined outside of all functions and whose scope is the entire document class.
To start, you'll need a movie clip on the Stage. Think of it as an artist's reference:
package
{
import flash.display.MovieClip;
public class DocumentClass extends MovieClip
{
public var blockFace:BlockFace;
public function DocumentClass()
{
blockFace = new BlockFace();
blockFace.x = 85;
blockFace.y = 160;
addChild(blockFace);
}
}
}
Note: Code changes are highlighted as shown.
package
{
import flash.display.MovieClip;
//import this class
import flash.display.Bitmap;
public class DocumentClass extends MovieClip
{
public var blockFace:BlockFace;
//create a public var to hold the canvas bitmap
public var canvasBitmap:Bitmap;
public function DocumentClass()
{
blockFace = new BlockFace();
blockFace.x = 85;
blockFace.y = 160;
addChild(blockFace);
//create the canvas bitmap, position it on the Stage,
//and add it to the display list
canvasBitmap = new Bitmap();
canvasBitmap.x = 0;
canvasBitmap.y = 0;
addChild(canvasBitmap);
}
}
}
If you test the movie now, you'll see no visible change to the SWF yet. Bitmaps don't contain pixels themselves, BitmapData objects do. Bitmaps merely display the pixels that are inside a given BitmapData object.
package
{
import flash.display.MovieClip;
import flash.display.Bitmap;
//import these two classes
import flash.display.BitmapData;
import flash.geom.Rectangle;
public class DocumentClass extends MovieClip
{
public var blockFace:BlockFace;
public var canvasBitmap:Bitmap;
//assign a new public variable to hold the canvas's pixels
public var canvasBitmapData:BitmapData;
//create a new rectangle the size of the canvas
//(use the Stage's dimensions)
public var canvasRectangle:Rectangle = new Rectangle(0,0,550,400);
public function DocumentClass()
{
blockFace = new BlockFace();
blockFace.x = 85;
blockFace.y = 160;
addChild(blockFace);
canvasBitmap = new Bitmap();
//create the actual BitmapData instance
//(give it the same dimensions as the Stage)
canvasBitmapData = new BitmapData(550, 400);
//fill the canvas's pixels with a rectangle of color
//(the rectangle is as large as the canvas,
//and 0xFFDDDDDD defines the color gray)
canvasBitmapData.fillRect(canvasRectangle,0xFFDDDDDD)
//link the canvas Bitmap to this BitmapData
//(in effect telling it to display these pixels)
canvasBitmap.bitmapData = canvasBitmapData;
canvasBitmap.x = 0;
canvasBitmap.y = 0;
addChild(canvasBitmap);
}
}
}
canvasBitmap.x = 0;
with the following:
canvasBitmap.x = 275;
There are two steps involved in actually blitting an object to the canvas. Essentially, you just draw an image of whatever you want to blit into a new BitmapData object, and then copy the pixels from that object on to the canvas:
DocumentClass() function should now look like this:public function DocumentClass()
{
blockFace = new BlockFace();
blockFace.x = 85;
blockFace.y = 160;
addChild(blockFace);
canvasBitmap = new Bitmap();
canvasBitmapData = new BitmapData(550, 400);
canvasBitmapData.fillRect(canvasRectangle,0xFFDDDDDD)
canvasBitmap.bitmapData = canvasBitmapData;
canvasBitmap.x = 275;
canvasBitmap.y = 0;
addChild(canvasBitmap);
//create new BitmapData to hold image of BlockFace
var blockFaceBitmapData:BitmapData
= new BitmapData(blockFace.width, blockFace.height);
//draw an image of the BlockFace instance into this BitmapData
blockFaceBitmapData.draw(blockFace);
}
copyPixels (appropriately enough) and requires three arguments:blockFace so that the entire image is copied.copyPixels() function on your canvas's BitmapData. Your DocumentClass() function will now look like this:public function DocumentClass()
{
blockFace = new BlockFace();
blockFace.x = 85;
blockFace.y = 160;
addChild(blockFace);
canvasBitmap = new Bitmap();
canvasBitmapData = new BitmapData(550, 400);
canvasBitmapData.fillRect(canvasRectangle, 0xFFDDDDDD)
canvasBitmap.bitmapData = canvasBitmapData;
canvasBitmap.x = 275;
canvasBitmap.y = 0;
addChild(canvasBitmap);
var blockFaceBitmapData:BitmapData
= new BitmapData(blockFace.width, blockFace.height);
blockFaceBitmapData.draw(blockFace);
//create a Rectangle that defines the region of the BlockFace BitmapData to copy
var areaRectangle:Rectangle = new Rectangle();
areaRectangle.width = blockFace.width;
areaRectangle.height = blockFace.height;
//create a Point that defines where on the canvas the pixels of the
//BlockFace BitmapData should be copied to
//(this will the same point on the canvas as the original BlockFace is
//on the stage)
var destinationPoint:Point = new Point(blockFace.x, blockFace.y);
//actually copy the pixels from the BlockFace BitmapData
//to the canvas BitmapData
canvasBitmapData.copyPixels(blockFaceBitmapData,areaRectangle,destinationPoint);
}
import flash.geom.Point up at the top of your code with the other import statements.
This is a good start. However, since you blitted the image just once, only the left face animates. The blitted face doesn't animate for the same reason the screenshots in this tutorial don't animate: the pixels were copied at a specific moment, and they aren't being changed over time.
If you blit the face every frame, it will animate exactly like the movie clip. The easiest way to do that is to move all the blitting code to an ENTER_FRAME event handler:
import statements:import flash.events.Event;
ENTER_FRAME event listener in the constructor by adding the following line just after the line that calls canvasBitmapData.copyPixels(): addEventListener(Event.ENTER_FRAME, onEnterFrame);
DocumentClass() function):public function onEnterFrame(evt:Event):void
{
var blockFaceBitmapData:BitmapData = new
BitmapData(blockFace.width, blockFace.height);
blockFaceBitmapData.draw(blockFace);
var areaRectangle:Rectangle = new Rectangle();
areaRectangle.width = blockFace.width;
areaRectangle.height = blockFace.height;
var destinationPoint:Point = new Point(blockFace.x, blockFace.y);
canvasBitmapData.copyPixels(blockFaceBitmapData,areaRectangle,destinationPoint);
}
onEnterFrame function:blockFace.y = blockFace.y + 1;
Because the blitted image is being copied on top of the old pixels each time, you can still see the old image beneath it. As explained in Renaun's article, you can fix this by simply repainting the whole canvas gray at the start of each frame.
fillRect call you used earlier to paint the entire canvas gray; add the following line at the start of your onEnterFrame() function:canvasBitmapData.fillRect(canvasRectangle, 0xFFDDDDDD);
Good, that fixes it. Now that you know it's not an issue anymore, you can remove the line that moves the face every frame.
blockFace.y = blockFace.y + 1; from onEnterFrame().How about blitting a couple more copies of the face to the canvas? All you need to do is duplicate the actual blitting code from your onEnterFrame() function and change the positions of the new faces:
public function onEnterFrame(evt:Event):void
{
canvasBitmapData.fillRect(canvasRectangle, 0xFFDDDDDD);
var blockFaceBitmapData:BitmapData = new BitmapData(blockFace.width, blockFace.height);
blockFaceBitmapData.draw(blockFace);
var areaRectangle:Rectangle = new Rectangle();
areaRectangle.width = blockFace.width;
areaRectangle.height = blockFace.height;
var destinationPoint:Point = new Point(blockFace.x, blockFace.y);
canvasBitmapData.copyPixels(blockFaceBitmapData,areaRectangle,destinationPoint);
var blockFaceBitmapData:BitmapData = new BitmapData(blockFace.width, blockFace.height);
blockFaceBitmapData.draw(blockFace);
var areaRectangle:Rectangle = new Rectangle();
areaRectangle.width = blockFace.width;
areaRectangle.height = blockFace.height;
//put this blitted face in a different position to the others
var destinationPoint:Point = new Point(blockFace.x + 10, blockFace.y - 120);
canvasBitmapData.copyPixels(blockFaceBitmapData,areaRectangle,destinationPoint);
var blockFaceBitmapData:BitmapData = new BitmapData(blockFace.width, blockFace.height);
blockFaceBitmapData.draw(blockFace);
var areaRectangle:Rectangle = new Rectangle();
areaRectangle.width = blockFace.width;
areaRectangle.height = blockFace.height;
//put this blitted face in a different position to the others
var destinationPoint:Point = new Point(blockFace.x + 20, blockFace.y + 120);
canvasBitmapData.copyPixels(blockFaceBitmapData,areaRectangle,destinationPoint);
}
This is a clumsy way of doing it. Flash will give you a few warnings about duplicate variable definitions but it will compile a working SWF nonetheless (see Figure 5).
The method used to animate the three faces is messy and far from optimal, so in this section you'll learn how to cache these objects, saving the pixels from the original BlockFace movie clip so that you don't have to draw them to three BitmapData objects every single frame.
If you read through that duplicated code in the onEnterFrame() function, you'll see that there are two parts of the code that don't need to be copied and pasted.
The first is the section below, in which the BlockFace object gets drawn to the BitmapData object:
var blockFaceBitmapData:BitmapData = new
BitmapData(blockFace.width, blockFace.height);
blockFaceBitmapData.draw(blockFace);
The second part is the code that defines the rectangle of pixels to copy from the BlockFace object's BitmapData:
var areaRectangle:Rectangle = new Rectangle();
areaRectangle.width = blockFace.width;
areaRectangle.height = blockFace.height;
Delete the second and third occurrences of these lines of code so that they are run only once per frame. This won't affect the SWF, and by removing them you can eliminate the Flash compiler warnings they generated. Also change the second and third instances of var destinationPoint:Point to simply destinationPoint.
The onEnterFrame() event handler should now look like this:
public function onEnterFrame(evt:Event):void
{
canvasBitmapData.fillRect(canvasRectangle, 0xFFDDDDDD);
var blockFaceBitmapData:BitmapData = new BitmapData(blockFace.width, blockFace.height);
blockFaceBitmapData.draw(blockFace);
var areaRectangle:Rectangle = new Rectangle();
areaRectangle.width = blockFace.width;
areaRectangle.height = blockFace.height;
var destinationPoint:Point = new Point(blockFace.x, blockFace.y);
canvasBitmapData.copyPixels(blockFaceBitmapData,areaRectangle,destinationPoint);
//remove the "var" from the front of this statement
//(you've already declared this var a few lines above)
destinationPoint = new Point(blockFace.x + 10, blockFace.y - 120);
canvasBitmapData.copyPixels(blockFaceBitmapData,areaRectangle,destinationPoint);
destinationPoint = new Point(blockFace.x + 20, blockFace.y + 120);
canvasBitmapData.copyPixels(blockFaceBitmapData,areaRectangle,destinationPoint);
}
In fact, you could even move the code concerning areaRectangle into the constructor, rather than having it run every single frame.
What else could be removed? Well, you have to repaint the canvas every frame to stop the trail effect, so that has to stay. Likewise destinationPoint has to be set once for each blitted face and must be updated every frame in case the original face moves, so that has to be kept too.
The rest of the code handles the actual copying of the pixels from the original face to the canvas and it has to be repeated. Obviously that can't be changed or you wouldn't see anything. Or can it?
Remember, that section of the code is in two parts—the pixels aren't copied directly from the face to the canvas. First, the face is drawn onto the BitmapData object as a bunch of pixels; second, the pixels are copied from this BitmapData object onto the canvas. The second part cannot be removed, but the first part could certainly be improved.
Instead of drawing the face to the BitmapData object every frame, you could create a number of BitmapData objects in the constructor and draw one frame of the face's animation to each of them. Then, all the onEnterFrame() function has to do is select the correct BitmapData object and copy the pixels from it to the canvas! That involves more work in the constructor function but it really cuts down on the work done each frame. Even better, it means you can remove the BlockFace movie clip, so Flash Player won't have to render it each frame and all animation will be handled via blitting.
As a test, try caching only the first frame to begin with:
var blockFaceBitmapData:BitmapData = new BitmapData(blockFace.width, blockFace.height);
blockFaceBitmapData.draw(blockFace);
Now you've cached the pixels of the first frame of the animation to blockFaceBitmapData; you're no longer drawing them from the BlockFace movie clip every frame.
blockFaceBitmapData into a global variable, of course, or the code in the onEnterFrame function won't be able to access it.areaRectangle. areaRectangle to a global variable as well. Your code will then look like this:package
{
import flash.display.MovieClip;
import flash.display.BitmapData;
import flash.display.Bitmap;
import flash.geom.Rectangle;
import flash.geom.Point;
import flash.events.Event;
public class DocumentClass extends MovieClip
{
public var blockFace:BlockFace;
public var canvasBitmap:Bitmap;
public var canvasBitmapData:BitmapData;
public var canvasRectangle:Rectangle = new Rectangle(0,0,550,400);
//make blockFaceBitmapData and areaRectangle into public variables, so that both functions can access them
public var blockFaceBitmapData:BitmapData;
public var areaRectangle:Rectangle;
public function DocumentClass()
{
blockFace = new BlockFace();
blockFace.x = 85;
blockFace.y = 160;
addChild(blockFace);
canvasBitmap = new Bitmap();
canvasBitmapData = new BitmapData(275, 400);
canvasBitmapData.fillRect(canvasRectangle, 0xFFDDDDDD);
canvasBitmap.bitmapData = canvasBitmapData;
canvasBitmap.x = 275;
canvasBitmap.y = 0;
addChild(canvasBitmap);
//these lines were moved from onEnterFrame to here
//also, removed the "var" from the start of the first and third lines
blockFaceBitmapData = new BitmapData(blockFace.width, blockFace.height);
blockFaceBitmapData.draw(blockFace);
areaRectangle = new Rectangle();
areaRectangle.width = blockFace.width;
areaRectangle.height = blockFace.height;
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
public function onEnterFrame(evt:Event):void
{
canvasBitmapData.fillRect(canvasRectangle, 0xFFDDDDDD);
var destinationPoint:Point = new Point(blockFace.x, blockFace.y);
canvasBitmapData.copyPixels(blockFaceBitmapData,areaRectangle,destinationPoint);
destinationPoint = new Point(blockFace.x + 10, blockFace.y - 120);
canvasBitmapData.copyPixels(blockFaceBitmapData,areaRectangle,destinationPoint);
destinationPoint = new Point(blockFace.x + 20, blockFace.y + 120);
canvasBitmapData.copyPixels(blockFaceBitmapData,areaRectangle,destinationPoint);
}
}
}
The blitted faces will all appear (and they'll even move with the original face if you re-insert the line that moves it), but since you've only cached one frame, none of them animates. The next step, then, is to cache all of the frames.
Rather than manually create dozens of individual BitmapData objects, you can use an array and a loop to automate the process:
public class DocumentClass extends MovieClip
{
public var blockFaceArray:Array;
addChild(canvasBitmap); call:blockFaceArray = new Array();
You'll use this array to hold a number of BitmapData objects, one for each frame of the BlockFace movie clip.
public function DocumentClass()
{
blockFace = new BlockFace();
blockFace.x = 85;
blockFace.y = 160;
addChild(blockFace);
canvasBitmap = new Bitmap();
canvasBitmapData = new BitmapData(275, 400);
canvasBitmapData.fillRect(canvasRectangle, 0xFFDDDDDD);
canvasBitmap.bitmapData = canvasBitmapData;
canvasBitmap.x = 275;
canvasBitmap.y = 0;
addChild(canvasBitmap);
blockFaceArray = new Array();
//loop through all the frames in the BlockFace movie clip
for ( var i:int = 1; i <= blockFace.totalFrames; i++ )
{
//move the movie clip to the "i-th" frame
blockFace.gotoAndStop(i);
//these two lines have been moved into the loop from below
blockFaceBitmapData = new BitmapData(blockFace.width, blockFace.height);
blockFaceBitmapData.draw(blockFace);
//add the BitmapData object to the array
blockFaceArray.push(blockFaceBitmapData);
}
//reset the animation to its first frame
blockFace.gotoAndPlay(1);
//(note that the two lines that draw an image of BlockFace into a BitmapData
//have been moved from here into the above loop)
areaRectangle.width = blockFace.width;
areaRectangle.height = blockFace.height;
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
You now have an array in which each element is a BitmapData object containing a frame of animation. Next, you need to get these BitmapData objects back out of the array and blit them to the canvas.
onEnterFrame() function:var currentBlockFaceFrame:BitmapData = blockFaceArray[blockFace.currentFrame - 1];
It's very important to subtract 1 from the animation's current frame, since the first frame of an animation is numbered 1 but the first element of an array is numbered 0. Otherwise, when the animation reaches the last frame, there'll be no corresponding element in the array and you'll get an error.
copyPixels() so that they reference this new BitmapData object; for example:canvasBitmapData.copyPixels(currentBlockFaceFrame,areaRectangle,destinationPoint);
Success! All the faces animate.
Now that the blitted faces are animating from a cache, the original movie clip can be removed from the Stage and Flash Player won't have to render it anymore. However, there are a couple of references to blockFace in the onEnterFrame() function that you'll have to eliminate.
First, the code picks which element of the array to use based on the current frame of the movie clip, so you need to store this in another way:
public class DocumentClass extends MovieClip
{
public var currentBlitFrame:int = 0;
onEnterFrame() function to use this new variable rather than blockFace.currentFrame:public function onEnterFrame(evt:Event):void
{
//manually increase this current frame by 1
currentBlitFrame = currentBlitFrame + 1;
//if this value goes past the end of the array, reset it to zero
if ( currentBlitFrame >= blockFaceArray.length )
{
currentBlitFrame = 0;
}
//grab the BitmapData that is stored in the element of the array
//corresponding to the currentBlitFrame variable
var currentBlockFaceFrame:BitmapData = blockFaceArray[currentBlitFrame];
canvasBitmapData.fillRect(canvasRectangle, 0xFFDDDDDD);
var destinationPoint:Point = new Point(blockFace.x, blockFace.y);
//replace the reference to "blockFaceBitmapData" with one to
//the newly-created "currentBlockFaceFrame"
canvasBitmapData.copyPixels(currentBlockFaceFrame,areaRectangle,destinationPoint);
destinationPoint = new Point(blockFace.x + 10, blockFace.y - 120);
//replace the reference to "blockFaceBitmapData" with one to the newly-created "currentBlockFaceFrame"
canvasBitmapData.copyPixels(currentBlockFaceFrame,areaRectangle,destinationPoint);
destinationPoint = new Point(blockFace.x + 20, blockFace.y + 120);
//replace the reference to "blockFaceBitmapData" with one to the newly-created "currentBlockFaceFrame"
canvasBitmapData.copyPixels(currentBlockFaceFrame,areaRectangle,destinationPoint);
}
Note that all the destinationPoint objects are based on the location of blockFace. You can remove this reference by using exact numbers.
var destinationPoint:Point = new Point(blockFace.x, blockFace.y);
with the following line:
var destinationPoint:Point = new Point(85, 160);
destinationPoint = new Point(blockFace.x + 10, blockFace.y - 120);
with the following line:
destinationPoint = new Point(95, 40);
destinationPoint = new Point(blockFace.x + 20, blockFace.y + 120);
with the following line:
destinationPoint = new Point(105, 280);
Finally, there are no references to blockFace outside the constructor.
addChild(blockFace) line and add the following line at the very end of the constructor function (this proves that it is no longer used elsewhere):blockFace = null;
All that's left to do now is move the canvas to its rightful place at the left edge of the screen:
canvasBitmap.x = 275; with canvasBitmap.x = 0;.0xFFFFFFFF instead of 0xFFDDDDDD.blockFace and blockFaceBitmapData variables in the constructor, since they no longer need to be accessed anywhere else.Congratulations! You've managed to manually render and animate a movie clip that was created in Flash by manipulating the individual pixels. OK, so the final result doesn't look like anything you couldn't have achieved using the standard display list, but you've learned a powerful technique that will enable you to improve performance when you need to create more complex animations.
There is much more to blitting than I've covered in this article. Not all situations will be as simple as the example presented here. For example, if you want to cache the animation of a movie clip with varying frame sizes, you'll need to figure out the largest width and height of all the frames and create your BitmapData object at this size. If your movie clip's registration point is not at the top-left corner, you'll need to pass a transform matrix to the draw() function of your BitmapData.
You lose some functionality when blitting, too. Rotating and scaling is tricky; usually the best option is to precache all the frames multiple times, at all the different rotations and scales you'll need. Consider also that the display list allows you to rearrange the z-order of objects, automatically stop/start/skip animations, use 9-slice scaling, and so on. If you need any of this functionality when blitting, you'll have to code it yourself.
On the other hand, blitting allows for some cool graphical effects. For instance, if you apply a 10% fade to your canvas every frame instead of repainting it with a blank background, that trail goes from being an annoying bug to a cool feature.
It's important to remember that blitting is just one tool in your toolbox. Sometimes it will turn out to be the best choice (especially when you need to render a large number of objects and move them around the screen) but sometimes the flexibility of the display list will make that the better choice instead.
Here are some great resources for further reading:

This work is licensed under a Creative Commons Attribution-Noncommercial 3.0 Unported License.
| 04/23/2012 | Auto-Save and Auto-Recovery |
|---|---|
| 04/23/2012 | Open hyperlinks in new window/tab/pop-up ? |
| 04/21/2012 | PNG transparencies glitched |
| 04/01/2010 | Workaround for JSFL shape selection bug? |
| 02/13/2012 | Randomize an array |
|---|---|
| 02/11/2012 | How to create a Facebook fan page with Flash |
| 02/08/2012 | Digital Clock |
| 01/18/2012 | Recording webcam video & audio in a flv file on local drive |