Tutorial 3 - IMGD Brothers

Getting Deeper in Game Development

  1. Introduction
  2. Set the Stage
  3. Make it Go
  4. Congrats

Introduction

In this tutorial you will learn how to:

As a look ahead, the game you will make is:

This is a flash file, but your browser does not run it. You might try IE on Windows.

You can download the full project source here. The archived file includes the sprite sheet used for the player's animations.

This tutorial only assumes you have some basic computer knowhow and a willingness to learn. The tutorial will use ActionScript 3.0 to handle the interactive elements. Though this tutorial is more code and math heavy than the last tutorial, all the code will be provided and the tutorial will try to explain it as clearly as possible. No prior programming experience is neccessary, though it would help.

Setting up the Project

Make a new project, calling it something like "IMGD Bros", and make two Layers, the "Foreground" and the "Background" (Fig.1).

We need a nice background for our game, I used the gradient tool to make a nice virtual horizon (Fig. 1). First, make a rectangle in the background layer that covers the whole stage. Then, see the Adobe tutorial on Gradients for more details.

Figure 1. Setting up the Background

Now you need to create a new player. Using the same technique as in the last tutorial, break apart the sprite sheet and create a frame for each animation of an idle sequence. If you space the keyframes out on the timline, it will stay on one sprite for a longer time period. If your Flash movie is set to 20 frames per second (fps), spending 10 frames on one sprite will mean that the player will see it for half a second. To extend an animation (without actually drawing new sprites), you can create a blank keyframe a few frames beyond your starting keyframe. Then, paste your single frame of animation into the set of frames (if you paste it into 1 of them, it will apply to all of them). Then, just keep doing this for every single frame of the animation so that it drags on longer and displays longer.

You should also make a frame label on the first frame of the Idle animation, and after the last frame, create a blank keyframe with an action of gotoAndPlay("Idle") so that the animation would repeat. A few frames to the right of the blank keyframe, create a "Run" animation, and to the right of that a "Jump" animation (again, making the first frame have the proper frame label) (Fig. 2). In my case, I actually created a "RunStart" animation which goes directly into the "Run" animation, which at the end loops back to "Run". This helps the transition from idle to running to be less jarring.

Make sure your animation has a symbol name and class name of "Player".

Important:

You must make sure that the symbol's registration point (the little + icon when you are editting it that normally starts in the top left) is centered horizontally on the symbol. This will ensure that we can mirror it without any problems. I put the registration point centered at the Player sprite's feet. When you are breaking up your spritesheet, make sure you are putting your sprites where you want in relation to the '+'. Wherever you put the '+', this is where the player will collide with objects. So, if it at his head, then the player won't collide with the ground until the head hits the ground (meaning his feet and body will be below ground).
Figure 2. Making the Player

Now, start drawing a level. This should be done in the foreground layer. You should make the level bigger than the stage by two or three times. This is because as the player moves throughout the level, our game will "move the camera" so that the relevant part of the stage is displayed. Next, turn the level into a symbol. First, make sure the background layer is locked. Then, hit ctrl-a to select all. You can then turn the entire level into a symbol and continue. Name the level "Level 1" and give it an instance name of "Level1". "Move the camera" is in quotes because Flash has no real "camera" - how we achieve this effect will be explained in a little bit.

Figure 3. Creating a Level

Put a Player symbol (this is the symbol with all the animations) onto the stage, and set it's instance name to "Play1".

Figure 4. Placing the Player

Note that you need to make sure the Player is "above" the level. To ensure this, with the Player selected, click "Modify" -> "Arrange" -> "Bring to Front".

Select both the Player symbol and the Level symbol, and turn the group into a new symbol, named "GameWorld" (with an instance name of "World") (Fig. 5). By doing this, we will be able to move the location of the World symbol, and to the player it will look like we are moving a camera which will eventually be following the player.

Figure 5. Combining into a GameWorld

Run the movie now, although not much will happen. You can see what my movie looks like at this point below, too - not much to see beyond a working idle animation.

This is a flash file.

Make it Go

Although the movie is not yet very impressive, we now have all the building blocks in place to actually add some interaction, the first of which will be gravity for the Player! In "Player.as", add the following code:

package  
{
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.geom.Point;
	
	public class Player extends MovieClip
	{
		public var velo:Point = new Point(0,0);
		private const GRAVITY_FORCE:Number = 0.7;        
		private const MAX_FALLSPEED = 10;
        
		private var level:MovieClip;
		
		public function Player() 
		{			
			this.addEventListener(Event.ENTER_FRAME, Update);
		}
		
		function Update(e:Event) {			
			
			var c:Point = new Point(x, y);

			velo.y += GRAVITY_FORCE;
            
			if (Math.abs(velo.y) > MAX_FALLSPEED)
				velo.y = MAX_FALLSPEED;
				
			c.x += velo.x;
			c.y += velo.y;
			
			x = c.x;
			y = c.y;
		}		
}

The code above makes a variable called "velo" (shorthand for "velocity"). In physics, for every unit of time the acceleration for that time is added to the velocity, and the velocity is added to the position. This creates an interesting effect. A constant velocity causes the position to linearly increase (this is how the Stars and Baddies in the last tutorial move). If we have a constant acceleration, though, the velocity will linearly increase, which means that the position will exponentially increase! This gives the realistic parabolic behavior that we expect from gravity (when you throw a ball into the air, it slows down, stops, and then accelerates). This is exactly what we do in our Update loop on line 23 by adding a constant GRAVITY_FORCE to the velocity, and then adding the velocity to the position on line 28-29.

If you run the movie now, your player will fall straight through the floor. This is not very good for a fun player experience, but we can fix it. We just need to add some collision detection. In "Player.as", add a little bit more:

 
package  
{
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	
	public class Player extends MovieClip
	{
		private const GRAVITY_FORCE:Number = 0.7;
		private const MAX_ITERATIONS = 20;
		private const MAX_FALLSPEED = 10;
		
		public var bounds:Rectangle = new Rectangle(0, 0, 23.5, 27.5);
		public var velo:Point = new Point(0,0);
		private var level:MovieClip;
		private var onGround:Boolean = false;
		
		public function Player() 
		{			
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
			
			this.addEventListener(Event.ENTER_FRAME, Update);
		}
		
		private function init(e:Event = null):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);			
			level = (parent as MovieClip).Level;
		}
		
		function Update(e:Event) {	
					
			var c:Point = new Point(x, y);
			c = (parent as MovieClip).localToGlobal(c);
			var old:Point = c.clone();
			
			//--------Gravity, Friction, and Velocity Calculation
			
			if (!onGround) 
				velo.y += GRAVITY_FORCE;	
			
			if (Math.abs(velo.y) > MAX_FALLSPEED)
				velo.y = MAX_FALLSPEED;
								
			c.x += velo.x;
			c.y += velo.y;
			
			//--------World Collision Handling
			
			if (level.hitTestPoint(c.x - bounds.width / 2 + 2 , c.y+1, true)
				||  level.hitTestPoint(c.x + bounds.width / 2 -2, c.y+1, true)) {
				if (!onGround) {
					if (level.hitTestPoint(c.x - bounds.width / 2 + 2 , c.y , true))
						c.y = findIntersection( old, c, new Point( - bounds.width / 2 + 2, 0)).y;
					else if (level.hitTestPoint(c.x + bounds.width / 2 -2, c.y, true))
						c.y = findIntersection( old, c, new Point( bounds.width / 2 - 2, 0)).y;	
				}
				velo.y = 0;
				onGround = true;
				
			} else {			
				onGround = false;
			}
			
			c = (parent as MovieClip).globalToLocal(c);
			x = c.x;
			y = c.y;
		}
		
		// Uses a bisection method to find the edge of an obstacle (tests the midpoint, and then goes closer or further
		// based on if it is solid at the midpoint)
		private function findIntersection( start:Point, end:Point, offset:Point ):Point {
			var a:Point = new Point(start.x + offset.x, start.y + offset.y);
			var b:Point = new Point();
			var c:Point = new Point(end.x + offset.x, end.y + offset.y);
			for (var i:int = 0; i < MAX_ITERATIONS; i++) {
				b.x = (a.x + c.x) / 2.0;
				b.y = (a.y + c.y) / 2.0;
				if (level.hitTestPoint(b.x, b.y, true)) {
					c = b.clone(); 	// if it was a collision, the edge must be between a and b.
				} else { 		// otherwise, it must be between b and c
					a = b.clone(); 	
					if ((Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)) < Math.pow(0.1, 2)) 
						break;											
				}							
			}
			return new Point(a.x - offset.x, a.y - offset.y);
		}
	}
	
}

Initially, we use Flash's built in function "hitTestPoint()" on line 51 to test whether the location we are about to move the player to is a solid part of the level. If it is, we then use a new function we have created called "findIntersection". This function uses a binary search algorithm to find the edge of the collision between the player and the level. It works by repeatedly testing midpoints. Initially it tests the midpoint of the Player's old position, and the Player's new position. If it detects a collision at the midpoint it means that we are too close to the new position, so it sets the midpoint as point C, and then runs again. This means it is then checking the midpoint between the old position, and the midpoint between the old and the new. It keeps repeating this way, setting the midpoint to C if it is a collision, and to A if it isn't, until the difference between the points is very small (or if it is running for too long, as defined by MAX_ITERATIONS). After it is done, it returns the best point it found (the closest to the edge of collision), and we use this number to move the Player to be just touching the ground, as well as reseting the velocity to 0 (making it stop trying to go down).

You may have also noticed the "localToGlobal()" and "globalToLocal()" functions that we use at the start and end of the function (lines 36 and 66). The "hitTest()" function uses the stage's (global) coordinate system, but because the level is within the GameWorld symbol, it is in a different coordinate system ([0,0] at a different point). These two functions translate the Player's position to the global coordinate system, and then back to the GameWorld's local coordinate system before changing the Player's position.

This is a flash file.

Have you tried running it yet? If not, try now. The Player should fall and stop on the platform below it. Pretty boring, but we're moving along. Now, let's add some Player interaction.

package  
{
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.ui.Keyboard;
	
	public class Player extends MovieClip
	{
		private const TOP_SPEED:Number = 8;
		private const GROUND_FRICTION:Number = 0.9;
		private const RUN_FORCE:Number = 0.8;	
		private const GRAVITY_FORCE:Number = 0.7;
		private const MAX_ITERATIONS = 20;
		private const MAX_FALLSPEED = 10;
		
		public var bounds:Rectangle = new Rectangle(0, 0, 23.5, 27.5);
		public var velo:Point = new Point(0,0);
		private var level:MovieClip;
		private var onGround:Boolean = false;
		
		private var kRight:int = 0;
		private var kLeft:int = 0;
		private var kJump:int = 0;
			// 0 - not pressed
			// 1 - just pressed
			// 2 - held down
				
		private var anim:int = 0;
			// 0 = Idle		
			// 1 = Running
			// 2 = Jump
		
		public function Player() 
		{			
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
			
			this.addEventListener(Event.ENTER_FRAME, Update);
			stage.addEventListener(KeyboardEvent.KEY_DOWN, KeyDown);
			stage.addEventListener(KeyboardEvent.KEY_UP, KeyUp);
		}
		
		private function init(e:Event = null):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);			
			level = (parent as MovieClip).Level;
		}
		
		public function KeyDown(e:KeyboardEvent) {
			switch  (e.keyCode) {
				case Keyboard.RIGHT:
					if (kRight == 0) kRight = 1;
					break;
				case Keyboard.LEFT:
					if (kLeft == 0) kLeft = 1;
					break;
				case Keyboard.UP:
				case Keyboard.SPACE:
					if (kJump == 0) kJump = 1;
					break;
			}
		}
		
		public function KeyUp(e:KeyboardEvent) {
			switch  (e.keyCode) {				
				case Keyboard.RIGHT:
					kRight = 0;
					break;
				case Keyboard.LEFT:
					kLeft = 0;
					break;
				case Keyboard.UP:
				case Keyboard.SPACE:
					kJump = 0;
					break;
			}
		}
		
		function Update(e:Event) {			
			
			var c:Point = new Point(x, y);
			c = (parent as MovieClip).localToGlobal(c);
			var old:Point = c.clone();
			
			//--------Gravity, Friction, and Velocity Calculation
			
			if (onGround) {			
				if (kLeft != 0) 
					velo.x -= RUN_FORCE;				
				if (kRight != 0) 
					velo.x += RUN_FORCE;
				if ((kLeft == 0 && kRight == 0) || (kLeft != 0 && kRight != 0)) {
					velo.x *= GROUND_FRICTION;	
					if (Math.abs(velo.x) < 0.5)
						velo.x = 0; // no pixel by pixel creeping.	
				}
			} else {
				velo.y += GRAVITY_FORCE;		
			}
			
			
			if (Math.abs(velo.x) > TOP_SPEED) 
				velo.x = TOP_SPEED * (velo.x < 0? -1:1);
			if (Math.abs(velo.y) > MAX_FALLSPEED)
				velo.y = MAX_FALLSPEED;
				
			c.x += velo.x;
			c.y += velo.y;
			
			//--------World Collision Handling
			
			if (level.hitTestPoint(c.x - bounds.width / 2 + 2 , c.y+1, true)
				||  level.hitTestPoint(c.x + bounds.width / 2 -2, c.y+1, true)) {
				if (!onGround) {
					if (level.hitTestPoint(c.x - bounds.width / 2 + 2 , c.y , true))
						c.y = findIntersection( old, c, new Point( - bounds.width / 2 + 2, 0)).y;
					else if (level.hitTestPoint(c.x + bounds.width / 2 -2, c.y, true))
						c.y = findIntersection( old, c, new Point( bounds.width / 2 - 2, 0)).y;	
				}
				velo.y = 0;
				onGround = true;
				
			} else {			
				onGround = false;	
			}
			
			
			//--------Animation Handling
			
			if (onGround) {
				if (Math.abs(velo.x) > 1) 
					PlayAnim(1);			
				if (Math.abs(velo.x) <= 1) 
					PlayAnim(0);				
			} else
				PlayAnim(2);
							
			if (velo.x > 0) 	 this.scaleX = -1.0;
			else if (velo.x < 0) this.scaleX = 1.0;
			
			//-------Key held detection..
			if (kLeft == 1) kLeft = 2;
			if (kRight == 1) kRight = 2;
			if (kJump == 1) kJump = 2;
			
			c = (parent as MovieClip).globalToLocal(c);
			x = c.x;
			y = c.y;
		}
		
		// Uses a bisection method to find the edge of an obstacle (tests the midpoint, and then goes closer or further
		// based on if it is solid at the midpoint)
		private function findIntersection( start:Point, end:Point, offset:Point ):Point {
			var a:Point = new Point(start.x + offset.x, start.y + offset.y);
			var b:Point = new Point();
			var c:Point = new Point(end.x + offset.x, end.y + offset.y);
			for (var i:int = 0; i < MAX_ITERATIONS; i++) {
				b.x = (a.x + c.x) / 2.0;
				b.y = (a.y + c.y) / 2.0;
				if (level.hitTestPoint(b.x, b.y, true)) {
					c = b.clone(); 	// if it was a collision, the edge must be between a and b.
				} else { 		// otherwise, it must be between b and c
					a = b.clone(); 	
					if ((Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)) < Math.pow(0.1, 2)) 
						break;											
				}							
			}
			return new Point(a.x - offset.x, a.y - offset.y);
		}
		
		private function PlayAnim(num:int) {
			if (num == 0 && anim != 0) {
				gotoAndPlay("Idle");
				anim = 0;
			} else if (num == 1 && anim != 1) {
				gotoAndPlay("RunStart");
				anim = 1;
			}
			else if (num == 2 && anim != 2) {
				gotoAndPlay("Jump");
				anim = 2;
			}
		}
	}
	
}

The most important aspect of the changes we just made was keyboard input. We made it so that we can detect keyboard input by registering a KEY_DOWN and a KEY_UP event listener on line 42 and 43. For each key we are interested in, we save a state. We set it at 0 when it is not pressed, and when pressed we switch it to 1. This indicates it was just pressed (this is our own meaning for it, the computer itself doesn't care what we set it to). At the end of the Update loop we change any button variable that we have set at 1 to 2 (lines 145-147). This ensures that we only have a key in the "key down" state for one frame. 2 will mean that the button is being held down. When the key is released, we just change it back to 0. We could have created a 4th state (setting it to 3) for the frame on which the key was released, but we won't need that for our game.

In the Update loop, we test this data to find out which direction the Player is trying to move. We then add a RUN_FORCE to the x velocity (line 92 and 93). We also set a top speed (line 105-106), and multiply the velocity by a friction force (line 96) so that when the key is released the Player doesn't keep going, or suddenly stop, but instead slides to a stop. We have created a movement that has a fast acceleration, but is capped at a max speed, and which takes a moment to come to a stop.

We also have added some simple animation switching to help increase the Player experience. When the player is falling it has a fall animation (line 139), and when running, a run animation (line 135). We use the x velocity to detect which direction the player should be facing, and if necessary, we mirror the sprite by setting the x scale to -1 (lines 142-143). Mirroring the sprite doesn't work correctly if the Player symbol's registration point isn't centered, which is why it was important to pay attention to this while making the symbol.

Try clicking on the movie below and running left and right by pressing the left and right arrow keys.

This is a flash file.

You may have noticed that you can run through walls, but this is a rather easy thing to fix due to the work we already put into making our findIntersection function.

package  
{
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.ui.Keyboard;
	
	public class Player extends MovieClip
	{
		private const TOP_SPEED:Number = 8;
		private const GROUND_FRICTION:Number = 0.9;
		private const RUN_FORCE:Number = 0.8;	
		private const GRAVITY_FORCE:Number = 0.7;
		private const MAX_ITERATIONS = 20;
		private const MAX_FALLSPEED = 10;
		
		public var bounds:Rectangle = new Rectangle(0, 0, 23.5, 27.5);
		public var velo:Point = new Point(0,0);
		private var level:MovieClip;
		private var onGround:Boolean = false;
		
		private var kRight:int = 0;
		private var kLeft:int = 0;
		private var kJump:int = 0;
			// 0 - not pressed
			// 1 - just pressed
			// 2 - held down
				
		private var anim:int = 0;
			// 0 = Idle		
			// 1 = Running
			// 2 = Jump
		
		public function Player() 
		{			
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
			
			this.addEventListener(Event.ENTER_FRAME, Update);
			stage.addEventListener(KeyboardEvent.KEY_DOWN, KeyDown);
			stage.addEventListener(KeyboardEvent.KEY_UP, KeyUp);
		}
		
		private function init(e:Event = null):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);			
			level = (parent as MovieClip).Level;
		}
		
		public function KeyDown(e:KeyboardEvent) {
			switch  (e.keyCode) {
				case Keyboard.RIGHT:
					if (kRight == 0) kRight = 1;
					break;
				case Keyboard.LEFT:
					if (kLeft == 0) kLeft = 1;
					break;
				case Keyboard.UP:
				case Keyboard.SPACE:
					if (kJump == 0) kJump = 1;
					break;
			}
		}
		
		public function KeyUp(e:KeyboardEvent) {
			switch  (e.keyCode) {				
				case Keyboard.RIGHT:
					kRight = 0;
					break;
				case Keyboard.LEFT:
					kLeft = 0;
					break;
				case Keyboard.UP:
				case Keyboard.SPACE:
					kJump = 0;
					break;
			}
		}
		
		function Update(e:Event) {			
			
			var c:Point = new Point(x, y);
			c = (parent as MovieClip).localToGlobal(c);
			var old:Point = c.clone();
			
			//--------Gravity, Friction, and Velocity Calculation
			
			if (onGround) {			
				if (kLeft != 0) 
					velo.x -= RUN_FORCE;				
				if (kRight != 0) 
					velo.x += RUN_FORCE;
				if ((kLeft == 0 && kRight == 0) || (kLeft != 0 && kRight != 0)) {
					velo.x *= GROUND_FRICTION;	
					if (Math.abs(velo.x) < 0.5)
						velo.x = 0; // no pixel by pixel creeping.	
				}
			} else {
				velo.y += GRAVITY_FORCE;		
			}
			
			
			if (Math.abs(velo.x) > TOP_SPEED) 
				velo.x = TOP_SPEED * (velo.x < 0? -1:1);
			if (Math.abs(velo.y) > MAX_FALLSPEED)
				velo.y = MAX_FALLSPEED;
				
			c.x += velo.x;
			c.y += velo.y;
			
			//--------World Collision Handling
			
			if (velo.x < 0) {
				if (level.hitTestPoint(c.x - (bounds.width / 2 + 1), c.y - bounds.height / 2, true)) {
					c.x = findIntersection(old, c, new Point( - (bounds.width / 2 + 1), - bounds.height / 2)).x;	
					velo.x = 0;
				} else if (level.hitTestPoint(c.x - (bounds.width / 2 + 1), c.y - 2, true)) {
					c.x = findIntersection(old, c, new Point( - (bounds.width / 2 + 1), -2)).x;	
					velo.x = 0;
				}
			}
			
			if (velo.x > 0) {
				if (level.hitTestPoint(c.x + (bounds.width / 2 + 1), c.y - bounds.height / 2, true)) {
					c.x = findIntersection(old, c, new Point( (bounds.width / 2 + 1), - bounds.height / 2)).x;	
					velo.x = 0;
				} else if (level.hitTestPoint(c.x + (bounds.width / 2 + 1), c.y - 2, true)) {
					c.x = findIntersection(old, c, new Point( (bounds.width / 2 + 1), -2)).x;
					velo.x = 0;
				}				
			}
			
			if (level.hitTestPoint(c.x - bounds.width / 2 + 2 , c.y+1, true)
				||  level.hitTestPoint(c.x + bounds.width / 2 -2, c.y+1, true)) {
				if (!onGround) {
					if (level.hitTestPoint(c.x - bounds.width / 2 + 2 , c.y , true))
						c.y = findIntersection( old, c, new Point( - bounds.width / 2 + 2, 0)).y;
					else if (level.hitTestPoint(c.x + bounds.width / 2 -2, c.y, true))
						c.y = findIntersection( old, c, new Point( bounds.width / 2 - 2, 0)).y;	
				}
				velo.y = 0;
				onGround = true;
				
			} else {			
				onGround = false;	
			}
			
			
			//--------Animation Handling
			
			if (onGround) {
				if (Math.abs(velo.x) > 1) 
					PlayAnim(1);			
				if (Math.abs(velo.x) <= 1) 
					PlayAnim(0);				
			} else
				PlayAnim(2);
							
			if (velo.x > 0) 	 this.scaleX = -1.0;
			else if (velo.x < 0) this.scaleX = 1.0;
			
			//-------Key held detection..
			if (kLeft == 1) kLeft = 2;
			if (kRight == 1) kRight = 2;
			if (kJump == 1) kJump = 2;
			
			c = (parent as MovieClip).globalToLocal(c);
			x = c.x;
			y = c.y;
		}
		
		// Uses a bisection method to find the edge of an obstacle (tests the midpoint, and then goes closer or further
		// based on if it is solid at the midpoint)
		private function findIntersection( start:Point, end:Point, offset:Point ):Point {
			var a:Point = new Point(start.x + offset.x, start.y + offset.y);
			var b:Point = new Point();
			var c:Point = new Point(end.x + offset.x, end.y + offset.y);
			for (var i:int = 0; i < MAX_ITERATIONS; i++) {
				b.x = (a.x + c.x) / 2.0;
				b.y = (a.y + c.y) / 2.0;
				if (level.hitTestPoint(b.x, b.y, true)) {
					c = b.clone(); 	// if it was a collision, the edge must be between a and b.
				} else { 		// otherwise, it must be between b and c
					a = b.clone(); 	
					if ((Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)) < Math.pow(0.1, 2)) 
						break;											
				}							
			}
			return new Point(a.x - offset.x, a.y - offset.y);
		}
		
		private function PlayAnim(num:int) {
			if (num == 0 && anim != 0) {
				gotoAndPlay("Idle");
				anim = 0;
			} else if (num == 1 && anim != 1) {
				gotoAndPlay("RunStart");
				anim = 1;
			}
			else if (num == 2 && anim != 2) {
				gotoAndPlay("Jump");
				anim = 2;
			}
		}
	}
	
}

All we did was use the same algorithm as with testing for the ground. First we test to see if the player is about to move into a wall, and if they are, we find the intersection of their movement and the wall so that we can place them directly next to it. When we hit a wall, we set our x velocity to 0 because if we don't the player will feel "stuck" to the wall while the player's velocity slows.

This is a flash file.

If you run it now, you will notice that it looks a lot better, but that you have a lot less freedom. You properly crash into walls, but without a way to move upwards you quickly run out of options. It's time to add a jump function. To do this we need to first make it so that when the player is on the ground they can jump, so that they can impact their movement in the air slightly, and so that they can't jump through ceilings.

package  
{
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.ui.Keyboard;
	
	public class Player extends MovieClip
	{
		private const TOP_SPEED:Number = 8;
		private const GROUND_FRICTION:Number = 0.9;
		private const RUN_FORCE:Number = 0.8;	
		private const JUMP_FORCE:Number = -7;
		private const AIR_CONTROL:Number = 0.2;
		private const GRAVITY_FORCE:Number = 0.7;
		private const MAX_ITERATIONS = 20;
		private const MAX_FALLSPEED = 10;
		
		public var bounds:Rectangle = new Rectangle(0, 0, 23.5, 27.5);
		public var velo:Point = new Point(0,0);
		private var level:MovieClip;
		private var onGround:Boolean = false;
		
		private var kRight:int = 0;
		private var kLeft:int = 0;
		private var kJump:int = 0;
			// 0 - not pressed
			// 1 - just pressed
			// 2 - held down
				
		private var anim:int = 0;
			// 0 = Idle		
			// 1 = Running
			// 2 = Jump
		
		public function Player() 
		{			
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
			
			this.addEventListener(Event.ENTER_FRAME, Update);
			stage.addEventListener(KeyboardEvent.KEY_DOWN, KeyDown);
			stage.addEventListener(KeyboardEvent.KEY_UP, KeyUp);
		}
		
		private function init(e:Event = null):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);			
			level = (parent as MovieClip).Level;
		}
		
		public function KeyDown(e:KeyboardEvent) {
			switch  (e.keyCode) {
				case Keyboard.RIGHT:
					if (kRight == 0) kRight = 1;
					break;
				case Keyboard.LEFT:
					if (kLeft == 0) kLeft = 1;
					break;
				case Keyboard.UP:
				case Keyboard.SPACE:
					if (kJump == 0) kJump = 1;
					break;
			}
		}
		
		public function KeyUp(e:KeyboardEvent) {
			switch  (e.keyCode) {				
				case Keyboard.RIGHT:
					kRight = 0;
					break;
				case Keyboard.LEFT:
					kLeft = 0;
					break;
				case Keyboard.UP:
				case Keyboard.SPACE:
					kJump = 0;
					break;
			}
		}
		
		function Update(e:Event) {			
			
			var c:Point = new Point(x, y);
			c = (parent as MovieClip).localToGlobal(c);
			var old:Point = c.clone();
			
			//--------Gravity, Friction, and Velocity Calculation
			
			if (onGround) {			
				if (kLeft != 0) 
					velo.x -= RUN_FORCE;				
				if (kRight != 0) 
					velo.x += RUN_FORCE;
				if (kJump == 1)
					velo.y = JUMP_FORCE;
					
				if ((kLeft == 0 && kRight == 0) || (kLeft != 0 && kRight != 0)) {
					velo.x *= GROUND_FRICTION;	
					if (Math.abs(velo.x) < 0.5)
						velo.x = 0; // no pixel by pixel creeping.	
				}
			} else {
				velo.y += GRAVITY_FORCE;	

				if (kLeft)
					velo.x -= AIR_CONTROL;
				if (kRight)
					velo.x += AIR_CONTROL;	
			}
			
			
			if (Math.abs(velo.x) > TOP_SPEED) 
				velo.x = TOP_SPEED * (velo.x < 0? -1:1);
			if (Math.abs(velo.y) > MAX_FALLSPEED)
				velo.y = MAX_FALLSPEED;
				
			c.x += velo.x;
			c.y += velo.y;
			
			//--------World Collision Handling
			
			if (velo.x < 0) {
				if (level.hitTestPoint(c.x - (bounds.width / 2 + 1), c.y - bounds.height / 2, true)) {
					c.x = findIntersection(old, c, new Point( - (bounds.width / 2 + 1), - bounds.height / 2)).x;	
					velo.x = 0;
				} else if (level.hitTestPoint(c.x - (bounds.width / 2 + 1), c.y - 2, true)) {
					c.x = findIntersection(old, c, new Point( - (bounds.width / 2 + 1), -2)).x;	
					velo.x = 0;
				}
			}
			
			if (velo.x > 0) {
				if (level.hitTestPoint(c.x + (bounds.width / 2 + 1), c.y - bounds.height / 2, true)) {
					c.x = findIntersection(old, c, new Point( (bounds.width / 2 + 1), - bounds.height / 2)).x;	
					velo.x = 0;
				} else if (level.hitTestPoint(c.x + (bounds.width / 2 + 1), c.y - 2, true)) {
					c.x = findIntersection(old, c, new Point( (bounds.width / 2 + 1), -2)).x;
					velo.x = 0;
				}				
			}

			if (level.hitTestPoint(c.x, c.y - (bounds.height + 1), true))  // Ceiling collisions
				c.y = findIntersection(old, c, new Point(0, - bounds.height)).y;				
						
			if (level.hitTestPoint(c.x - bounds.width / 2 + 2 , c.y+1, true)
				||  level.hitTestPoint(c.x + bounds.width / 2 -2, c.y+1, true)) {
				if (!onGround) {
					if (level.hitTestPoint(c.x - bounds.width / 2 + 2 , c.y , true))
						c.y = findIntersection( old, c, new Point( - bounds.width / 2 + 2, 0)).y;
					else if (level.hitTestPoint(c.x + bounds.width / 2 -2, c.y, true))
						c.y = findIntersection( old, c, new Point( bounds.width / 2 - 2, 0)).y;	
				}
				velo.y = 0;
				onGround = true;
				
			} else {			
				onGround = false;	
			}
			
			
			//--------Animation Handling
			
			if (onGround) {
				if (Math.abs(velo.x) > 1) 
					PlayAnim(1);			
				if (Math.abs(velo.x) <= 1) 
					PlayAnim(0);				
			} else
				PlayAnim(2);
							
			if (velo.x > 0) 	 this.scaleX = -1.0;
			else if (velo.x < 0) this.scaleX = 1.0;
			
			//-------Key held detection..
			if (kLeft == 1) kLeft = 2;
			if (kRight == 1) kRight = 2;
			if (kJump == 1) kJump = 2;
			
			c = (parent as MovieClip).globalToLocal(c);
			x = c.x;
			y = c.y;
		}
		
		// Uses a bisection method to find the edge of an obstacle (tests the midpoint, and then goes closer or further
		// based on if it is solid at the midpoint)
		private function findIntersection( start:Point, end:Point, offset:Point ):Point {
			var a:Point = new Point(start.x + offset.x, start.y + offset.y);
			var b:Point = new Point();
			var c:Point = new Point(end.x + offset.x, end.y + offset.y);
			for (var i:int = 0; i < MAX_ITERATIONS; i++) {
				b.x = (a.x + c.x) / 2.0;
				b.y = (a.y + c.y) / 2.0;
				if (level.hitTestPoint(b.x, b.y, true)) {
					c = b.clone(); 	// if it was a collision, the edge must be between a and b.
				} else { 		// otherwise, it must be between b and c
					a = b.clone(); 	
					if ((Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)) < Math.pow(0.1, 2)) 
						break;											
				}							
			}
			return new Point(a.x - offset.x, a.y - offset.y);
		}
		
		private function PlayAnim(num:int) {
			if (num == 0 && anim != 0) {
				gotoAndPlay("Idle");
				anim = 0;
			} else if (num == 1 && anim != 1) {
				gotoAndPlay("RunStart");
				anim = 1;
			}
			else if (num == 2 && anim != 2) {
				gotoAndPlay("Jump");
				anim = 2;
			}
		}
	}
	
}

By testing to see if the jump key's saved state is 1, which we previously decided would indicate the initial press of a button, we have made it so that the user can't hold the jump button to go hopping along. They will need to press it every time they want to jump. When they do jump, all we do is add their JUMP_FORCE to their y velocity, and the rest of the code we already wrote takes care of it.

This is a flash file.

The last issue we have is that the player can run off the edge of the screen, which makes it very hard to get around the level. We need to make it so that the camera follows the player. Flash doesn't actually have a camera that we can move, but because we put our level and player into a GameWorld symbol, we can just move that instance of the GameWorld relative to the stage to give the appearance of movement. We will be putting the code to achieve this effect in the Document Class, "IMGDBros.as".

package 
{
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	
	public class IMGDBros extends MovieClip 
	{
		public const EDGE_DISTANCE:Number = 200;
		private var cam:Point = new Point(0, 0);
		
		public function IMGDBros():void 
		{
			addEventListener(Event.ENTER_FRAME, Update);
		}
		 
		public function Update(e:Event) {
			var W:MovieClip = (this.World as MovieClip);
			var P1:Player = (W.Play1 as Player);
			
			// Create a smooth camera that will anticipate the player using the player's x velocity
			// (camera focus will be in front of the player, but will lag behind when player is falling)
			var velo:Point = new Point( P1.x + P1.velo.x * 20 - cam.x, P1.y - cam.y);
			cam.x += velo.x * .1;
			cam.y += velo.y * .1;
			
			// Keep camera focus onscreen without letting camera show outside the level area
			var focus:Point = W.localToGlobal( new Point( cam.x, cam.y));
			
			var bounds:Rectangle = new Rectangle(EDGE_DISTANCE, EDGE_DISTANCE,
				stage.stageWidth - 2 * EDGE_DISTANCE, stage.stageHeight - 2 * EDGE_DISTANCE);
				
			if ( focus.x > bounds.right) {
				W.x -= ( focus.x - bounds.right);
				if (W.x < - ( W.Level.width - stage.stageWidth))
					W.x = - (W.Level.width - stage.stageWidth);
			}
			if (focus.x < bounds.left) {
				W.x += bounds.left - focus.x;
				if (W.x > 0)
					W.x = 0;
			}
			if (focus.y < bounds.top) {
				W.y -= focus.y - bounds.top;
				if (W.y > 0) 
					W.y = 0;
			}
			if (focus.y > bounds.bottom) {
				W.y -= focus.y - bounds.bottom;
				if (W.y < - (W.Level.height - stage.stageHeight))
					W.y = - (W.Level.height - stage.stageHeight);
			}		
		}
	}
	
}

You also need to assign a document class to the stage. Make sure your stage is selected and type in "IMGDBros" as the document class (under the "Class:" field).

There are two elements to our camera code. The first part stores the point at which we want the camera to be focusing (line 11 is our camera's coordinates, we modify it in line 25 and 26). We use the player's xvelocity to anticipate the direction the player is moving, and using that information the camera will try to focus on a point in front of the player.

The second part, starting on line 29, makes it so that our camera's focus point can't go off screen, and that the stage view will only move when the focus point gets within a certain distance of the edge of the stage (if the focus point tries to move too close the edge of the screen, the view will move to keep it within the boundary area). There are some conditional statements (lines 36, 51, 46, and 51) which ensure that the stage view also does not move past the edge of the level, because we don't want the player to be able to see past it. Because we use this technique (ather than just using the camera's focus point directly), it is a little bit smoother for the player, especially if they keep switching direction.

Try it out, and see what you think.

This is a flash file.

Congratulations

Here are some ways to expand the complexity of this project:

Congratulations on your final victory. Now, play some more with this framework for a platformer and then go and make your own games!

If you ever encounter a complicated problem you don't think you can solve, break it down and solve the smaller problems one by one. If you do, nothing can stop you.

Back to Top