Posts tagged ‘Performance’

Double-buffering with ActionScript 3

Ever wondered why, when you create a Flash game or animation that requires a lot of movement, Flash’s performances aren’t so great? Ever wondered how come other people can make theirs behave correctly? The answer is simple: double buffering. If you are not familiar with the term, here’s a simple explanation:

The principle of double buffering is to create two separate buffers to render your graphics. The first buffer, called the “Frame Buffer” is used to build out your frame. The second buffer, called the “Render Buffer”, is used for display only. Let’s say, for example, that you have 10,000 objects on your stage, that are animating. Normally, Flash would freak out, even with a resolution of 550×400, giving you a very poor performance. Have you ever tried NOT displaying anything on the stage, but keep the simulation running? Yep! Flash runs at 30fps, like it was set to. Flash’s weak point is the rendering. Let’s set that heavy lifting aside and tell Flash to render differently. In a lot of scenarios, perfomance will be more important than graphics sharpness or scalability. Already, taking away the vector graphics and replacing it with bitmap graphics enhances Flash’s performance greatly. But, we’re not out of the woods yet. Flash still can’t animate 10,000 objects simulatneously and keep a good framerate. For Flash to render properly, you will need a “camera”. The camera is simply your field of view, the boundaries of the visible stage. Your main movie clip animating might be 40,000×40,000 pixels large, but your stage is 550×400, or 1024×768, or whatever size you decide to give it. Just not 40,000×40,000. By moving the “camera”, you simply move the X and Y coordinates of the animating movie clip in the opposite direction, so it looks like the user is actually moving the camera over a 2D plane.

The concept is to replace Flash’s timeline timer completely and make it do what you want. Even if you have animated movie clips on your stage, they will all need to be stopped. All the frames of all objects will need to be rendered once, at the beginning and stored somewhere, as bitmap data. So, the first thing we’ll do is loop through them all, loop through all their frames, and take a bitmap snapshot of each frame and store them into a mutli-dimensional array, like so:

private var renderedAssets:Array = new Array();
private var assets:Array = new Array();

// Cycling through the base assets and create the original objects, which will be used to populate the
// other arrays.
for (var i:int = 0; i < base_assets.length; i++) {
    // Creates a reference to the class reference contained in the array.
    var baseClass:Class = base_assets[i] as Class;
    // This creates the actual dynamic object.
    var baseObj:MovieClip = new baseClass();

    // Cycling through all the dynamic objects, extracting their frames.
    // Skipping the first frame, as it is the empty tile.
    for (var j:int = 0; j < (baseObj.totalFrames - 1); j++) {
        // Initializing the renderedAssets array.
        renderedAssets[assets.length] = new Array();
         // Moving to the correct frame before taking a bitmap snapshot
         baseObj.gotoAndStop(j+2);
         // Check if the child object in this frame is a movie clip. If it's not, we know there is no need
         // to cycle through its frames to extract the bitmap information. There's only 1 frame.
         if (baseObj.getChildAt(0) is MovieClip) {
             // Checking to see if there is more than one frame. If not, we know there is only one and can extract
             // that single frame without a loop.
             if ((baseObj.getChildAt(0) as MovieClip).totalFrames > 1) {
                // Looping through all the frames within the base object's child
                for (var k:int = 0; k < ((baseObj.getChildAt(0) as MovieClip).totalFrames - 1); k++) {
                    // Postioning the timeline to the right frame
                    (baseObj.getChildAt(0) as MovieClip).gotoAndStop(k+2);
                    renderedAssets[assets.length][k] = getAssetBitmap(baseObj.getChildAt(0) as MovieClip);
                }
            }
            // Only 1 frame
            else {
                // Adding the rendered snapshot to the rendered assets array.
                renderedAssets[assets.length].push(getAssetBitmap(baseObj.getChildAt(0)));
            }
        }
        // Not a MovieClip, only 1 frame
        else {
            // Adding the rendered snapshot to the rendered assets array.
            renderedAssets[assets.length].push(getAssetBitmap(baseObj.getChildAt(0)));
        }
        // Add the object to the assets repository array.
        assets.push(baseObj);
    }
    // Clear the baseObj object (free memory)
    baseObj = null;
}

// Populating the grid with 10000 objects.
for (i = 0; i < 10000; i++) {
    // If this is a new row, create a new array and put it in.
    if (grid[(i % 100)] == null) {
        grid[(i % 100)] = new Array(100);
    }
    var randNum:int = (Math.floor(Math.random() * assets.length));
    // Add a random number, associated with the index of the assets repository array.
    grid[(i % 100)][Math.floor(i / 100)] = randNum;
}

private function getAssetBitmap(baseObj:DisplayObject, secondObj:MovieClip = null):BitmapData
{
	// Creating an empty BitmapData, the size of the asset.
	var tmpBMData:BitmapData = new BitmapData(baseObj.width, baseObj.height);
	// Taking the bitmap snapshot of the current frame.
	tmpBMData.draw(baseObj);

	if (secondObj != null) {
   	tmpBMData.draw(secondObj, new Matrix(1, 0, 0, 1, (baseObj.width / 2 - secondObj.width / 2), (baseObj.height / 2 - secondObj.height / 2)));
	}
	// Returning the rendered snapshot.
	return tmpBMData;
}

Setup the stage to listen to the ENTER_FRAME event. The callback will become your application’s main loop.

addEventListener(Event.ENTER_FRAME, renderFrame);

In your main loop, you will determine which frame you are on by increasing a counter.

private function renderFrame(evt:Event):void
{
	frameCounter++;
}

Knowing where all our objects are supposed to be positioned, we can begin building our frame buffer. Looping through each individual objects that is supposed to be animating, or be present on the stage, determine if they should be visible, according to their X and Y coordinates, as well as their width and height. If you have determined that the object should NOT be visible at that time, skip it completely. One down! 9,999 to go!

Once we reach an object that needs to be shown (even 1 pixel of it!), we need render it. To do that, we’ll need a Bitmap data from that particular object, of its current state. We will then position the movie clip’s frame to (totalFrameCounter % mcNumFrames). That is, the total number of frames we went through in the application main loop and divide it by the number of total frames in the animating movie clip. Move to the rest of the division. Here is the function I wrote to determine that:

private function buildFrame(frameCounter:uint):BitmapData
{
    // Create a new BitmapData, the size of the stage.
    frameBuffer = new BitmapData(stage.stageWidth, stage.stageHeight);
    // Cycle through all the grid elements
    for (var i:int = 0; i < 10000; i++) {
        var gridValue:uint = grid[(i % 100)][Math.floor(i / 100)];
        // If the element we're on is supposed to be visible, according to the camera position, we need to render.
        if (
            (((i % 100) * (cellSize * (renderedAssets[gridValue][0].width / cellSize))) + cameraX) < stage.stageWidth &&
            ((Math.floor(i / 100) * (cellSize * (renderedAssets[gridValue][0].width / cellSize))) + cameraY) < stage.stageHeight &&
             ((i % 100) * (cellSize * (renderedAssets[gridValue][0].width / cellSize))) >= (-(cameraX) - (cellSize * (renderedAssets[gridValue][0].width / cellSize))) &&
            (Math.floor(i / 100) * (cellSize * (renderedAssets[gridValue][0].width / cellSize))) >= (-(cameraY) - (cellSize * (renderedAssets[gridValue][0].width / cellSize)))

        ) {
            // Positions the point to the right position in the frame buffer
            pt.x = (((i % 100) * (cellSize * (renderedAssets[gridValue][0].width / cellSize))) + cameraX);
            pt.y = ((Math.floor(i / 100) * (cellSize * (renderedAssets[gridValue][0].width / cellSize))) + cameraY);

            try {
                // Calculates which frame needs to be showing. Since the assets might not have the same amount of frames,
                // the modulo takes care of that by returning a relative position.
                var frame:uint = frameCounter % renderedAssets[gridValue].length;
                // Copies the actual pixels into the frame buffer.
                frameBuffer.copyPixels(renderedAssets[gridValue][frame], rect, new Point(pt.x, pt.y));
            }
            // Oh shit!
            catch (e:Error) {
            }
        }
    }
    // Return the frame buffer.
    return frameBuffer;
}

Modulo (the % sign) is a mathematical operator that will perform an integer division and return the rest. For example, 7 % 3 will return 1. 7 divided by 3 will obviously return a fraction so, we’ll take the “floor” value of that fraction, and multiply it by the divider (which is 3). The floor of 7 / 3 is 2. 2 multiplied by 3 is 6. 7 minus 6 = 1. It is a pretty complicated mathematical operation, but ActionScript takes care of all that for you.

Once all your visible assets are determined and rendered to the frame buffer, it’s time to take a big screenshot of that framebuffer and push that to the render buffer. Let’s add more code to our rendreFrame function:

private function renderFrame(evt:Event):void
{
	// Build the frame buffer.
    frameBuffer = buildFrame(frameCounter);
    // Clone the frame buffer into the render buffer.
    renderBuffer = frameBuffer.clone();
    // Destroy the frame buffer.
    frameBuffer.dispose();

    // Draw the render buffer into the document class' graphics using the BitmapFill.
    // Since we're already dealing with bitmap data, no need to create heavy objects such as flash.display.Bitmap.
    this.graphics.clear();
    this.graphics.beginBitmapFill(renderBuffer);
    this.graphics.drawRect(0, 0, stage.stageWidth, stage.stageHeight);
    this.graphics.endFill();

 	frameCounter++;
}

Now, every time the application enters a frame, all those operations will be performed. Tada! We now have a double buffering application. As long as Flash can compute the coordinates of all the objects we need to animate, we should get a decent framerate.

To make a long story short, instead of making Flash “Render object -> Display object, Render object -> Display object” 10,000 times, we do “Render object, Render object, Render object, … , Display visibible objects”. Makes much more sense.

I attached a test project I wrote using that principle. The code is pretty detailed. It uses Simcity Classic assets (Micropolis). Some of them are animated, some of them are not. You’ll see!

DoubleBuffering.zip

Thanks for reading and please, feel free to leave comments!