Do you like my tutorials?

Then consider supporting me on Ko-fi

Talking about Actionscript 3, Flash and Starling.

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.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.