How to properly optimize your Starling Flash projects

I am going to release my next game using Starling because it has a lot of interesting features, but to fully take advantage of all this stuff, you have to properly optimize your projects, since even slight differences can lead to very different performance results, as you are about to see.

Let’s see a basic example: I am using Flash CS6 but since I do not use any symbol in the library, you can use your favourite Flash IDE.

This is the main class, the very basic Starling proje3ct:

package {
	
	import flash.display.Sprite;
	import starling.core.Starling;
	
	[SWF(width="640",height="480",frameRate="60",backgroundColor="#ffffff")]
	
	public class Main extends Sprite {
		private var _starling:Starling;
		public function Main() {
			_starling=new Starling(Game,stage);
			_starling.showStats=true;
			_starling.start();
		}
	}
}

I just want you to have a look at line 12 where I tell Starling to show stats. It’s a little black box in the upper left corner which will tell us the frames per second, the memory usage, and the number of drawing processes for each frame.

Now look at Game class, which basically embeds a black and a red circle, and randomly places and moves 100 of them on the stage, a bit like in my previous Starling post Circle Chain engine using Starling.

package {
	
	import starling.display.Sprite;
	import starling.display.Image;
	import starling.textures.Texture;
	import starling.events.EnterFrameEvent;

	public class Game extends Sprite {

		[Embed(source="assets/black.png")]
		private static const BlackCircle:Class;

		[Embed(source="assets/red.png")]
		private static const RedCircle:Class;

		private var circleVector:Vector.<Object>=new Vector.<Object>();

		public function Game() {
			for (var i:Number=0; i<100; i++) {
				var circle:Object=new Object();
				var randomDir:Number=Math.random()*2*Math.PI;
				circle.xSpeed=2*Math.cos(randomDir);
				circle.ySpeed=2*Math.sin(randomDir);
				if (Math.random()>0.5) {
					circle.image=new Image(Texture.fromBitmap(new BlackCircle()));
				}
				else {
					circle.image=new Image(Texture.fromBitmap(new RedCircle()));
				}
				circle.image.x=Math.round(Math.random()*480);
				circle.image.y=Math.round(Math.random()*480);
				circle.image.pivotX=13;
				circle.image.pivotY=13;
				addChild(circle.image);
				circleVector.push(circle);
			}
			addEventListener(EnterFrameEvent.ENTER_FRAME, moveImg);
		}
		
		private function moveImg(event:EnterFrameEvent):void {
			for (var i:Number=0; i<circleVector.length; i++) {
				circleVector[i].image.x+=circleVector[i].xSpeed;
				circleVector[i].image.y+=circleVector[i].ySpeed;
				if (circleVector[i].image.x<0) {
					circleVector[i].image.x+=640;
				}
				if (circleVector[i].image.x>640) {
					circleVector[i].image.x-=640;
				}
				if (circleVector[i].image.y<0) {
					circleVector[i].image.y+=480;
				}
				if (circleVector[i].image.y>480) {
					circleVector[i].image.y-=480;
				}
			}
		}
	}
}

Now look at the result:

It runs at 60FPS, at least on my computer, but we have 100 drawing processes, basically one for each circle. That’s really too much, and will heavily slow down mobile performances.

This is because of the texture creation, which is made at every loop iteration to create a new circle.

Look what happens when we create the texture outside loop creation:

package {
	
	import starling.display.Sprite;
	import starling.display.Image;
	import starling.textures.Texture;
	import starling.events.EnterFrameEvent;

	public class Game extends Sprite {

		[Embed(source="assets/black.png")]
		private static const BlackCircle:Class;

		[Embed(source="assets/red.png")]
		private static const RedCircle:Class;

		private var circleVector:Vector.<Object>=new Vector.<Object>();

		public function Game() {
			var blackTexture:Texture=Texture.fromBitmap(new BlackCircle());
			var redTexture:Texture=Texture.fromBitmap(new RedCircle())
			for (var i:Number=0; i<100; i++) {
				var circle:Object=new Object();
				var randomDir:Number=Math.random()*2*Math.PI;
				circle.xSpeed=2*Math.cos(randomDir);
				circle.ySpeed=2*Math.sin(randomDir);
				if (Math.random()>0.5) {
					circle.image=new Image(blackTexture);
				}
				else {
					circle.image=new Image(redTexture);
				}
				circle.image.x=Math.round(Math.random()*480);
				circle.image.y=Math.round(Math.random()*480);
				circle.image.pivotX=13;
				circle.image.pivotY=13;
				addChild(circle.image);
				circleVector.push(circle);
			}
			addEventListener(EnterFrameEvent.ENTER_FRAME, moveImg);
		}
		
		private function moveImg(event:EnterFrameEvent):void {
			for (var i:Number=0; i<circleVector.length; i++) {
				circleVector[i].image.x+=circleVector[i].xSpeed;
				circleVector[i].image.y+=circleVector[i].ySpeed;
				if (circleVector[i].image.x<0) {
					circleVector[i].image.x+=640;
				}
				if (circleVector[i].image.x>640) {
					circleVector[i].image.x-=640;
				}
				if (circleVector[i].image.y<0) {
					circleVector[i].image.y+=480;
				}
				if (circleVector[i].image.y>480) {
					circleVector[i].image.y-=480;
				}
			}
		}
	}
}

Here is the result:

Now drawing processes should range from 40 to 60. This number is given from the amount of times we switch from the creation of a black circle to the creation of a red circle, or from the creation of a red circle to the creation of a black circle.

Surely, it’s better than before, but that’s not enough.

Let’s try to draw all black circles at first, then all red circles, like I am doing in this script in a very unelegant but clear way:

package {

	import starling.display.Sprite;
	import starling.display.Image;
	import starling.textures.Texture;
	import starling.events.EnterFrameEvent;

	public class Game extends Sprite {

		[Embed(source="assets/black.png")]
		private static const BlackCircle:Class;

		[Embed(source="assets/red.png")]
		private static const RedCircle:Class;

		private var circleVector:Vector.<Object>=new Vector.<Object>();

		public function Game() {
			var blackTexture:Texture=Texture.fromBitmap(new BlackCircle());
			var redTexture:Texture=Texture.fromBitmap(new RedCircle());
			for (var i:Number=0; i<100; i++) {
				var circle:Object=new Object();
				var randomDir:Number=Math.random()*2*Math.PI;
				circle.xSpeed=2*Math.cos(randomDir);
				circle.ySpeed=2*Math.sin(randomDir);
				if (Math.random()>0.5) {
					circle.image=new Image(blackTexture);
					circle.image.x=Math.round(Math.random()*480);
					circle.image.y=Math.round(Math.random()*480);
					circle.image.pivotX=13;
					circle.image.pivotY=13;
					addChild(circle.image);
					circleVector.push(circle);
				}
			}
			for (i=circleVector.length; i<100; i++) {
				circle=new Object();
				randomDir=Math.random()*2*Math.PI;
				circle.xSpeed=2*Math.cos(randomDir);
				circle.ySpeed=2*Math.sin(randomDir);
				circle.image=new Image(redTexture);
				circle.image.x=Math.round(Math.random()*480);
				circle.image.y=Math.round(Math.random()*480);
				circle.image.pivotX=13;
				circle.image.pivotY=13;
				addChild(circle.image);
				circleVector.push(circle);
			}
			addEventListener(EnterFrameEvent.ENTER_FRAME, moveImg);
		}

		private function moveImg(event:EnterFrameEvent):void {
			for (var i:Number=0; i<circleVector.length; i++) {
				circleVector[i].image.x+=circleVector[i].xSpeed;
				circleVector[i].image.y+=circleVector[i].ySpeed;
				if (circleVector[i].image.x<0) {
					circleVector[i].image.x+=640;
				}
				if (circleVector[i].image.x>640) {
					circleVector[i].image.x-=640;
				}
				if (circleVector[i].image.y<0) {
					circleVector[i].image.y+=480;
				}
				if (circleVector[i].image.y>480) {
					circleVector[i].image.y-=480;
				}
			}
		}
	}
}

And the result is…

This time we only have two drawing processes, one for all the black circles, one for all red circles.

The problem is we have all red circles places after all black circles, and this could be a problem in some game concept. Moreover, what if we have 50 different circles?

That’s why we are using a texture atlas, something old school programmers would call sprite sheet. A single image containing all the graphic assets, and an XML file to define images name and coordinates.

This will give us two big advantages: first, we can lower the amount of drawing processes to 1. Second, every image embedded should have width and height as a power of 2, that is 2, 4, 8, 16, 32 and so on. In Starling you can import images of any size, and it will take care about the rest, but you’ll probably pay for it a performance fee. With a texture atlas, it’s easier to satisfy this request since you’ll have a single big image.

Here is the XML:

<?xml version="1.0" encoding="UTF-8"?>
<TextureAtlas imagePath="images.png">
    <SubTexture name="black" x="2" y="30" width="26" height="26"/>
    <SubTexture name="red" x="2" y="2" width="26" height="26"/>
</TextureAtlas>

and this is how we import it:

package {

	import starling.display.Sprite;
	import starling.display.Image;
	import starling.textures.Texture;
	import starling.textures.TextureAtlas;
	import starling.events.EnterFrameEvent;

	public class Game extends Sprite {

		[Embed(source="assets/images.xml",mimeType="application/octet-stream")]
		public static const AtlasXml:Class;

		[Embed(source="assets/images.png")]
		private static const AtlasTexture:Class;

		private var circleVector:Vector.<Object>=new Vector.<Object>();

		public function Game() {
			var texture:Texture = Texture.fromBitmap(new AtlasTexture());
			var xml:XML = XML(new AtlasXml());
			var atlas:TextureAtlas=new TextureAtlas(texture,xml);
			var blackTexture:Texture=atlas.getTexture("black");
			var redTexture:Texture=atlas.getTexture("red");
			for (var i:Number=0; i<100; i++) {
                var circle:Object=new Object();
                var randomDir:Number=Math.random()*2*Math.PI;
                circle.xSpeed=2*Math.cos(randomDir);
                circle.ySpeed=2*Math.sin(randomDir);
                if (Math.random()>0.5) {
                    circle.image=new Image(blackTexture);
                }
                else {
                    circle.image=new Image(redTexture);
                }
                circle.image.x=Math.round(Math.random()*480);
                circle.image.y=Math.round(Math.random()*480);
                circle.image.pivotX=13;
                circle.image.pivotY=13;
                addChild(circle.image);
                circleVector.push(circle);
            }
			addEventListener(EnterFrameEvent.ENTER_FRAME, moveImg);
		}

		private function moveImg(event:EnterFrameEvent):void {
			for (var i:Number=0; i<circleVector.length; i++) {
				circleVector[i].image.x+=circleVector[i].xSpeed;
				circleVector[i].image.y+=circleVector[i].ySpeed;
				if (circleVector[i].image.x<0) {
					circleVector[i].image.x+=640;
				}
				if (circleVector[i].image.x>640) {
					circleVector[i].image.x-=640;
				}
				if (circleVector[i].image.y<0) {
					circleVector[i].image.y+=480;
				}
				if (circleVector[i].image.y>480) {
					circleVector[i].image.y-=480;
				}
			}
		}
	}
}

And finally, the fully optimized result:

It’s very important to keep this in mind when you want to easily develop your projects for mobile devices.

Download the source code, Starling library and graphic assets included.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers