How to make a 2d Platform Game – part 3 ladders and AI

Lands anim
Submit to StumbleUpon

Hello and welcome back to my blog!

This is part 3 in a series of articles where I talk about how to go about making a 2d platform game like this one:


Click the game to give it focus… Apologies for the programmer art, and my level design (not my best qualities!)

Here is the previous article in case you missed it.

In this particular episode I’m going to talk about the horror of Ladders and the joy of AI.

Ladders

Ok, so ladders are a complete pain in the ass on all fronts! The reason being is that they represent a discontinuity in control mechanism and physics response.

The player must transition from walking along in an environment where they experience gravity and can jump to one where they do not and cannot jump. On top of that, you have several different methods of starting the climb:

  • Walking along the floor and joining the bottom of a ladder
  • Walking along the top of a platform and joining the top of a ladder
  • Landing on the middle of the ladder after falling or doing a jump

The Horror

So why have these distinctions?

Walking along the floor and joining the bottom of a ladder

You don’t want to walk past the bottom of a ladder and automatically start climbing it, so that means you must have a special case for being at the bottom of the ladder and pressing up (which would normally have jumped you).

Walking along the top of a platform and joining the top of a ladder

Again, you don’t want to start climbing down a ladder just by walking over the very top of one, or indeed by landing on the top, so you need a special case for pressing down while being on the top of the ladder. Additionally, this is made even more of a pain because this is the only instance in the game where the player can pass down through a platform (in order to join the ladder).

Landing on the middle of the ladder after falling or doing a jump

I did wrestle with this idea and for a while I had it set so you couldn’t land on the middle of a ladder but after a while of playing I realised this was going to be too fiddly for the player and might lead to a bad play experience. Landing on the middle of the ladder is tricky because it has to act exactly like a platform, but a platform which exists at whatever pixel you land. Also, it represents yet another transition from normal falling to ladder climbing.

Ladder tile types

I have two different tiles which represent a ladder:

The middle

The middle section and:

The top

The top part. The top part is special since it acts like a jump through platform and a ladder, depending on where the player is in relation to it.

Solving the problem

In order to address each of these issues I found I needed to track a fair amount of state and transitions between state.

package Code.Characters
{
	...
 
	public class Player extends Character
	{
		...
 
		private var m_collideLadder:uint;
		private var m_oldCollideLadder:uint;
		private var m_climbLadder:Boolean;
		private var m_onLadderTop:Boolean;
		private var m_disableLadderTopCollision:Boolean;
 
		...
	}
}
private var m_collideLadder:uint;

The variable m_collideLadder is updated every frame and represents the ladder tile type that the player is currently colliding with (i.e. player’s centre point is within the AABB of the tile). It can either be eTileTypes.kLadderMid, eTileTypes.kLadderTop, or eTileTypes.kInvalid when not colliding with a ladder tile.

In addition I have:

private var m_oldCollideLadder:uint;

This tracks the last state of m_collideLadder, so whenvever m_collideLadder changes, the previous state is stored in m_oldCollideLadder. This allows me to detect transitions between colliding with the ladder and not.

In particular the case where the player lands on the middle of a ladder by falling or jumping looks like this:

package Code.Characters
{
	...
 
	public class Player extends Character
	{
		...
 
		/// <summary>
		/// Special implementation for the player
		/// </summary>
		protected override function PostCollisionCode( ):void
		{
			super.PostCollisionCode( );
 
			// if we're not colliding with the ladder or we're on the ground, we're not climbing the ladder
			if ( !m_collideLadder || m_OnGround )
			{
				m_climbLadder = false;
			}
 
			// if we fall onto the top of a ladder, we start climbing it
			if ( m_oldCollideLadder==eTileTypes.kEmpty && Map.IsTileLadder(m_collideLadder) && !m_OnGround )
			{
				m_climbLadder = true;
				m_animationController.PlayOnce( kLandAnim );
			}
 
			if ( !m_OnGround&&m_OnGroundLast && m_vel.m_y > 0 )
			{
				m_animationController.PlayOnce( kFallAnim );
			}
		}
		...
	}
}

You should be able to see that part where this transition is picked up, and the kLandAnim animation gets played.

private var m_climbLadder:Boolean;

This is set when the player is confirmed as climbing the ladder; in this event a special case control mechanism will take over from the normal gravity/friction behaviour which actually enables the climbing itself.

private var m_onLadderTop:Boolean;

This is set by the collision system when the player is confirmed as being on the top of a ladder – this has a dual purpose; it lets me have a special case so I can apply the same collision logic as I would do when standing on a jump-through platform and it also allows me to detect the down arrow in the player input control mechanism as an indication that the player wants to join the ladder at the top.

private var m_disableLadderTopCollision:Boolean;

This is set when joining the ladder at the top and allows the collision system to ignore the ladder top tile until the player is confirmed as having his centre point in the AABB of any ladder tile. This causes the player to fall briefly down until they start colliding with the middle of the ladder tile, where collision is then re-enabled.

Special ladder physics

I covered the regular collision response code in the last article (the part where friction is applied etc). When m_climbLadder is set, this code is ignored and some new code takes over.

The player has a special variable called m_velTarget which is 0,0 by default and only comes into use when we’re confirmed as being on a ladder. This gets set to the maximum player speed in the X or Y axis inside the player input system when the player is moving left/right or up/down and is on a ladder.

The main body of the code tries to alter the player’s actual velocity (which is Player.m_vel) towards this new target velocity, and to prevent the player from arriving at the target velocity too quickly there is a special Player.kLadderStayStrength constant. This prevents the ladder movement from contrasting with the player’s movement in the rest of the game.

The full code looks like this:

package Code.Characters
{
	...
 
	public class Player extends Character
	{
		/// <summary>
		/// Special case code for the player colliding with different tile types
		/// </summary>	
		protected override function InnerCollide(tileAabb:AABB, tileType:int, dt:Number, i:int, j:int ):void
		{
			...
 
			else if ( Map.IsTileLadder( tileType ) )
			{
				//
				// Are we colliding with the top of a ladder?
				//
 
				var checkLadderMiddle:Boolean = false;
 
				if ( Map.IsTileLadderTop( tileType ) && !m_disableLadderTopCollision )
				{
					m_onLadderTop = Collide.AabbVsAabbTopPlane( this, tileAabb, m_contact, i, j, m_map );
 
					if ( m_onLadderTop )
					{
						CollisionResponse( m_contact.m_normal, m_contact.m_dist, dt );
					}
					else 
					{
						checkLadderMiddle = true;
					}
				}
				else 
				{
					checkLadderMiddle = true;
				}
 
				if (checkLadderMiddle)
				{
					//
					// check to see if we're colliding with the middle of a ladder
					//
 
					if (Collide.PointInAabb(m_Pos, tileAabb))
					{
						m_collideLadder = tileType;
						m_disableLadderTopCollision = false;
 
						if ( m_climbLadder )
						{
							// remove a portion of the total velocity of the character
							var delta:Vector2 = m_velTarget.Sub( m_vel );
 
							var len:Number = delta.m_Len;
							var change:Vector2 = delta;
							if ( len>kLadderStayStrength )
							{
								// limit the amount we can remove velocity by
								change.MulScalarTo( kLadderStayStrength/len );
							}
 
							m_vel.AddTo( change );
						}
					}
				}
			}
 
			...
		}
	}
}

The relevant part is at the bottom.

What’s going on in the above code? Well, once we’re inside the condition:

if ( m_climbLadder )
{

then we’re actually doing the ladder physics. First of all we work out the difference between our current velocity and the desired velocity:

var delta:Vector2 = m_velTarget.Sub( m_vel );

Then, if the length of this vector is longer than our ladder stay strength amount (i.e. the movement is too much to do in one frame), we have to clamp the movement so its just as much as we’re allowed to move by the constant kLadderStayStrength:

var change:Vector2 = delta;
if ( len>kLadderStayStrength )
{
	// limit the amount we can remove velocity by
	change.MulScalarTo( kLadderStayStrength/len );
}

Once this has been clamped we add this delta to our velocity vector and we’re done:

m_vel.AddTo( change );

Once the player stops colliding with a ladder tile, the normal collision resolution code takes over again and we’re done with ladders completely!

The joy of AI

I really enjoyed working on the AI for this demo because it went so amazingly smoothly, nearly everything I wanted to try worked first time. It was refreshing after all the messing around with the ladder stuff; you learn to enjoy these small victories after you’ve been programming for a while because they’re so rare :)

Lets have another look at the class hierarchy I showed in the last article:

AI Hierarchy

The complete AI hierarchy is shown as the yellow branch which inherits from Character.

Enemy, the base class

Ok, so lets have a look at the base class which all enemies derive from:

package Code.Characters
{
	import flash.display.*;
	import Code.Maths.*;
	import Code.*;
	import Code.Geometry.*;
	import Code.Graphics.AnimationController;
	import Code.System.*;
	import Code.Level.*;
 
	public class Enemy extends Character
	{
		// animation names
		private const kHitAnim:String = "hitAnim";
 
		/// <summary>
		/// Simple constructor
		/// </summary>
		public function Enemy( )
		{
			// aways go to the left by default;
			m_vel.m_x = -m_WalkSpeed;
 
			m_animationController.PlayLooping( kWalkAnim );
			m_animationController.GotoRandomFrameInCurrentAnim( );
		}
 
		/// <summary>
		/// Simple update function
		/// </summary>	
		public override function Update( dt:Number ):void
		{
			if ( m_hurtTimer > 0 )
			{
				m_hurtTimer--;
			}
 
			super.Update( dt );
		}
 
		/// <summary>
		/// Hurt this enemy, player was at hurtPos
		/// </summary>	
		public override function Hurt( hurtPos:Vector2 ):void
		{
			if ( m_hurtTimer==0 )
			{
				m_hurtTimer = kEnemyHurtFrames;
 
				m_animationController.PlayOnce( kHitAnim );
				m_animationController.SetEndOfAnimCallback( DeleteEnemy );
			}
		}
 
		/// <summary>
		/// Animation callback used to delete this enemy and spawn a pick-up
		/// </summary>
		private function DeleteEnemy( animName:String ):void
		{
			// mark this enemy as dead, it will be deleted
			m_dead = true;
 
			// spawn a pickup
			m_platformer.SpawnMo( DiamondPickupFla, m_pos.Clone( ), true );
		}
 
		/// <summary>
		/// What speed should this guy walk at?
		/// </summary>	
		protected function get m_WalkSpeed( ):Number
		{
			throw new NotImplementedException;
		}
 
		/// <summary>
		/// Does he kill on touch?
		/// </summary>
		public function get m_KillsOnTouch( ):Boolean
		{
			throw new NotImplementedException;
		}
	}
}

It requires any concrete implementations to define a couple of attributes: m_WalkSpeed and m_KillsOnTouch. m_WalkSpeed is used in the constructor and as the name suggests is the speed at which the enemy walks. m_KillsOnTouch is used in the collision detection system and defines whether this particular enemy will kill the player on touch.

All enemies are required to provide a couple of animations with specific names which are referenced in this base class (or in Character from which this derives):

Star walk anim

  • ‘walkAnim’ – the walk animation this enemy has
  • ‘hitAnim’ – the animation to play when this enemy is punched by the player

The individual enemy types may well provide additional animations, but the above is the bare minimum.

The other function worth mentioning is Hurt() which is actually defined in Character. This is overridden here with a custom implementation which counts down a few frames, then triggers the ‘hitAnim’ and attaches a callback which will happen when the animation is done playing; this marks the enemy for deletion and also spawns a pick-up where the enemy was at the time.

Simple enemy

This is another shared, non-concrete class which encompasses the behaviour of two of the enemy types.

Simple enemies

package Code.Characters
{
	import flash.display.*;
	import Code.Maths.*;
	import Code.*;
	import Code.Geometry.*;
	import Code.Graphics.AnimationController;
	import Code.System.*;
	import Code.Physics.*;
 
	public class SimpleEnemy extends Enemy
	{
		/// <summary>
		/// 
		/// </summary>
		public function SimpleEnemy()
		{
			super();
		}
 
		/// <summary>
		/// Simple update function which just checks for kReverseDirection markers
		/// </summary>	
		public override function Update( dt:Number ):void
		{
			if ( m_hurtTimer == 0 )
			{
				// form AABB for character
				var min:Vector2 = m_pos.Sub( m_halfExtents );
				var max:Vector2 = m_pos.Add( m_halfExtents );
 
				m_map.DoActionToTilesWithinAabb( min, max, InnerCollide, dt );
			}
 
			super.Update( dt );
		}
 
		/// <summary>
		/// Is the current tile a reverse direction maker? If so, change direction
		/// </summary>	
		protected override function InnerCollide( tileAabb:AABB, tileType:int, dt:Number, i:int, j:int ):void
		{
			if ( tileType==eTileTypes.kReverseDirection && MoveableObject.HeadingTowards(tileAabb.m_Centre, this) )
			{
				// toggle direction
				m_vel.m_x *= -1;
				this.scaleX *= -1;
			}
		}
 
		/// <summary>
		/// This enemy type is always updated, this is to stop them from bunching up 
		/// due to the off-screen deactivation code
		/// </summary>
		public override function get m_ForceUpdate( ):Boolean
		{
			return true;
		}
	}
}

Their basic behaviour is to head in a constant direction until they encounter a special marker type which indicates they should reverse direction. This marker type is always mapped in the middle collision layer in the map along with all other AI stuff.

Change direction marker

The marker itself gets a special tile (shown above) so the mapper (i.e me) can see where I’ve placed it in the map. At runtime, these tiles are turned invisible, or rather they don’t actually have any visual data exported with them.

AI markers as seen in Mappy

The above shows them in a level in Mappy.

The relevant code is for this behaviour is:

/// <summary>
/// Simple update function which just checks for kReverseDirection markers
/// </summary>	
public override function Update( dt:Number ):void
{
	if ( m_hurtTimer == 0 )
	{
		// form AABB for character
		var min:Vector2 = m_pos.Sub( m_halfExtents );
		var max:Vector2 = m_pos.Add( m_halfExtents );
 
		m_map.DoActionToTilesWithinAabb( min, max, InnerCollide, dt );
	}
 
	super.Update( dt );
}

…which forms an AABB for the extents of the character and then calls out to the collision system to return all tiles which intersect with this AABB. The collision system then calls back to my callback function InnerCollide:

/// <summary>
/// Is the current tile a reverse direction maker? If so, change direction
/// </summary>	
protected override function InnerCollide( tileAabb:AABB, tileType:int, dt:Number, i:int, j:int ):void
{
	if ( tileType==eTileTypes.kReverseDirection && MoveableObject.HeadingTowards(tileAabb.m_Centre, this) )
	{
		// toggle direction
		m_vel.m_x *= -1;
		this.scaleX *= -1;
	}
}

…this checks the tile type being collided with is of type eTileTypes.kReverseDirection, if so and if the enemy is heading towards this tile then we toggle the enemy’s direction and also have him face the opposite way. That last condition (which checks the enemy’s direction) is very important, otherwise it could be that the enemy is still going to be colliding with the same change direction marker next frame; this would lead to a very confused enemy who would toggle direction constantly.

For reference, here is the utility function to work out if an object is heading towards a point (in the X axis):

/// <summary>
/// Is the given candidate heading towards towardsPoint? 
/// </summary>
static public function HeadingTowards( towardsPoint:Vector2, candidate:MoveableObject ):Boolean
{
	var deltaX:Number = towardsPoint.m_x-candidate.m_Pos.m_x;
	var headingTowards:Boolean = deltaX*candidate.m_Vel.m_x>0;
 
	return headingTowards;
}

I’ve found this useful in a couple of places.

Star

This is where we really start to feel the benefit of all this abstraction.

Star

Here is the complete implementation of this enemy:

package Code.Characters
{
	public class Star extends SimpleEnemy
	{
		private const kWalkSpeed:Number = 40;
		private const kWalkAnimMultiplier:Number = 0.125;
 
		/// <summary>
		/// 
		/// </summary>
		public function Star()
		{
			super();
		}
 
		/// <summary>
		/// Simple accessor
		/// </summary>	
		protected override function get m_WalkSpeed( ):Number
		{
			return kWalkSpeed;
		}
 
		/// <summary>
		/// Simple accessor
		/// </summary>
		protected override function get m_AnimSpeedMultiplier( ):Number
		{
			return kWalkAnimMultiplier;
		}
 
		/// <summary>
		/// Kills on touch
		/// </summary>
		public override function get m_KillsOnTouch( ):Boolean
		{
			return true;
		}
	}
}

As you can see, he is nothing more than three simple accessors which don’t even require an explanation! All the functionality is performed by the classes he derives from.

Ladybird

This one is a little more complex because he has a special behaviour where he pauses, plays an anim, emits another object, player another anim and then resumes his original motion.

Ladybird

Side note: I have no idea why we call this creature a ‘ladybird’ in the uk – logic dictates that ‘ladybug’ (the american name) is surely more fitting!

package Code.Characters
{
	import Code.Maths.*;
	import Code.Physics.*;
 
	public class LadyBird extends SimpleEnemy
	{
		// animation names
		private const kFireInAnim:String = "fireInAnim";
		private const kFireOutAnim:String = "fireOutAnim";
 
		private const kWalkSpeed:Number = 40;
		private const kWalkAnimMultiplier:Number = 1;
		private const kFireSpikesDelay:int = 60;
		private const kTriggerTimerRadius:Number = 200;
 
		private var m_fireSpikesCounter:int;
		private var m_originalXVel:Number;
 
		/// <summary>
		/// 
		/// </summary>
		public function LadyBird()
		{
			super();
 
			m_fireSpikesCounter = kFireSpikesDelay;
		}
 
		/// <summary>
		/// 
		/// </summary>	
		public override function Update( dt:Number ):void
		{
			super.Update( dt );
 
			var withinRadius:Boolean = m_platformer.m_Player.m_Pos.Sub( m_pos ).m_Len<kTriggerTimerRadius;
			var facingPlayer:Boolean = MoveableObject.HeadingTowards( m_platformer.m_Player.m_Pos, this );
 
			if (withinRadius && facingPlayer && m_hurtTimer==0 )
			{
				// every so often, we spawn some spikes
				if ( m_fireSpikesCounter>0 )
				{
					m_fireSpikesCounter--;
				}
				else if (m_animationController.m_Playing == kWalkAnim)
				{
					// begin a series of callbacks which eventually plays the spawn anim
					m_animationController.StopAtEnd( );
					m_animationController.SetEndOfAnimCallback( WaitTillEndOfAnim );
				}
			}
		}
 
		/// <summary>
		/// 
		/// </summary>
		private function WaitTillEndOfAnim( animName:String ):void
		{
			m_animationController.PlayOnce( kFireInAnim );
			m_animationController.SetEndOfAnimCallback( SpawnSpikes );
 
			// pause motion
			m_originalXVel = m_vel.m_x;
 
			if ( m_hurtTimer==0 )
			{
				m_vel.m_x = 0;
			}
		}
 
		/// <summary>
		/// 
		/// </summary>
		private function SpawnSpikes( animName:String ):void
		{
			var velX:Number = -this.scaleX*kWalkSpeed*2;
 
			var spikes:Character = Character(m_platformer.SpawnMo( LadySpikesFla, m_pos.Clone(), true, velX ));
 
			// put behind us in display list order
			m_platformer.setChildIndex( spikes, m_platformer.getChildIndex( this )-1 );
 
			m_animationController.PlayOnce( kFireOutAnim );
			m_animationController.SetEndOfAnimCallback( ResumeNormalAnim );
		}
 
		/// <summary>
		/// 
		/// </summary>
		private function ResumeNormalAnim( animName:String ):void
		{
			m_animationController.PlayLooping( kWalkAnim );
 
			// reset counter
			m_fireSpikesCounter = kFireSpikesDelay;
 
			// resume motion
			if ( m_hurtTimer==0 )
			{
				m_vel.m_x = m_originalXVel;
			}
		}
 
		/// <summary>
		/// 
		/// </summary>	
		protected override function get m_WalkSpeed( ):Number
		{
			return kWalkSpeed;
		}
 
		/// <summary>
		/// 
		/// </summary>
		protected override function get m_AnimSpeedMultiplier( ):Number
		{
			return kWalkAnimMultiplier;
		}
 
		/// <summary>
		/// 
		/// </summary>
		public override function get m_KillsOnTouch( ):Boolean
		{
			return false;
		}
	}
}

Lets have a look at the main update loop:

/// <summary>
/// 
/// </summary>	
public override function Update( dt:Number ):void
{
	super.Update( dt );
 
	var withinRadius:Boolean = m_platformer.m_Player.m_Pos.Sub( m_pos ).m_Len<kTriggerTimerRadius;
	var facingPlayer:Boolean = MoveableObject.HeadingTowards( m_platformer.m_Player.m_Pos, this );
 
	if (withinRadius && facingPlayer && m_hurtTimer==0 )
	{
		// every so often, we spawn some spikes
		if ( m_fireSpikesCounter>0 )
		{
			m_fireSpikesCounter--;
		}
		else if (m_animationController.m_Playing == kWalkAnim)
		{
			// begin a series of callbacks which eventually plays the spawn anim
			m_animationController.StopAtEnd( );
			m_animationController.SetEndOfAnimCallback( WaitTillEndOfAnim );
		}
	}
}

So, what’s going in here? Firstly we do some simple maths to determine whether the player is within some radius of the enemy. We also work out if the enemy is facing the player (after all, no point trying to attack the player if he’s behind you). If both of these conditions are true we decrement a counter, once this counter gets to 0, we trigger a sequence of animations via callbacks:

The first simply waits until the end of the current anim – this is to make sure the animation frames match up; since I designed the anims to follow on from each other.

/// <summary>
/// 
/// </summary>
private function WaitTillEndOfAnim( animName:String ):void
{
	m_animationController.PlayOnce( kFireInAnim );
	m_animationController.SetEndOfAnimCallback( SpawnSpikes );
 
	// pause motion
	m_originalXVel = m_vel.m_x;
 
	if ( m_hurtTimer==0 )
	{
		m_vel.m_x = 0;
	}
}

When the above callback is called, we trigger the actual animation for spawning the spikes. This is done so the player knows to prepare themselves to get out of the way. I also pause the motion of the creature at this point, taking a note of what its original motion was so I can restore it afterwards.

/// <summary>
/// 
/// </summary>
private function SpawnSpikes( animName:String ):void
{
	var velX:Number = -this.scaleX*kWalkSpeed*2;
 
	var spikes:Character = Character(m_platformer.SpawnMo( LadySpikesFla, m_pos.Clone(), true, velX ));
 
	// put behind us in display list order
	m_platformer.setChildIndex( spikes, m_platformer.getChildIndex( this )-1 );
 
	m_animationController.PlayOnce( kFireOutAnim );
	m_animationController.SetEndOfAnimCallback( ResumeNormalAnim );
}

Spawned spikes

Once the spawn spikes animation has finished, I actually spawn a new object, which is another very simple class which obeys gravity and does full collision but also kills on touch. I make sure it heads in the same direction that the original enemy is facing, make sure it appears behind this enemy and lastly play the fired animation which sets up the synchronisation to match with the frame the normal animation ended on.

/// <summary>
/// 
/// </summary>
private function ResumeNormalAnim( animName:String ):void
{
	m_animationController.PlayLooping( kWalkAnim );
 
	// reset counter
	m_fireSpikesCounter = kFireSpikesDelay;
 
	// resume motion
	if ( m_hurtTimer==0 )
	{
        	m_vel.m_x = m_originalXVel;
	}
}

Once the previous animation has finished, the firing action is complete, so we resume playing the regular walk animation, reset the counter and restore the original motion of the enemy.

The brain

This enemy type has a pretty simple behaviour as well: it kills on touch and it will move for a while, then pause for a while and then carry on. When it starts to move it heads towards wherever the player was at the time.

The brain

package Code.Characters
{
	import Code.Maths.*;
 
	public class Brain extends Enemy
	{
		private const kWalkSpeed:Number = 80;
 
		private const kThinkSeconds:Number = 2;
		private const kMoveSeconds:Number = 1;
 
		private var m_thinkTimer:Number;
		private var m_moveTimer:Number;
		private var m_think:Boolean;
 
		public function Brain( )
		{
			m_thinkTimer = Scalar.RandBetween( 0, kThinkSeconds );
			m_think = true;
		}
 
		/// >summary<
		/// Pause, then target the player and move for a bit, repeat
		/// >/summary<	
		public override function Update( dt:Number ):void
		{
			super.Update( dt );
 
			if (!IsHurt())
			{
				if ( m_think )
				{
					// have we gone past our think timer limit?
					if ( m_thinkTimer>0 )
					{
						// toggle modes
						m_think = false;
 
						// work out a new target velocity
						m_vel = m_platformer.m_Player.m_Pos.Sub( m_pos ).UnitTo( ).MulScalarTo( kWalkSpeed );
 
						// reset this timer
						m_moveTimer = kMoveSeconds;
					}
 
					m_thinkTimer -= dt;
				}
				else 
				{
					// have we gone past our move timer limit?
					if ( m_moveTimer>0 )
					{
						// toggle modes
						m_think = true;
 
						// stop moving
						m_vel.Clear( );
 
						// reset this timer
						m_thinkTimer = kThinkSeconds;
					}
 
					m_moveTimer -= dt;
				}
			}
		}
 
		/// >summary<
		/// Simple accessor
		/// >/summary<	
		protected override function get m_WalkSpeed( ):Number
		{
			return kWalkSpeed;
		}
 
		/// >summary<
		/// Simple accessor
		/// >/summary<
		public override function get m_KillsOnTouch( ):Boolean
		{
			return true;
		}
	}
}

All the magic is within the Update function. As you can see there are a couple of simple timers which tick down depending on what mode the enemy is currently in (either m_think==true or not). When the think timer becomes less than 0, the enemy toggles modes and picks a direction to move.

// work out a new target velocity
m_vel = m_platformer.m_Player.m_Pos.Sub( m_pos ).UnitTo( ).MulScalarTo( kWalkSpeed );

This new direction takes the form of a velocity. This velocity is based on the vector from the enemy to the player, which is then made unit length and scaled by the kWalkSpeed of the enemy. This will cause the enemy to move in the direction of the player at a constant speed. Once he gets there he kills on touch.

The skeleton

This is the final AI character and has the most complex behaviour of them all.

Skeleton

Its all driven via the animations though, which makes it rather flexible.

package Code.Characters
{
	import Code.Maths.Vector2;
 
	public class Skeleton extends Enemy
	{
		// animation names
		private const kJumpAnim:String = "jumpAnim";
		private const kDeadlyAnim:String = "deadlyAnim";
 
		private const kJumpStrength:Number = 800;
 
		private var m_jumping:Boolean;
 
		public function Skeleton( )
		{
			super( );
 
			ResetJumpSequence(null);
		}
 
		/// >summary<
		/// Stand still, play walk anim
		/// >/summary<
		private function ResetJumpSequence( animName:String  ):void
		{
			// stand still
			m_vel.m_x = 0;
			m_jumping = false;
 
			m_animationController.PlayOnce( kWalkAnim );
			m_animationController.SetEndOfAnimCallback( PlayJumpAnim );
		}
 
		/// >summary<
		/// Play the jump anim
		/// >/summary<	
		private function PlayJumpAnim( animName:String ):void
		{
			m_animationController.PlayOnce( kJumpAnim );
			m_animationController.SetEndOfAnimCallback( JumpTowardPlayer );
		}
 
		/// >summary<
		/// Actually jump towards the player
		/// >/summary<	
		private function JumpTowardPlayer( animName:String ):void
		{
			if ( !IsHurt( ) )
			{
				// work out a new target velocity
				var targetPos:Vector2 = m_platformer.m_Player.m_Pos.Add( Vector2.RandomRadius( 100 ) );
				var playerUnitDirection:Vector2 = targetPos.SubFrom( m_pos ).UnitTo( );
 
				// must jump upwards!
				playerUnitDirection.m_y = -2;
 
				// re-normalise
				playerUnitDirection.UnitTo( );
 
				// jump towards player
				m_vel.AddTo( playerUnitDirection.MulScalarTo( kJumpStrength ) );
 
				m_jumping = true;
 
				m_animationController.SetEndOfAnimCallback( null );
			}
		}
 
		/// >summary<
		/// Check for landing, play the deadly animation when landed
		/// >/summary<	
		public override function Update( dt:Number ):void
		{
			super.Update( dt );
 
			if ( m_jumping && !IsHurt() )
			{
				if ( m_OnGround&&!m_OnGroundLast )
				{
					// landed!
					m_animationController.PlayOnce( kDeadlyAnim );
					m_animationController.SetEndOfAnimCallback( ResetJumpSequence );
				}
			}
		}
 
		/// >summary<
		/// Apply collision detection only when not hurt
		/// >/summary<
		public override function get m_HasWorldCollision( ):Boolean
		{
			return !IsHurt();
		}
 
		/// >summary<
		/// Apply gravity only when not hurt
		/// >/summary<
		protected override function get m_ApplyGravity( ):Boolean
		{
			return !IsHurt();
		}
 
		/// >summary<
		/// Apply friction only when not hurt
		/// >/summary<
		protected override function get m_ApplyFriction( ):Boolean
		{
			return !IsHurt();
		}
 
		/// >summary<
		/// Doesn't walk
		/// >/summary<	
		protected override function get m_WalkSpeed( ):Number
		{
			return 0;
		}
 
		/// >summary<
		/// Force update when jumping so he's not left in mid air when going off screen
		/// >/summary<
		public override function get m_ForceUpdate( ):Boolean
		{
			return super.m_ForceUpdate || m_jumping;
		}
 
		/// >summary<
		/// Only kills on touch when the deadly anim is playing
		/// >/summary<
		public override function get m_KillsOnTouch( ):Boolean
		{
			return m_animationController.m_Playing==kDeadlyAnim;
		}
	}
}

Like in the case of the LadyBird there are a number of animations which all lead on from each other when played in sequence:

Walk anim

(His eyes just move left to right)

Prepare and then jump anim

(Prepares to and then finally jumps – to give the player fair warning)

Lands anim

(Lands and deadly spikes protrude – he will kill on touch at this point)

The only part of the code which needs a special mention is where the skeleton is choosing which direction to jump in:

/// >summary<
/// Actually jump towards the player
/// >/summary<	
private function JumpTowardPlayer( animName:String ):void
{
	if ( !IsHurt( ) )
	{
		// work out a new target velocity
		var targetPos:Vector2 = m_platformer.m_Player.m_Pos.Add( Vector2.RandomRadius( 100 ) );
		var playerUnitDirection:Vector2 = targetPos.SubFrom( m_pos ).UnitTo( );
 
		// must jump upwards!
		playerUnitDirection.m_y = -2;
 
		// re-normalise
		playerUnitDirection.UnitTo( );
 
		// jump towards player
		m_vel.AddTo( playerUnitDirection.MulScalarTo( kJumpStrength ) );
 
		m_jumping = true;
 
		m_animationController.SetEndOfAnimCallback( null );
	}
}

The code picks a random position around the player:

// work out a new target velocity
var targetPos:Vector2 = m_platformer.m_Player.m_Pos.Add( Vector2.RandomRadius( 100 ) );

This was done to stop multiple skeletons from landing in the exact same spot too much, which looked unnatural. Then, this target position is turned into a unit length direction vector, which points from the skeleton towards the player:

var playerUnitDirection:Vector2 = targetPos.SubFrom( m_pos ).UnitTo( );

But of course when jumping, one must always jump upwards, so I simply set the y axis of the vector to be -2. This then breaks the unit length constraint, so I have to renormalise again:

// must jump upwards!
playerUnitDirection.m_y = -2;
 
// re-normalise
playerUnitDirection.UnitTo( );

The reason it was -2 and not -1 or some other number is that this forces the final vector to be mostly pointing upwards with just a little side to side motion. If I’d chosen -1, it would have been a more even spread.

You can use normalisation like this to create different spread patterns, whereby a random unit vector is added to a known fixed direction vector, then renormalised. I’ve used this technique in the past to create an emission spread for a particle system which needed to emit particles in a cone formation; it was random point in a unit sphere plus some predefined forward direction (scaled as appropriate) and then renormalised.

Then I actually perform the jump by setting the skeleton’s velocity and clear the animation callback to reset the sequence:

// jump towards player
m_vel.AddTo( playerUnitDirection.MulScalarTo( kJumpStrength ) );
 
m_jumping = true;
 
m_animationController.SetEndOfAnimCallback( null );

And that’s it!

The motley crew

That concludes my series on how to make a 2d platformer, I hope you’ve enjoyed reading it!

As ever, if you want, you can buy the source-code for the entire game (or even try a version for free), including all the assets and levels you see above. It will require Adobe Flash CS4+, the Adobe Flex Compiler 4.0+ and either Amethyst, or Flash Develop to get it to build. And you’ll want Mappy or some alternative in order to create your own levels!

Following on from feedback from the Angry Birds article, I’ve included a Flash Develop project as well as an Amethyst project inside the .zip file, to help you get started more quickly, no matter which development environment you have.

You are free to use it for whatever purposes you see fit, even for commercial games or in multiple projects, the only thing I ask is that you don’t spread/redistribute the source-code around. Please note that you will need some programming and design skills in order to make the best use of this!

Go to the source-code option page to choose the version you’d like – from completely free to the full version!

Subscribers can access the source here

Until next time, Have fun!

Cheers, Paul.

Submit to StumbleUpon

About Paul Firth

A games industry veteran of ten years, seven of which spent at Sony Computer Entertainment Europe, he has had key technical roles on triple-A titles like the Bafta Award Winning Little Big Planet (PSP), 24: The Game (PS2), special effects work on Heavenly Sword (PS3), some in-show graphics on the BBC’s version of Robot Wars, the TV show, as well as a few more obscure projects.   Now joint CEO of Wildbunny, he is able to give himself hiccups simply by coughing.   1NobNQ88UoYePFi5QbibuRJP3TtLhh65Jp
This entry was posted in AS3, Ladders and AI, Platform game, Technical and tagged , , , , , , . Bookmark the permalink.

14 Responses to How to make a 2d Platform Game – part 3 ladders and AI

  1. Chuck says:

    Awesome write-up, man. And it comes at a time where I was *really* wanting to make a game of some sort too.

  2. Anonymous says:

    I’m glad you are updating this blog again. Your writeups are thorough and enjoyably detailed.

  3. Pingback: How to make a 2d platform game – part 2 collision detection | Paul's blog@Wildbunny

  4. Ladders says:

    Fair play to you the game playes great. It reminds me a lot of Tresure Island Dizzy.

  5. Pingback: How to make games | Paul's blog@Wildbunny

  6. Abbas says:

    hi paul,
    i kinda completed my game, but i couldn’t manage to understand how the 2 background images work (furthest one is drawn with respect to the map, and its fixed) but the mid-ground image is definitely not the same.

    here is the game link:
    http://www.vistamoda.com/index.php/have-fun/54-grant-the-warrior-ant

    (i posted the link earlier but the game then was the infrastructure)

  7. Leyla says:

    I’m so lost. I don’t get where these codes are supposed to go when pasted into layers or objects. Which codes get the AI scripts? Where exactly does all that hit detection script go? A particular layer, perhaps? If so, which one? I keep reading and re-reading, but I’m just not seeing it. I understand the dynamics of what the codes are meant to accomplish, even when reading it from the script, but I don’t get where they are placed. Please, help…

  8. Hahaha wow this is a funny game. The visual effects are really great and the way that character punches is hilarious.

  9. mark says:

    Hi I was wondering how do you make the door to the next level. I cant get my “doors” to unload the current level and load a new one.

    • Paul Firth says:

      Hi Mark,

      Are you using my source or is this your own stuff?

      In my stuff I have a special tile which represents a door. When its collided with I set a timer and when that expires I just load the new map, with the same code which loaded the original map.

      Cheers, Paul.

  10. Josh Smithers says:

    Excellent article thanks! If anybody’s interested in developing flash games I would also recommend reading http://www.scratchpad-cloud.com/blogs.htm as it talks through how Flash games enemy artificial intelligence works. Really helpful like this article.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

WP-SpamFree by Pole Position Marketing