R – How to reduce the flash game’s background engine 600MB memory usage peaks

actionscript-3backgroundflashmemorymemory-management

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.)