Categories
Articles

Flixel Hello World Basic Shooter Game in Flash and Flex

Decided to try an Actionscript 3 game engine and chose Flixel to start. I started late afternoon Halloween and when the devil (local neighbor in costume) showed up with treats and pizza, I was well on the way.

The Flixel site lead me off to a “hello world” space ship shooter game laid out by Andreas Zecher. He has a nice incremental tutorial that shows the changes if you follow it. I decided to create my own game pieces and also comment all the code so I understood on a line by line basis what was happening. I also made some changes to data moving it out of the code to at least the class member level. This post shows the results of the work.

I am using version Flixel version 2.35 because and included a swc in the download files so you do not need to download it.

You can build this with the free Flex SDK by using the code in the src folder and be sure to include a path to the Flixel library. For Flash CS3 and later versions, you need to create a Flash Document in the src folder and set the document class to Main and then add a library path to the Flixel 2.35 code or SWC if you created one. For your convenience the Flash CS4 example download is included with a Flixel 2.35 SWC.

Also for Flash CS3 on up you will need some Flex library swc files because of the use of the Embed meta tag. Tareq AlJaber has good article on embedding meta data in Flash CS4.

This article shows the code for the Flex project.

Application Class – Main
The applicationCompleteHandler creates Flixel game instance variable named flxGame. It is easily added to the Flex SpriteVisualElement and we are off and playing.

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
			   xmlns:s="library://ns.adobe.com/flex/spark" 
			   xmlns:mx="library://ns.adobe.com/flex/mx" 
			   applicationComplete="applicationCompleteHandler(event)"
			   width = "644"
			   height = "484"
			   >
	<fx:Script>
		<![CDATA[
			import mx.events.FlexEvent;
			
			import org.flixel.FlxGame;
			/**
			 * Set the preloader.
			 * */
			[Frame(factoryClass="Preloader")]
			/**
			 * Properties for background
			 * @see background_bc
			 */
			private static const backgroundColor:Number = 0x333333;
			/**
			 * The Flixel game.
			 */
			private var flxGame:FlxGame;
			/**
			 * Handler for applicationComplete event. Setup world and begin animation.
			 * */
			protected function applicationCompleteHandler(event:FlexEvent):void
			{
				trace( className + ".applicationCompleteHandler(...)");
				flxGame = new FlxGame(640, 480, PlayState, 1 ); 
				box2DContainer.addChild(flxGame);
				// Offset to show box2DContainer evenly.
				flxGame.x = 1;
				flxGame.y = 1;
			}			
		]]>
	</fx:Script>
	<!--
	Background for app 
	--> 
	<s:BorderContainer id = "background_bc"
					   width="{width}" height = "{height}"
					   
					   backgroundColor="{backgroundColor}">
		
		<!--
		Container for the FlxGame
		-->
		<s:SpriteVisualElement id = "box2DContainer" />
	</s:BorderContainer> 
</s:Application>

[ad name=”Google Adsense”]
PlayState Class
The PlayState class does the real game specific details. Flixel does the rest of the work.

Overall the plan is to have a player space ship pointing to the right only and that moves up, down, left and right with arrow keys. The spacebar is used for firing bullets. Enemy space ships appear of the right and travel left with a slight sine wave. Collisions of bullets with enemy ships terminates them in an explosion. Collision of the player ship with an enemy ship terminates both ships and ends the game.

Enemy ships are added in more frequently as the game progress. Bullets are unlimited and have no repetition governor.

The create method on line 88 sets the game up and starts the game. In 2.5 seconds the first enemy ship appears.

The update method on line 116 represents the testing for player input and collisions. Lines 119 and 121 detect collisions.

Lines 123 – 127 test for starting the game which is done by detecting if the player ship is inactive (“dead”) and the ENTER key is pressed. Game in play is based on the player ship being active.

Lines 128 – 133 set the flag to fire a bullet when the SPACE BAR is pressed.

Lines 135 – 144 simple detect if it is time to add another enemy ship and to call resetSpawnTimer() on line 173 that adjusts the timer to a smaller interval.

The handler for an enemy ship hit with a bullet is the overlapAlienBullet method on line 188. Basically the enemy ship and bullet are destroyed and broken into exploding pixels. That code I have not studied but is handled by the createEmitter() method on line 228 and the FlxEmitter class.

The overlapAlienShip method on line 207 handles an enemy ship colliding with the play ship. The game is rendered inactive at this point, however enemy ships keep showing up in larger numbers: something I did not bother to fix. The game gets a shake using the FlxG.quake.start method on line 214.

package
{
	import org.flixel.FlxEmitter;
	import org.flixel.FlxG;
	import org.flixel.FlxGroup;
	import org.flixel.FlxPoint;
	import org.flixel.FlxSprite;
	import org.flixel.FlxState;
	import org.flixel.FlxText;
	import org.flixel.FlxU;
	
	public class PlayState extends FlxState
	{
		/**
		 * Player ship.
		 * */
		private var _playerShip:PlayerShip;
		/**
		 * FlxGroup for EnemyShip objects.
		 * */
		private var _enemyShips:FlxGroup;
		/**
		 * FlxGroup for player ship bullets
		 * */
		private var _bullets:FlxGroup;
		private var _enemyShipSpawnTimer:Number;
		/**
		 * Starting number of seconds that new enemy ships are spawned.
		 * */
		private var _enemyShipSpawnInterval:Number = 2.5;
		/**
		 * Minimum seconds that new enemy ships are spawned.
		 * */
		private var _enemyShipSpawnIntervalMin:Number = .1;
		/**
		 * Reducing factor for seconds next enemy ship is spawned.
		 * */
		private var _enemyShipSpawnIntervalReductionFactor:Number = 0.95;
		/**
		 * Game text displaying number of enemy ships hit.
		 * */
		private var _scoreText:FlxText;
		/**
		 * Score text font size.
		 * */
		private var _scoreTextFontSize:Number = 32;
		/**
		 * Score text font color.
		 * */
		private var _scoreTextFontColor:Number = 0xFF597137;
		/**
		 * Score text horizontal alignment.
		 * */
		private var _scoreTextAlign:String = "left";
		/**
		 * Game text displaying end of game message.
		 * */
		private var _gameOverText:FlxText;
		/**
		 * End of came message.
		 * */
		private var _lang_GameOverMessage:String = "GAME OVER\nPRESS ENTER TO PLAY AGAIN";
		/**
		 * Sound for exploding game ship
		 * */
		[Embed(source="assets/ExplosionShip.mp3")]
		private var SoundExplosionShip:Class;
		/**
		 * Sound for exploding enemy ship
		 * */
		[Embed(source="assets/ExplosionAlien.mp3")]
		private var SoundExplosionEnemyShip:Class;
		/**
		 * Sound for ship bullet
		 * */
		[Embed(source="assets/Bullet.mp3")]
		private var SoundBullet:Class;
		/**
		 * The game play state constructor.
		 * */
		public function PlayState()
		{
			super();
		}
		/**
		 * Create game objects
		 */
		override public function create():void
		{
			// Set the FlxState bgColor. The default background color of the game.
			bgColor = 0xFFABCC7D;
			// Set the Flixel starting score value
			FlxG.score = 0;
			// Set the costume score value
			_scoreText = new FlxText(10, 8, 200, "0");
			_scoreText.setFormat(null, _scoreTextFontSize, _scoreTextFontColor, _scoreTextAlign);
			add(_scoreText);
			// Add player ship.
			_playerShip = new PlayerShip();
			add(_playerShip);
			// Add a group for enemy ships
			_enemyShips = new FlxGroup();
			add(_enemyShips);
			// Add a group for player ship bullets
			_bullets = new FlxGroup();
			add(_bullets);
			// Reset the enemy ship spawning timer.
			resetSpawnTimer();
			// Flixel framework creating process.
			super.create();
		}
		/**
		 * Automatically calls update on everything added to the game loop.
		 * Handle custom input and perform collisions.
		 */
		override public function update():void
		{
			// Bullet hit an enemy ship
			FlxU.overlap(_enemyShips, _bullets, overlapAlienBullet);
			// Enemy ship hit player ship
			FlxU.overlap(_enemyShips, _playerShip, overlapAlienShip);
			// Ship dead and Enter key is pressed
			if(FlxG.keys.ENTER && _playerShip.dead)
			{
				// Reset the game state to start.
				FlxG.state = new PlayState();
			}	
			// Space key and ship is active
			if(FlxG.keys.justPressed("SPACE") && _playerShip.dead == false)
			{
				// Fire a bullet
				spawnBullet(_playerShip.getBulletSpawnPosition());
			}
			// Reduce enemy ship spawning timer by seconds that passed since last frame
			_enemyShipSpawnTimer -= FlxG.elapsed;
			// Enemy ship spawning timer is negative
			if(_enemyShipSpawnTimer < 0)
			{
				// Create enemy ship
				spawnEnemyShip();
				// Reset the enemy ship spawning timer.
				resetSpawnTimer();
			}
			// Update default game actions			
			super.update();
		}
		/**
		 * Launch an enemy ship at random location.
		 * */
		private function spawnEnemyShip():void
		{
			// Enemy ship starts on right off screen
			var x: Number = FlxG.width;
			// Enemy ship random y position
			var y: Number = Math.random() * (FlxG.height - 100) + 50;
			_enemyShips.add(new EnemyShip(x, y));
		}
		/**
		 * Fire a bullet.
		 * */
		private function spawnBullet(p:FlxPoint):void
		{
			// Create a bullet.
			var bullet: PlayerShipBullet = new PlayerShipBullet(p.x, p.y);
			// Add bullet to this play state.
			_bullets.add(bullet);
			// Make bullet firing sound
			FlxG.play(SoundBullet);
		}
		/**
		 * Reset the bullet and enemy ship spawning timer
		 * */
		private function resetSpawnTimer():void
		{
			// Reset the count down timer
			_enemyShipSpawnTimer = _enemyShipSpawnInterval;
			// Shorten the countdown timer interval for next pass.
			_enemyShipSpawnInterval *= _enemyShipSpawnIntervalReductionFactor;
			// Below minimum interval
			if(_enemyShipSpawnInterval < _enemyShipSpawnIntervalMin)
			{
				_enemyShipSpawnInterval = _enemyShipSpawnIntervalMin;
			}
		}
		/**
		 * Enemy ship hit by bullet.
		 * */
		private function overlapAlienBullet(enemyShip:EnemyShip, bullet:PlayerShipBullet):void
		{
			// Set the enemy ship as dead. See FlxObject.kill();
			enemyShip.kill();
			// Set the bullet as dead. See FlxObject.kill();
			bullet.kill();
			// Update score for Flixel.
			FlxG.score += 1;
			// Update score costume.
			_scoreText.text = FlxG.score.toString();
			// Explode the enemy ship.
			var emitter:FlxEmitter = createEmitter();
			emitter.at(enemyShip);
			// Play sound for enemy ship exploding.
			FlxG.play(SoundExplosionEnemyShip);
		}
		/**
		 * Play ship and enemy ship collide.
		 * */
		private function overlapAlienShip(enemyShip:EnemyShip, playerShip:PlayerShip):void
		{
			// Set the player ship as dead. See FlxObject.kill();
			playerShip.kill();
			// Set the enemy ship as dead. See FlxObject.kill();
			enemyShip.kill();
			// Shake game.
			FlxG.quake.start(0.02);
			// Explode player ship
			var emitter:FlxEmitter = createEmitter();
			emitter.at(playerShip);
			// Play player ship explosion sound.
			FlxG.play(SoundExplosionShip);
			// Update game text costume.
			_gameOverText = new FlxText(0, FlxG.height / 2, FlxG.width,_lang_GameOverMessage);
			_gameOverText.setFormat(null, 16, 0xFF597137, "center");
			add(_gameOverText);
		}
		/**
		 * Ship explosion particle emitter animation.
		 * */
		private function createEmitter():FlxEmitter
		{
			var emitter:FlxEmitter = new FlxEmitter();
			emitter.delay = 1;
			emitter.gravity = 0;
			emitter.maxRotation = 0;
			emitter.setXSpeed(-500, 500);
			emitter.setYSpeed(-500, 500);
			var particles: int = 10;
			for(var i: int = 0; i < particles; i++)
			{
				var particle:FlxSprite = new FlxSprite();
				particle.createGraphic(2, 2, 0xFF597137);
				particle.exists = false;
				emitter.add(particle);
			}
			emitter.start();
			add(emitter);
			return emitter;
		}
	}
}

[ad name=”Google Adsense”]
PlayerShip Class
This class adds the graphic for the player ship. When the Flixel framework calls the update method, the player ship is repositioned and kept with boundaries.

When the PlayerState class requires a bullet the getBulletSpawnPosition() on line 104 provides the starting position based on the graphic design for the gun barrel.

package
{
	import org.flixel.FlxG;
	import org.flixel.FlxPoint;
	import org.flixel.FlxSprite;
	
	/**
	 * Defines the player's ship.
	 * */
	public class PlayerShip extends FlxSprite
	{
		/**
		 * Starting game horizontal position.
		 * */
		private const SHIP_START_X:Number = 50;		
		/**
		 * Starting game vertical position.
		 * */
		private const SHIP_START_Y:Number = 50;		
		/**
		 * Ship movement velocity.
		 * */
		private const SHIP_MOVEMENT_VELOCITY:Number = 250;		
		/**
		 * Padding for movement of ships.
		 * */
		private const SHIP_MOVEMENT_PADDING:Number = 16;		
		/**
		 * Graphic for player ship.
		 * */
		[Embed(source="assets/ship_published.png")]
		private var ImgShip:Class;
		
		/**
		 * Player ship.
		 * */
		public function PlayerShip():void
		{
			super(SHIP_START_X, SHIP_START_Y, ImgShip);
		}
		/**
		 * Update ship.
		 * */
		override public function update():void
		{
			// Set horizontal velocity to stopped position.
			velocity.x = 0;
			// Set vertical velocity to stopped position.
			velocity.y = 0;
			
			// Left key.
			if(FlxG.keys.LEFT)
			{
				// Set horizontal velocity to left.
				velocity.x = -SHIP_MOVEMENT_VELOCITY;
			}
			// Right key.
			else if(FlxG.keys.RIGHT)
			{
				// Set horizontal velocity to right.
				velocity.x = SHIP_MOVEMENT_VELOCITY;
			}
			// Up key.
			if(FlxG.keys.UP)
			{
				// Set vertical velocity to up.
				velocity.y = -SHIP_MOVEMENT_VELOCITY;
			}
			// Down key.
			else if(FlxG.keys.DOWN)
			{
				// Set vertical velocity to down.
				velocity.y = SHIP_MOVEMENT_VELOCITY;
			}
			// Postion is near right side of screen.
			if(x > FlxG.width - width - SHIP_MOVEMENT_PADDING)
			{
				// Hold the position.
				x = FlxG.width - width - SHIP_MOVEMENT_PADDING;
			}
			// Postion is near left side of screen.
			else if(x < SHIP_MOVEMENT_PADDING)
			{
				// Hold the position.
				x = SHIP_MOVEMENT_PADDING;
			}
			// Postion is near bottom of screen.
			if(y > FlxG.height - height - SHIP_MOVEMENT_PADDING)
			{
				// Hold the position.
				y = FlxG.height - height - SHIP_MOVEMENT_PADDING;
			}
			// Postion is near top of screen.
			else if(y < SHIP_MOVEMENT_PADDING)
			{
				// Hold the position.
				y = SHIP_MOVEMENT_PADDING;
			}			
			super.update();
		}
		/**
		 * Position bullet starting position at tip of gun barrel. 
		 * */
		public function getBulletSpawnPosition():FlxPoint
		{
			// See ImgShip asset for placement offset metrics to end of gun barrel.
			var p: FlxPoint = new FlxPoint(x + 108, y + 27);
			return p;
		}
	}
}

EnemyShip Class
This class adds the graphic for the enemy ship and the initial game position when created. The PlayState spawnEnemyShip method computes the vertical starting position using some random computations and the horizontal position always off the right side of the game screen..

It mainly provides the velocity for the enemy ships which include a left only horizontal direction and a sine wave for vertical direction.

package
{
	import org.flixel.FlxSprite;
	/**
	 * Defines enemy ship.
	 * */
	public class EnemyShip extends FlxSprite
	{
		/**
		 * Horizontal velocity for enemy ship.
		 * */
		private const ENEMY_SHIP_HORIZONTAL_VELOCITY:Number = 200;
		/**
		 * Vertical velocity for enemy ship.
		 * */
		private const ENEMY_SHIP_VERTICAL_VELOCITY:Number = 50;
		/**
		 * Graphic for enemy ship.
		 * */
		[Embed(source="assets/enemy_ship_published.png")]
		private var ImgEnemy:Class;
		/**
		 * Enemy ship constructor.
		 * */
		public function EnemyShip(x: Number, y: Number):void
		{
			super(x, y, ImgEnemy);
			// Enemy ship moves left.
			velocity.x = -ENEMY_SHIP_HORIZONTAL_VELOCITY;
		}
		/**
		 * Update the enemy ship.
		 * */
		override public function update():void
		{
			// Sine wave for vertical velocity.
			velocity.y = Math.cos(x / ENEMY_SHIP_VERTICAL_VELOCITY) * ENEMY_SHIP_VERTICAL_VELOCITY;
			super.update();
		}

	}
}

[ad name=”Google Adsense”]
PlayerShipBullet Class
Defines the player ship bullet using the FlxSprite createGraphic method which simply creates a rectangular image.

The player bullet x velocity is set to right. This matches the player ship always pointing right.

It could be interesting providing an alternative sine based vertical velocity.

package
{
	import org.flixel.FlxSprite;
	
	public class PlayerShipBullet extends FlxSprite
	{
		/**
		 * Player ship bullet.
		 * */
		public function PlayerShipBullet(x: Number, y: Number):void
		{
			super(x, y);
			// Use simple flat colored square image for bullet.
			createGraphic(16, 3, 0xFF597137);
			velocity.x = 1000;
		}
	}
}

Preloader Class
This is supposed to show a nice 8-bit style preloader bar according to Andreas Zecher. I noticed a comment on histutorial that it did not work. I ran this in the Flash CS4 bandwidth profiler and could not show the preloader. All the content is in the first frame, and for that matter in Main, for CS4 and thus all needs loading before any code would run to show a preloader.

I kept the coding in and the Preloader class simply needs to know what class it is to measure for the preloading animation to run.

package
{
	import org.flixel.FlxPreloader;
	
	/**
	 * Flixel preloader.
	 * */
	public class Preloader extends FlxPreloader
	{
		/**
		 * Set Flixel preloader class name.
		 * */
		override public function Preloader()
		{
			className = "Main";
			super();
		}
	}
}

Default.css
This is blank and appears to be needed to remove the “Default css file not found” warning message in Flex Builder 4. You can see the solution for this in APPENDIX C: That Annoying Default.css Warning of an article on setting up a Flixel Actionscript project in Flex Builder.

Basically you need a blank Default.css file and add the switch -defaults-css-url Default.css to the project Actionscript Compiler Additional compiler arguments found in the project properties.