Managing multiple iOS resolutions with Starling – real world example

When making iOS games, you have to handle various kind of devices, starting from the iPhone 3GS up to the iPhone 5.

Every device has its own resolution, and you should be able to make one single version of your game which will fit in every iPhone model.

I am showing you how I am managing this in an actual project, the sequel of Circle Chain game I showed you in the post how to handle your Flash Starling animations using the Juggler.

It’s time to port such project on mobile devices and see how it runs.

Before doing this, if you are using Flash Professional it’s always good to check if your AIR version is up to date. So download the latest version at this page and proceed this way:

Go to Help -> Manage AIR SDK

Click the plus symbol and select the folder containing your recently downloaded AIR folder

Select the latest AIR version and click OK

Finally you are ready to taget the latest AIR for iOS version

Fron now on, when you have to publish for iOS, you’ll just have to enter your certificate and provisioning profile

If you don’t know how to get certificates, provisioning profiles or how to install your Flash projects to iPhone, follow the guide I made in the post Creation of an iPhone App with Flash and without a Mac (for all Windows lovers). If you have a Mac, it will be even easier.

The guide is 2 years old so the page layout in the Apple developer portal has changed a bit, but the process remains the same.

Now that you are ready, here is what we are going to create with Starling: basically it’s the same thing I made in the post how to handle your Flash Starling animations using the Juggler, with a Main class:

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

Look at line 6 how I am setting the width and the height of the 3GS model, 480×320.

And this is Game class:

package {

	import starling.display.Sprite;
	import starling.display.Image;
	import starling.textures.Texture;
	import starling.animation.Tween;
	import starling.core.Starling;
	import starling.display.Button;

	public class Game extends Sprite {

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

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

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

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

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

		private var splashSprite:Sprite;

		// buttons
		private var normalModeButton:Button;
		private var timeAttackModeButton:Button;

		// textures
		private var gameTitleTexture:Texture;
		private var gridEditionTexture:Texture;
		private var normalModeTexture:Texture;
		private var timeAttackModeTexture:Texture;

		public function Game() {
			addBackground();
			showGameTitle();
		}

		private function addBackground():void {
			var backgroundTexture:Texture=Texture.fromBitmap(new MainBG());
			var backgroundImage:Image=new Image(backgroundTexture);
			addChild(backgroundImage);
		}

		/*
		
		*****************
		* SPLASH SCREEN *    
		*****************
		
		*/

		private function showGameTitle():void {
			splashSprite=new Sprite();
			addChild(splashSprite);
			showCircleChain();
		}

		private function showCircleChain():void {
			gameTitleTexture=Texture.fromBitmap(new GameTitle());
			var gameTitleImage:Image=new Image(gameTitleTexture);
			splashSprite.addChild(gameTitleImage);
			gameTitleImage.x=111;
			gameTitleImage.y=-100;
			var gameTitleTween:Tween=new Tween(gameTitleImage,0.7);
			gameTitleTween.moveTo(111,50);
			gameTitleTween.onComplete=showGridEdition;
			Starling.juggler.add(gameTitleTween);
		}

		private function showGridEdition():void {
			gridEditionTexture=Texture.fromBitmap(new GridEdition());
			var gridEditionImage:Image=new Image(gridEditionTexture);
			splashSprite.addChild(gridEditionImage);
			gridEditionImage.y=90;
			gridEditionImage.x=481;
			var gridEditionTween:Tween=new Tween(gridEditionImage,0.7);
			gridEditionTween.moveTo(276,90);
			gridEditionTween.onComplete=showButtons;
			Starling.juggler.add(gridEditionTween);
		}

		private function showButtons():void {
			normalModeTexture=Texture.fromBitmap(new NormalMode());
			normalModeButton=new Button(normalModeTexture);
			splashSprite.addChild(normalModeButton);
			normalModeButton.x=186;
			normalModeButton.y=170;
			normalModeButton.scaleWhenDown=0.95;
			normalModeButton.alpha=0;
			var normalModeTween:Tween=new Tween(normalModeButton,0.7);
			normalModeTween.fadeTo(1);
			Starling.juggler.add(normalModeTween);
			timeAttackModeTexture=Texture.fromBitmap(new TimeAttackMode());
			timeAttackModeButton=new Button(timeAttackModeTexture);
			splashSprite.addChild(timeAttackModeButton);
			timeAttackModeButton.x=167;
			timeAttackModeButton.y=230;
			timeAttackModeButton.scaleWhenDown=0.95;
			timeAttackModeButton.alpha=0;
			var timeAttackModeTween:Tween=new Tween(timeAttackModeButton,0.7);
			timeAttackModeTween.delay=0.2;
			timeAttackModeTween.fadeTo(1);
			Starling.juggler.add(timeAttackModeTween);
		}
	}
}

Now it’s time to make them run on my various devices and see what happens.

This is the actual screenshot of my 3GS:

Everything looks fine. Let’s see my iPhone 4:

The image has been scaled down for blogging layout purpose, but it’s easy to see the game only covers a quarter of the 960×640 retina display. Things get even worse on my iPhone 5

Less than a quarter of my 1136×640 display has been covered.

That’s what you get when you target the non-retina 480×320 display.

We have to make some changes to Main class, this way:

package {

	import flash.display.Sprite;
	import starling.core.Starling;
	import flash.geom.Rectangle;

	[SWF(width="480",height="320",frameRate="60",backgroundColor="#000000")]

	public class Main extends Sprite {
		private var _starling:Starling;
		public function Main() {
			var screenWidth:int=stage.fullScreenWidth;
			var screenHeight:int=stage.fullScreenHeight;
			var viewPort:Rectangle=new Rectangle(0,0,screenWidth,screenHeight);
			_starling=new Starling(Game,stage,viewPort);
			_starling.stage.stageWidth=480;
			_starling.stage.stageHeight=320;
			_starling.showStats=true;
			_starling.start();
		}
	}
}

Basically you are saying: no matter the width and the height of the stage (which continues to be set at 480×320), enlarge the content until it fills the entire screen.

This method is called upscaling and it’s the quickest, yet cheapest, way to fix the problem.

Look at the iPhone 4 now:

The game covers the entire retina display, although it remains a low resolution graphics. Same thing with the iPhone 5:

iPhone 3GS keeps working in the same way as before. Nobody forces you to look for a better solution, but retina iPhone owners tend to give negative feedback to games released in 2013 with no retina support.

Another idea would be to work with high resolution textures, target a 960×640 stage and downscale the game on 3GS iPhones. This way you will have retina resolution on retina models scaled down on iPhone 3GS models, which will probably run the game at an extremely low framerate due to their old hardware not able to handle high resolution textures.

What if we could read the iPhone resolution and load a different set of textures according to the model?

Change Game class this way:

package {

	import starling.display.Sprite;
	import starling.display.Image;
	import starling.textures.Texture;
	import starling.animation.Tween;
	import starling.core.Starling;
	import starling.display.Button;

	public class Game extends Sprite {

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

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

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

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

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

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

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

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

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

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

		private var splashSprite:Sprite;

		// buttons
		private var normalModeButton:Button;
		private var timeAttackModeButton:Button;

		// textures
		private var backgroundTexture:Texture;
		private var gameTitleTexture:Texture;
		private var gridEditionTexture:Texture;
		private var normalModeTexture:Texture;
		private var timeAttackModeTexture:Texture;

		public function Game() {
			addBackground();
			showGameTitle();
		}

		private function isHD():Boolean {
			return Starling.current.viewPort.width>480;
		}

		private function addBackground():void {
			if (isHD()) {
				backgroundTexture=Texture.fromBitmap(new MainBGHD(),true,false,2);
			}
			else {
				backgroundTexture=Texture.fromBitmap(new MainBG());
			}
			var backgroundImage:Image=new Image(backgroundTexture);
			addChild(backgroundImage);
		}

		/*
		
		*****************
		* SPLASH SCREEN *    
		*****************
		
		*/

		private function showGameTitle():void {
			splashSprite=new Sprite();
			addChild(splashSprite);
			showCircleChain();
		}

		private function showCircleChain():void {
			if (isHD()) {
				gameTitleTexture=Texture.fromBitmap(new GameTitleHD(),true,false,2);
			}
			else {
				gameTitleTexture=Texture.fromBitmap(new GameTitle());
			}
			var gameTitleImage:Image=new Image(gameTitleTexture);
			splashSprite.addChild(gameTitleImage);
			gameTitleImage.x=111;
			gameTitleImage.y=-100;
			var gameTitleTween:Tween=new Tween(gameTitleImage,0.7);
			gameTitleTween.moveTo(111,50);
			gameTitleTween.onComplete=showGridEdition;
			Starling.juggler.add(gameTitleTween);
		}

		private function showGridEdition():void {
			if (isHD()) {
				gridEditionTexture=Texture.fromBitmap(new GridEditionHD(),true,false,2);
			}
			else {
				gridEditionTexture=Texture.fromBitmap(new GridEdition());
			}
			var gridEditionImage:Image=new Image(gridEditionTexture);
			splashSprite.addChild(gridEditionImage);
			gridEditionImage.y=90;
			gridEditionImage.x=481;
			var gridEditionTween:Tween=new Tween(gridEditionImage,0.7);
			gridEditionTween.moveTo(276,90);
			gridEditionTween.onComplete=showButtons;
			Starling.juggler.add(gridEditionTween);
		}

		private function showButtons():void {
			if (isHD()) {
				normalModeTexture=Texture.fromBitmap(new NormalModeHD(),true,false,2);
			}
			else {
				normalModeTexture=Texture.fromBitmap(new NormalMode());
			}
			normalModeButton=new Button(normalModeTexture);
			splashSprite.addChild(normalModeButton);
			normalModeButton.x=186;
			normalModeButton.y=170;
			normalModeButton.scaleWhenDown=0.95;
			normalModeButton.alpha=0;
			var normalModeTween:Tween=new Tween(normalModeButton,0.7);
			normalModeTween.fadeTo(1);
			Starling.juggler.add(normalModeTween);
			if (isHD()) {
				timeAttackModeTexture=Texture.fromBitmap(new TimeAttackModeHD(),true,false,2);
			}
			else {
				timeAttackModeTexture=Texture.fromBitmap(new TimeAttackMode());
			}
			timeAttackModeButton=new Button(timeAttackModeTexture);
			splashSprite.addChild(timeAttackModeButton);
			timeAttackModeButton.x=167;
			timeAttackModeButton.y=230;
			timeAttackModeButton.scaleWhenDown=0.95;
			timeAttackModeButton.alpha=0;
			var timeAttackModeTween:Tween=new Tween(timeAttackModeButton,0.7);
			timeAttackModeTween.delay=0.2;
			timeAttackModeTween.fadeTo(1);
			Starling.juggler.add(timeAttackModeTween);
		}
	}
}

In the script, I am embedding both retina and non retina images, check for screen resolution and build the textures accordingly. On the iPhone 3GS keeps working in the same way, look at iPhone 4:

The game covers the entire screen, but now it supports the retina display, look at the definition of the actual size.

Same thing for the iPhone 5

Almost the entire screen is covered with an high resolution game.

This method is robust and always works, but iPhone 5 users tend to give negative feedback if their bigger screens are not supported. Moreover, I am always working with twice the images I actually need. How to fix this?

Showing it to you next time.

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