Deprecated: preg_replace(): The /e modifier is deprecated, use preg_replace_callback instead in /home3/csandahl/public_html/luckoverskill.com/lib/smarty/Smarty_Compiler.class.php on line 270
LuckOverSkill - 02 - Adding a Player
Deprecated: preg_replace(): The /e modifier is deprecated, use preg_replace_callback instead in /home3/csandahl/public_html/luckoverskill.com/lib/smarty/Smarty_Compiler.class.php on line 270

02 - Adding a Player

Previous Tutorial: Getting Started
Next Tutorial: Adding Weapons

Goals

  1. Create a basic playable level
  2. Create a sprite that can move around with keyboard input

Files

Firing Up the Demo

Now we have a box around the whole level and a player running and hopping around. Use LEFT/RIGHT to move and SPACE to jump. Want more? The player gets a gun in the next tutorial.

Project Structure

Files in Project Once again, download the source and extract it into the project directory (ex: C:\Development\Flash Platformer Tutorials). Open up the new project file (\02 - Adding a Player\02 - Adding a Player.as3proj) to see the project structure. We have added or modified several files and folders:
[new] [modified] [no change]

  1. /assets/gui/title.png
  2. /assets/sprites/spaceman.png - the sprite sheet for the player (all images needed to animate running, jumping, etc)
  3. /assets/tiles/tech_tiles.png - the sprite sheet for the level tiles
  4. /src/levels/LevelBase.as - base class for all future levels; contains general functionality (like adding a box around the level) that many levels will need
  5. /src/sprites/Player.as - player animation and control
  6. /src/states/MenuState.as - press "x" to move to the actual game
  7. /src/states/PlayState.as - the game window while playing the game (as opposed to a menu)
  8. Assets.as - player and level images added
  9. Default.css
  10. Main.as - Flixel Power Tools initialization added
  11. Preloader.as

Finishing the Menu

The image for our menu states very clearly: "Press X to continue." Perhaps we should do that.

Open up MenuState.as. We need to check if the player has pressed the "X" key, so we need to add an update() function:

/**
 * Update each frame
 */
override public function update():void {
    super.update();
    // switch on a key press
    if (FlxG.keys.justPressed("X")) {
        FlxG.switchState(new PlayState());
    }
}

FLIXEL 101: update()

update() is a built-in function for any class that extends a Flixel class (like FlxState, FlxSprite, etc). Basically, this gets called every update cycle (every frame more or less). If you need something to be done or checked continuously, update() is your guy.

First, we want to take care of the stuff that happens regularly when Flixel calls update(), so we pass it back to FlxState with super.update().

Next we check for our "X" key. Flixel gives you access to the keyboard via FlxG.keys. Depending on how you want to handle your keyboard input, you can use pressed() (key is being pressed right now), justPressed() (key is being pressed right now but it wasn't being pressed a moment ago), or justReleased() (key isn't being pressed right now but was a moment ago).

If you wanted to check for something other than "X", you can use things like "LEFT", "ESCAPE", and "CAPSLOCK". See the Keyboard constructor inside the Flixel source code for a list of valid key names.

So, now that we know the player just pressed "X", we can actually do something. As we learned in the previous tutorial, Flixel uses states to represent things like screens. When we want to move from the main menu to the actual game, we need to change our state. FlxG.switchState(new PlayState()) tells Flixel to change from this state to PlayState so we can actually start playing the game!

Playing Games With PlayState

Speaking of PlayState, open up PlayState.as to see our brand spanking new Flixel state. It's not too large, so here's the full thing:

package states
{
	import levels.LevelBase;
	import org.flixel.*;

	/**
	 * Playing the game
	 */
	public class PlayState extends FlxState
	{
		/**
		 * Constants
		 */
		public static var LEVEL_SIZE:FlxPoint = new FlxPoint(640, 640); // level size in pixels
		public static var BLOCK_SIZE:FlxPoint = new FlxPoint(8, 8); // block size in pixels
		 
		/**
		 * Game objects
		 */
		protected var level:LevelBase = null;
		
		/**
		 * Create state
		 */
		override public function create():void {
			FlxG.bgColor = 0xff131c1b;
			FlxG.mouse.hide();
			// load level
			level = new LevelBase(this, LEVEL_SIZE, BLOCK_SIZE);
			level.create();
		}
		
		/**
		 * Update each timestep
		 */
		override public function update():void {
			super.update();
			level.update();
		}
	}
}

Let's take it from the top. First, we need to define some constants that we will use later. LEVEL_SIZE says how big our level will be. Notice that our level is 640x640 while our window (defined in Main.as) is 640x480. When your level is bigger than your screen, you will need to add the ability to scroll around (see Player.as a little further down). BLOCK_SIZE defines how large our blocks are by default. You can use blocks of different sizes on the same level, but this will make our life easier.

Next we have our level, which is one of our new classes (LevelBase). Remember that Flixel thinks of states similar to a window. With that in mind, we want to be able to load in any level using this same "window" (PlayState). That's why we have a level variable instead of putting all our level code in PlayState. As we progress through these tutorials, LevelBase will get some new tricks and we'll see some different types of levels. This setup means PlayState won't have to be massively changed when we want a whole new kind of level.

In our create() method, we are setting up the "window" and loading in our level. FlxG.bgcolor tells Flixel what color to use if nothing else is being drawn at a given location. FlxG.mouse.hide() is shockingly used to hide the mouse (remember that MenuState told Flixel to show the mouse). Finally, we create the level variable and then tell that level to create() itself. When we want a different level, now all we have to do is change level = new LevelBase(this, LEVEL_SIZE, BLOCK_SIZE) to level = new MyNewLevel(this, LEVEL_SIZE, BLOCK_SIZE). You'll see this in later tutorials, but hopefully you can see why it's setup this way.

At the end of the file we have our update() method. Remember that this is called every time step (think of it as every frame). All we need to do is make sure our level gets updated (thru level.udpate()) and that the normal update() behavior happens (thru super.update()). No magic here, but if we didn't do this then nothing would happen on our screen. This is a consequence of having a level variable instead of having the level code inside PlayState.

All Your LevelBase Are Belong To Us

Now the real fun begins. This is a big file, so we're going to have to break it down into chunks. If you are a "show me the goods" kind of person, here's the whole file:

package levels 
{
	import org.flixel.*;
	import sprites.Player;
	/**
	 * Base class for all levels
	 * TODO: getters and setters
	 * @author Cody Sandahl
	 */
	public class LevelBase
	{
		/**
		 * Map
		 */
		public var state:FlxState; // state displaying the level
		public var levelSize:FlxPoint; // width and height of level in pixels
		public var blockSize:FlxPoint; // width and height of each block in pixels
		public var levelBlocks:FlxPoint; // width and height in blocks (not pixels)
		public var mapGroup:FlxGroup; // all the map blocks
		public var gravity:Number; // TODO: base acceleration due to gravity
		
		/**
		 * Player
		 */
		public var player:Player;
		public var playerStart:FlxPoint;
		
		/**
		 * Constructor
		 */
		public function LevelBase(state:FlxState, levelSize:FlxPoint, blockSize:FlxPoint):void {
			this.state = state;
			this.levelSize = levelSize;
			this.blockSize = blockSize;
			if (levelSize && blockSize)
				this.levelBlocks = new FlxPoint(Math.floor(levelSize.x / blockSize.x), Math.floor(levelSize.y / blockSize.y));
			// setup groups
			this.mapGroup = new FlxGroup();
		}
		
		/**
		 * Create the whole level, including all sprites, maps, blocks, etc
		 */
		public function create():void {
			createMap();
			createPlayer();
			addGroups();
			createCamera();
		}
		
		/**
		 * Create the map (blocks, ladders, doors, etc)
		 */
		public function createMap():void {
			var block:FlxTileblock;
			// ground
			block = new FlxTileblock(0, levelSize.y - blockSize.y, levelSize.x, blockSize.y);
			block.loadTiles(Assets.TECHTILES_TILE);
			mapGroup.add(block);
			// ceiling
			block = new FlxTileblock(0, 0, levelSize.x, blockSize.y);
			block.loadTiles(Assets.TECHTILES_TILE);
			mapGroup.add(block);
			// left wall
			block = new FlxTileblock(0, blockSize.x, blockSize.y, levelSize.y - blockSize.y * 2);
			block.loadTiles(Assets.TECHTILES_TILE);
			mapGroup.add(block);
			// right wall
			block = new FlxTileblock(levelSize.x - blockSize.x, blockSize.y, blockSize.x, levelSize.y - blockSize.y * 2);
			block.loadTiles(Assets.TECHTILES_TILE);
			mapGroup.add(block);
		}
		
		/**
		 * Create the player, bullets, etc
		 */
		public function createPlayer():void {
			player = new Player();
		}
		
		/**
		 * Add groups to the state. They are rendered in the order they're added, so last added is always on top.
		 */
		public function addGroups():void {
			state.add(mapGroup);
			state.add(player);
		}
		
		/**
		 * Create the default camera for this level
		 */
		public function createCamera():void {
			FlxG.camera.setBounds(0, 0, levelSize.x, levelSize.y, true);
			FlxG.camera.follow(player, FlxCamera.STYLE_PLATFORMER);
		}
		
		/**
		 * Update each timestep
		 */
		public function update():void {
			FlxG.collide(mapGroup, player);
		}
	}
}

OOP 101: getters and setters

As a side note, you'll see TODO: getters and setters near the top of the file. I have declared all our variables public just to make it easier to learn. In real life, you would probably want most of these to be protected and use getter and setter methods to allow access from other classes. If those sentences sound like gibberish, check out the Encapsulation section of this intro to object oriented programming from The Code Project.

We'll break this down one method at a time. Let's start with our constructor:


/**
 * Constructor
 */
public function LevelBase(state:FlxState, levelSize:FlxPoint, blockSize:FlxPoint):void {
    this.state = state;
    this.levelSize = levelSize;
    this.blockSize = blockSize;
    if (levelSize && blockSize)
        this.levelBlocks = new FlxPoint(Math.floor(levelSize.x / blockSize.x), Math.floor(levelSize.y / blockSize.y));
    // setup groups
    this.mapGroup = new FlxGroup();
}

We pass in a state (ex: PlayState) so we can add our level objects to the state's display. Without this you'll see something about as interesting as a polar bear blinking in the snow. We already saw levelSize and blockSize inside PlayState.

levelBlocks is a convenience variable to tell us how many blocks we would have if we stacked them up horizontally and vertically across the whole levelSize. I chose to use Math.floor so we have only whole blocks (ex: 3.5 becomes 3), though you could use Math.ceil if you wanted to count partial blocks (3.5 becomes 4) or nothing if you wanted the fraction (ex: 3.5 stays at 3.5). This only comes into play if your level size isn't evenly divisible by your block size, so it's really just your preference.

Finally, let me say a word about groups. Groups are where a lot of the magic happens in Flixel. They come in peace. We can use them to speed up and simplify display, collisions, and even artificial intelligence. mapGroup will hold all the level blocks that form the map. As we add more functionality to LevelBase in future tutorials, we'll see more groups popping up.

Next we have the create() method:

/**
 * Create the whole level, including all sprites, maps, blocks, etc
 */
public function create():void {
    createMap();
    createPlayer();
    addGroups();
    createCamera();
}

Remember that we call this method from PlayState. This is really just a way to help us down the road when we make new levels. By splitting up createMap(), createPlayer(), addGroups(), createCamera(), and the other methods we'll be adding eventually, we allow future levels to only change what they want and leave everything else as it is. So, for instance, if we have a level where the map needs to be different but everything else is fine all we have to do is change the createMap() method and go about our merry way. Nice. As a side note, there is a rhyme to my reason on calling these methods in this order. If you want to check something on the level before deciding where the player starts off, for example, you need createMap() to come before createPlayer(). Since createCamera() needs to know about both the map and the player, it should come at the end. addGroups() really could go anywhere, but placing it toward the end allows us to define extra groups down the road and have it still play nicely with others.

Speaking of createMap(), why don't we do that next?

/**
 * Create the map (blocks, ladders, doors, etc)
 */
public function createMap():void {
    var block:FlxTileblock;
    // ground
    block = new FlxTileblock(0, levelSize.y - blockSize.y, levelSize.x, blockSize.y);
    block.loadTiles(Assets.TECHTILES_TILE);
    mapGroup.add(block);
    // ceiling
    block = new FlxTileblock(0, 0, levelSize.x, blockSize.y);
    block.loadTiles(Assets.TECHTILES_TILE);
    mapGroup.add(block);
    // left wall
    block = new FlxTileblock(0, blockSize.x, blockSize.y, levelSize.y - blockSize.y * 2);
    block.loadTiles(Assets.TECHTILES_TILE);
    mapGroup.add(block);
    // right wall
    block = new FlxTileblock(levelSize.x - blockSize.x, blockSize.y, blockSize.x, levelSize.y - blockSize.y * 2);
    block.loadTiles(Assets.TECHTILES_TILE);
    mapGroup.add(block);
}

More Flixel magic. FlxTileblock allows us to make a solid block of whatever size we want, and Flixel will fill it in with smaller blocks from our tileset sprite sheet (ex: Assets.TECHTILES_TILE). blockSize describes how big those smaller blocks will be (although changing blockSize won't change what's displayed - you have to actually change your sprite sheet for that). So, when we use block = new FlxTileblock(0, levelSize.y - blockSize.y, levelSize.x, blockSize.y), we're telling Flixel to make a block with these dimensions:
x = 0
y = levelSize.y - blockSize.y (one block up from the bottom of the level)
width = levelSize.x (whole level width)
height = blockSize.y (one block high)

Now that we have the right dimensions, we tell Flixel to fill that space in with tiles from our tileset sprite sheet (Assets.TECHTILES_TILE). Once it's ready to go we can add it to mapGroup so it will be displayed and used in collisions. Rinse and repeat for ceiling, left wall, and right wall.

Next we have the very simple createPlayer() method:

{literal}
/**
 * Create the player, bullets, etc
 */
public function createPlayer():void {
    player = new Player();
}

Once again this is to help us down the road. If we wanted to change where the player starts the level, update gravity, or anything else different than normal, all we have to do is override this method.

addGroups() is a very key method:

/**
 * Add groups to the state. They are rendered in the order they're added, so last added is always on top.
 */
public function addGroups():void {
    state.add(mapGroup);
    state.add(player);
}

The comment at the top is the big takeaway here. player is added last, so player is always visible. When we have coins, powerups, bullets, and water, this will be important. Additionally, you must call state.add() for every group you want to actually show up on the screen (NOTE: since player isn't part of another group, we add it directly). Otherwise you'll have those blinking polar bears again.

Next on our hit list is a function we won't be seeing again, createCamera():

/**
 * Create the default camera for this level
 */
public function createCamera():void {
    FlxG.camera.setBounds(0, 0, levelSize.x, levelSize.y, true);
    FlxG.camera.follow(player, FlxCamera.STYLE_PLATFORMER);
}

We are only planning on using one camera view for the whole game, so we can use the built-in FlxG.camera. Calling setBounds() just tells the camera how far it can go (anywhere on our level), while follow() tells the camera to track player anywhere it goes (using the platformer style). FlxCamera.STYLE_PLATFORMER means the camera has a little lag in following the player up and down, but it's pretty fast going left and right. If you didn't want any lag in following the player, you would use FlxCamera.STYLE_LOCKON. It's just a stylistic choice.

At last, we reach our final method:

/**
 * Update each timestep
 */
public function update():void {
    FlxG.collide(mapGroup, player);
}

This method will be growing as we add coins, weapons, and other functionality to LevelBase. Right now, though, all it does is do default collision handling between mapGroup (currently just the box around the level) and player. Without this, player would fall through the floor.

Running and Jumping with Player

Behind door number two we have Player.as. Once again, we'll need to break this down into pieces. And once again, here is the whole code dump for those big picture people:

package sprites
{
	import org.flixel.*;
	import org.flixel.plugin.photonstorm.FlxControl;
	import org.flixel.plugin.photonstorm.FlxControlHandler;
	
	public class Player extends FlxSprite
	{
		/**
		 * Constants
		 */
		protected static const STARTX_DEFAULT:int = 300;
		protected static const STARTY_DEFAULT:int = 300;
		protected static const GRAVITY_DEFAULT:Number = 420;
		protected static const RUNSPEED:int = 80;
		protected static const JUMPHEIGHT:Number = 200;
		protected static const JUMPDELAY:Number = 250;
		protected static const JUMPFROMFALL:Number = 200;
		protected static const SIZE:int = 8;
		
		/**
		 * Constructor
		 */
		public function Player(X:Number = STARTX_DEFAULT, Y:Number = STARTY_DEFAULT):void {
			super(X, Y);
			loadGraphic(Assets.PLAYER_SPRITE, true, true, SIZE);
			// movement controls
			FlxControl.create(
				this, // sprite
				FlxControlHandler.MOVEMENT_ACCELERATES, // movement type = accelerate (instead of instant top speed)
				FlxControlHandler.STOPPING_DECELERATES, // stopping type = decelerate (instead of instant stop)
				1, // player number (1-4)
				true, // update facing direction (ex: looking left while moving left)
				false // don't use default arrow keys because we want a jump button
			);
			FlxControl.player1.setCursorControl(false, false, true, true); // use left and right
			FlxControl.player1.setJumpButton(
				"SPACE", // key to use
				FlxControlHandler.KEYMODE_PRESSED, // every time the button is pressed
				JUMPHEIGHT, // how high to jump
				FlxObject.FLOOR, // the surfaces to allow jumping (ex: could have left/right to jump off walls)
				JUMPDELAY, // how long to delay before allowing jumping again (in milliseconds)
				JUMPFROMFALL // how long to allow jumping even after falling off a platform (in milliseconds)
			);
			// movement speed (since we're using MOVEMENT_ACCELERATES)
			FlxControl.player1.setMovementSpeed(
				RUNSPEED * 4, // x acceleration => top speed reached in 1/4 of a second
				0, // y acceleration
				RUNSPEED, // max x speed
				JUMPHEIGHT, // max y speed
				RUNSPEED * SIZE, // x drag
				0 // y drag
			);
			// gravity
			FlxControl.player1.setGravity(0, GRAVITY_DEFAULT);
			// animations
			addAnimation("idle", [0]);
			addAnimation("run", [1, 2, 3, 0], 12);
			addAnimation("jump", [4]);
			addAnimation("idle_up", [5]);
			addAnimation("run_up", [6, 7, 8, 5], 12);
			addAnimation("jump_up", [9]);
			addAnimation("jump_down", [10]);
		}
		
		/**
		 * Update each timestep
		 */
		public override function update():void {
			// animations
			if(velocity.y != 0) {
				play("jump");
			} else if(velocity.x == 0) {
				play("idle");
			} else {
				play("run");
			}
			// finish updating
			super.update();
		}
		
		/**
		 * Free up resources
		 */
		override public function destroy():void {
			FlxControl.clear(); // free up plugin
			super.destroy();
		}
	}
}

Yikes, right? Well, let's break it down into pieces again, starting with the constructor. The first thing we do is load in the graphic for the player: loadGraphic(Assets.PLAYER_SPRITE, true, true, SIZE). This means we're loading in the graphic from our Assets class, that it is animated, that we want it flipped when facing the opposite direction, and that the graphic is SIZE pixels.

Next we have our first Flixel Power Tools section:

// movement controls
FlxControl.create(
    this, // sprite
    FlxControlHandler.MOVEMENT_ACCELERATES, // movement type = accelerate (instead of instant top speed)
    FlxControlHandler.STOPPING_DECELERATES, // stopping type = decelerate (instead of instant stop)
    1, // player number (1-4)
    true, // update facing direction (ex: looking left while moving left)
    false // don't use default arrow keys because we want a jump button
);

FlxControl is one of the helper classes included in the Flixel Power Tools. While there are easier ways of controlling a sprite, FlxControl adds some nice features that aren't trivial. First off, we're using MOVEMENT_ACCELERATES, which means the player has to accelerate from a stop to full speed. If we used FlxControlHander.MOVEMENT_INSTANT, the player would reach top speed immediately. The next line chooses the same option for stopping (decelerate from our movement speed to a stop rather than immediately stopping). After that we have our player number. By default, FlxControl has room to control four sprites, and using 1 means we'll access ours with FlxControl.player1. The only other tricky part is that we pass in false at the end. In a 2D platformer, the player can only freely move horizontally. Vertical movement is handled through gravity, jumping, jetpacks, etc. If we were making a top-down RPG or space game, we could probably use true here and just move up, down, left, and right using the default arrow keys.

Since we're not using the default arrow keys, we need to setup our controls:

FlxControl.player1.setCursorControl(false, false, true, true); // use left and right
FlxControl.player1.setJumpButton(
    "SPACE", // key to use
    FlxControlHandler.KEYMODE_PRESSED, // every time the button is pressed
    JUMPHEIGHT, // how high to jump
    FlxObject.FLOOR, // the surfaces to allow jumping (ex: could have left/right to jump off walls)
    JUMPDELAY, // how long to delay before allowing jumping again (in milliseconds)
    JUMPFROMFALL // how long to allow jumping even after falling off a platform (in milliseconds)
);

As we already said, we aren't using up and down, just left and right. FlxControl does have jump behavior pre-baked, and setJumpButton sets all the parameters. The comments explain these parameters, but let me comment on FlxControlHandler.KEYMODE_PRESSED. This means that the player will keep trying to jump as long as the player holds down the jump button (spacebar in our game). But we also set JUMPDELAY, so the player won't actually jump again until JUMPDELAY milliseconds have passed. We would have different behavior with FlxControlHandler.KEYMODE_JUST_DOWN (jump once and don't try again until the player releases the key and presses it again) or FlxControlHandler.KEYMODE_RELEASED (only try to jump once the player releases the key, not when it is pressed down). The surface parameter (FlxObject.FLOOR) tells FlxControl to only allow the player to jump when touching the given surface. If we had passed in FlxObject.FLOOR | FlxObject.WALL, the player could jump off floors and walls. Finally, the last parameter (JUMPFROMFALL) allows the player to walk off a ledge and still be able to jump for an instant (a grace period, if you will).

Now that we have our controls, we need to control our movement:

// movement speed (since we're using MOVEMENT_ACCELERATES)
FlxControl.player1.setMovementSpeed(
    RUNSPEED * 4, // x acceleration => top speed reached in 1/4 of a second
    0, // y acceleration
    RUNSPEED, // max x speed
    JUMPHEIGHT, // max y speed
    RUNSPEED * SIZE, // x drag
    0 // y drag
);
// gravity
FlxControl.player1.setGravity(0, GRAVITY_DEFAULT);

The comments explain what each argument is doing, so let me focus on the acceleration side of things (the first two parameters). This value is in pixels per second, so RUNSPEED * 4 when our maximum speed is RUNSPEED means we're accelerating to top speed in 1/4 of a second. If we had used RUNSPEED * 2 it would have meant top speed in 1/2 of a second, and using RUNSPEED would have left us at a full second to top speed. Get it? The last two parameters (x and y drag) determine how quickly we decelerate to a stop. Same math as accelerating.

Just to finish things off, we define our animations. There's nothing special here, but I'll list it again anyway. We set the animation name, the array of frames to use from our spritesheet, and finally the frames per second (if it uses more than one frame).

// animations
addAnimation("idle", [0]);
addAnimation("run", [1, 2, 3, 0], 12);
addAnimation("jump", [4]);
addAnimation("idle_up", [5]);
addAnimation("run_up", [6, 7, 8, 5], 12);
addAnimation("jump_up", [9]);
addAnimation("jump_down", [10]);

That constructor was a beast, but Flixel Power Tools really takes care of it the rest of the way. If we weren't using FlxControl, our update function would have a ton of logic inside to handle keyboard events, movement speed, gravity, etc. Instead, all we need to do is figure out which animation to use:

/**
 * Update each timestep
 */
public override function update():void {
    // animations
    if(velocity.y != 0) {
        play("jump");
    } else if(velocity.x == 0) {
        play("idle");
    } else {
        play("run");
    }
    // finish updating
    super.update();
}

I'm pretty sure you could call super.update() before figuring out your animation, too. It might give you slightly different results, but it's a matter of preference as far as I know. To wrap this class in a nice little bow, we need to play nice with our resources:

/**
 * Free up resources
 */
override public function destroy():void {
    FlxControl.clear(); // free up plugin
    super.destroy();
}

This is really just one of the requirements of using FlxControl. Since it uses the Flixel 2.5 plugin architecture, we need to let Flixel know that we're done with it once Player is destroyed.

Rounding Things Out

We're in the home stretch! All that stands between us and a finished tutorial is some housekeeping. First, we need to update Assets.as to allow Player and LevelBase to use the new images:

// sprites
[Embed(source="../assets/sprites/spaceman.png")] public static var PLAYER_SPRITE:Class;

// tiles
[Embed(source="../assets/tiles/tech_tiles.png")] public static var TECHTILES_TILE:Class;

Finally, we need to tell Flixel about the FlxControl plugin from our Main class:

/**
 * Constructor
 */
public function Main() {
    super(320, 240, MenuState, 2);
    // load power tools plugins
    FlxG.addPlugin(new FlxControl());
}

We'll never touch Main.as again in this tutorial series.

Conclusion

That was a lot of work, but now we're off and running...and jumping. Things are starting to get interesting around here, and our next tutorial will help as we'll be adding weapons.

Previous Tutorial: Getting Started
Next Tutorial: Adding Weapons