I'm creating a flash game in actionscript 3 with an infinite universe. Because the universe is infinite the background is created dynamically using the following background engine:
BackgroundEngine.as
package com.tommedema.background
{
import br.com.stimuli.loading.BulkLoader;
import com.tommedema.utils.Settings;
import com.tommedema.utils.UtilLib;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.utils.Timer;
public final class BackgroundEngine extends Sprite
{
//general
private static var isLoaded:Boolean = false;
private static var bulkLoader:BulkLoader = new BulkLoader("backgroundEngine");
private static var assetsBitmapData:Array = [];
private static var drawTimer:Timer;
//objects
private static var masterContainer:Sprite;
private static var containers:Array = [];
//stage
private static var stageWidth:uint;
private static var stageHeight:uint;
private static var stageCenterX:Number;
private static var stageCenterY:Number;
//moves the background's X and Y coord
public static function moveXY(xAmount:Number, yAmount:Number):void
{
if (!masterContainer) return;
if (xAmount != 0) masterContainer.x += xAmount;
if (yAmount != 0) masterContainer.y += yAmount;
}
//returns whether the background engine has been loaded already
public static function loaded():Boolean
{
return isLoaded;
}
//loads the background engine
public final function load():void
{
if (isLoaded) return;
UtilLib.log("BackgroundEngine load.");
//set stage width, height and center
stageWidth = stage.stageWidth;
stageHeight = stage.stageHeight;
stageCenterX = stageWidth / 2;
stageCenterY = stageHeight / 2;
//load drawing timer
drawTimer = new Timer(Settings.BG_DRAW_IV);
drawTimer.addEventListener(TimerEvent.TIMER, updateBackground, false, 0, true);
drawTimer.start();
//retrieve background assets
if ((bulkLoader.get("background/4.png")) && (bulkLoader.get("background/4.png").isLoaded))
{
loadAssets();
}
else
{
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/1.gif", {id: "background/1.gif"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/2.png", {id: "background/2.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/3.png", {id: "background/3.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/4.png", {id: "background/4.png"});
bulkLoader.addEventListener(BulkLoader.COMPLETE, assetsComplete, false, 0, true);
bulkLoader.start();
}
//set isLoaded to true
isLoaded = true;
}
//unloads the background engine
public final function unload():void
{
if (!isLoaded) return;
UtilLib.log("BackgroundEngine unload method has been called.");
//unload drawTimer
drawTimer.removeEventListener(TimerEvent.TIMER, updateBackground);
drawTimer.stop();
drawTimer = null;
//clean the asset array
assetsBitmapData = [];
//remove containers
for each (var container:Sprite in containers)
{
if (container)
{
masterContainer.removeChild(container);
container = null;
}
}
containers = [];
//remove master container
if (masterContainer)
{
removeChild(masterContainer);
masterContainer = null;
}
//set isLoaded to false
isLoaded = false;
}
//updates the background
private final function updateBackground(event:TimerEvent):void
{
if (masterContainer)
{
collectGarbage();
drawNextContainer();
}
}
//poller function for drawing next background squares
private static function drawNextContainer():void
{
var curContainer:Sprite = hasBackground(stageCenterX, stageCenterY);
if (curContainer)
{
if (!hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY - stageHeight * 0.75)) //top left
drawNewSquare(curContainer.x - curContainer.width, curContainer.y - curContainer.height);
if (!hasBackground(stageCenterX, stageCenterY - stageHeight * 0.75)) //top
drawNewSquare(curContainer.x, curContainer.y - curContainer.height);
if (!hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY - stageHeight * 0.75)) //top right
drawNewSquare(curContainer.x + curContainer.width, curContainer.y - curContainer.height);
if (!hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY)) //center left
drawNewSquare(curContainer.x - curContainer.width, curContainer.y);
if (!hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY)) //center right
drawNewSquare(curContainer.x + curContainer.width, curContainer.y);
if (!hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY + stageHeight * 0.75)) //bottom left
drawNewSquare(curContainer.x - curContainer.width, curContainer.y + curContainer.height);
if (!hasBackground(stageCenterX, stageCenterY + stageHeight * 0.75)) //bottom center
drawNewSquare(curContainer.x, curContainer.y + curContainer.height);
if (!hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY + stageHeight * 0.75)) //bottom right
drawNewSquare(curContainer.x + curContainer.width, curContainer.y + curContainer.height);
}
}
//draws the next square and adds it to the master container
private static function drawNewSquare(x:Number, y:Number):void
{
containers.push(genSquareBg());
var cIndex:uint = containers.length - 1;
containers[cIndex].x = x;
containers[cIndex].y = y;
masterContainer.addChild(containers[cIndex]);
}
//returns whether the given location has a background and if so returns the corresponding square
private static function hasBackground(x:Number, y:Number):Sprite
{
var stageX:Number;
var stageY:Number;
for(var i:uint = 0; i < containers.length; i++)
{
stageX = masterContainer.x + containers[i].x;
stageY = masterContainer.y + containers[i].y;
if ((containers[i]) && (stageX < x) && (stageX + containers[i].width > x) && (stageY < y) && (stageY + containers[i].height > y)) return containers[i];
}
return null;
}
//polling function for old background squares garbage collection
private static function collectGarbage():void
{
var stageX:Number;
var stageY:Number;
for(var i:uint = 0; i < containers.length; i++)
{
if ((containers[i]) && (!isRequiredContainer(containers[i])))
{
masterContainer.removeChild(containers[i]);
containers.splice(i, 1);
}
}
}
//returns whether the given container is required for display
private static function isRequiredContainer(container:Sprite):Boolean
{
if (hasBackground(stageCenterX, stageCenterY) == container) //center
return true;
if (hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY - stageHeight * 0.75) == container) //top left
return true;
if (hasBackground(stageCenterX, stageCenterY - stageHeight * 0.75) == container) //top
return true;
if (hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY - stageHeight * 0.75) == container) //top right
return true;
if (hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY) == container) //center left
return true;
if (hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY) == container) //center right
return true;
if (hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY + stageHeight * 0.75) == container) //bottom left
return true;
if (hasBackground(stageCenterX, stageCenterY + stageHeight * 0.75) == container) //bottom center
return true;
if (hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY + stageHeight * 0.75) == container) //bottom right
return true;
return false;
}
//dispatched when all assets have finished downloading
private final function assetsComplete(event:Event):void
{
loadAssets();
}
//loads the assets
private final function loadAssets():void
{
assetsBitmapData.push(bulkLoader.getBitmapData("background/1.gif")); //stars simple
assetsBitmapData.push(bulkLoader.getBitmapData("background/2.png")); //star bright
assetsBitmapData.push(bulkLoader.getBitmapData("background/3.png")); //cloud white
assetsBitmapData.push(bulkLoader.getBitmapData("background/4.png")); //cloud red
init();
}
//initializes startup background containers
private final function init():void
{
masterContainer = new Sprite(); //create master container
//generate default background container
containers.push(genSquareBg()); //top left
containers[0].x = 0;
containers[0].y = 0;
masterContainer.addChild(containers[0]);
//display the master container
masterContainer.x = -(stageWidth / 2);
masterContainer.y = -(stageHeight / 2);
masterContainer.cacheAsBitmap = true;
addChild(masterContainer);
}
//generates a background square
private static function genSquareBg():Sprite
{
var width:Number = stageWidth * 2;
var height:Number = stageHeight * 2;
var startX:Number = 0;
var startY:Number = 0;
var scale:Number;
var drawAmount:uint;
var tmpBitmap:Bitmap;
var i:uint;
//create container
var container:Sprite = new Sprite();
//show simple stars background
tmpBitmap = UtilLib.copyDataToBitmap(assetsBitmapData[0], false, 0x000000);
tmpBitmap.x = startX;
tmpBitmap.y = startY;
container.addChild(tmpBitmap);
//draw bright stars
drawAmount = UtilLib.getRandomInt(1, 2);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = UtilLib.copyDataToBitmap(assetsBitmapData[1], true, 0x000000);
tmpBitmap.alpha = UtilLib.getRandomInt(3, 7) / 10;
tmpBitmap.rotation = UtilLib.getRandomInt(0, 360);
scale = UtilLib.getRandomInt(3, 10) / 10;
tmpBitmap.scaleX = scale; tmpBitmap.scaleY = scale;
tmpBitmap.x = UtilLib.getRandomInt(startX + tmpBitmap.width, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY + tmpBitmap.height, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//draw white clouds
drawAmount = UtilLib.getRandomInt(2, 4);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = UtilLib.copyDataToBitmap(assetsBitmapData[2], true, 0x000000);
tmpBitmap.alpha = UtilLib.getRandomInt(3, 10) / 10;
scale = UtilLib.getRandomInt(15, 40);
tmpBitmap.scaleX = scale / 10;
tmpBitmap.scaleY = UtilLib.getRandomInt(scale / 1.5, scale * 1.5) / 10;
tmpBitmap.x = UtilLib.getRandomInt(startX, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//draw red clouds
drawAmount = UtilLib.getRandomInt(0, 2);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = UtilLib.copyDataToBitmap(assetsBitmapData[3], true, 0x000000);
tmpBitmap.alpha = UtilLib.getRandomInt(3, 10) / 10;
scale = UtilLib.getRandomInt(5, 40) / 10;
tmpBitmap.scaleX = scale; tmpBitmap.scaleY = scale;
tmpBitmap.x = UtilLib.getRandomInt(startX, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//convert all layers to a single bitmap layer and return
return container;
}
}
}
UtilLib.as copyDataToBitmap function:
//copies bitmap data and returns a new bitmap
public static function copyDataToBitmap(bitmapData:BitmapData, transparency:Boolean = false, flatBackground:uint = 0x000000):Bitmap
{
var width:Number = bitmapData.width;
var height:Number = bitmapData.height;
var tmpBitmapData:BitmapData = new BitmapData(width, height, transparency, flatBackground);
tmpBitmapData.copyPixels(bitmapData, new Rectangle(0, 0, width, height), new Point(0, 0));
return new Bitmap(tmpBitmapData);
}
The used background images are all around 30 kilobytes small, but some are very large:
alt text http://feedpostal.com/client/assets/background/1.gif
alt text http://feedpostal.com/client/assets/background/2.png
alt text http://feedpostal.com/client/assets/background/3.png
alt text http://feedpostal.com/client/assets/background/4.png
I used to convert all container layers into 1 flat bitmap, but that seemed to reduce performance. The memory usage remained the same.
The problem is that when I profile the application in Flex 3, the memory usage starts at 20MB (that's 1 container), when I start moving more containers are being loaded and while the old ones are being set to null the garbage collector isn't immediately removing them from memory causing a 600MB memory peak, after which it goes back to 20MB and starts again. Notice though that I have 8GB RAM with a 64bit OS, maybe flash is running the garbage collector on different intervals depending on your memory?
If not, I would really appreciate some help on how to reduce the memory it uses. The images are already very optimized (used Photoshop).
Best Answer
I can suggest a few points not covered in other answers.
Regarding GCs: The precise triggers for what causes a GC are not (I believe) publicly published, but I do know that in some environments they occur naturally on a timer (if not triggered otherwise), and trigger when Flash's memory usage exceeds a certain percentage of the total memory available to it. Since you mention in comments that your overall memory usage doesn't go too high if you force GCs, then my first answer would be that you should stop worrying about memory usage until you find evidence that it is causing problems in this or that environment. Generally speaking Flash will avoid GCs if it thinks there is plenty of memory left, for performance, so if you see usage climbing but it's not affecting system performance, then attempting to "fix" that sounds suspiciously like premature optimization - your time would probably be better spent elsewhere.
Regarding your code: I only half-grok what you're doing here, but one thing I noticed is that you don't seem to call the dispose() method on any of your bitmapData objects. If it's not necessary ignore this, but if you are leaving any BMDs to be collected automatically, make sure you're disposing them.
Regarding architecture: I think you would see much lower memory usage overall (and possibly better performance) if you try a "one large bitmap" approach to this problem. That is, you keep one big (screen-sized) bitmap, and each frame you blank it out and use
copyPixels()
to copy in whatever decals (overlays) need to be in whatever positions. This can be faster than using displayObjects (like sprites) for your game objects, particularly when they are bitmaps originally, and don't need to be rotated, etc (which seems to apply to your case).Compare with this question about the performance of using display objects vs. using a bitmap framebuffer. The asker ultimately found using a framebuffer was faster, and as an added bonus it ought to have much more predictable memory usage, since you won't be creating and destroying bitmaps or display objects. (All you create and destroy is the data you use to track where your decals are.)