Article: I Can Has Platformer? (Part 2)
by Casey Young
Welcome back to the second part of my series on how to create a simple platformer game. In this article I take you on a little journey in adding our hero to the game, and have him interact with the platforms and blocks that make up the level.
Please refer to my first article to grab the final project before continuing this part, you will need that code to edit along with to make things work. =D
Now to get things started here, I want to introduce to you our hero of our ScribblePlatformer.

Sure he isn't much, but don't let him hear you say that, he has feelings too you know. Save our hero and put him into the folder Sprites/Player in the Content project of our ScribblePlatformer game.
Now that we have our player added to our project, let's get started with the code. First thing we want to do is to add a new class file to our project and name it "RectangleExtensions." This will house an function that extends the Rectangle object, and will be used for when we do collisoin with the tiles/platforms.
using System;
using Microsoft.Xna.Framework;
namespace ScribblePlatformer
{
/// <summary>
/// Helpful method for working with rectangles.
/// </summary>
public static class RectangleExtensions
{
/// <summary>
/// Calculates the signed depth of intersection between two rectangles.
/// </summary>
/// <returns>
/// The amount of overlap between two intersecting rectangles. These
/// depth values can be negative depending on which wides the rectangles
/// intersect. This allows callers to determine the correct direction
/// to push objects in order to resolve collisions.
/// If the rectangles are not intersecting, Vector2.Zero is returned.
/// </returns>
public static Vector2 GetIntersectionDepth(this Rectangle _rectA, Rectangle _rectB)
{
// Calculate half sizes.
float halfWidthA = _rectA.Width / 2.0f;
float halfHeightA = _rectA.Height / 2.0f;
float halfWidthB = _rectB.Width / 2.0f;
float halfHeightB = _rectB.Height / 2.0f;
// Calculate centers.
Vector2 centerA = new Vector2(_rectA.Left + halfWidthA, _rectA.Top + halfHeightA);
Vector2 centerB = new Vector2(_rectB.Left + halfWidthB, _rectB.Top + halfHeightB);
// Calculate current and minimum-non-intersecting distances between centers.
float distanceX = centerA.X - centerB.X;
float distanceY = centerA.Y - centerB.Y;
float minDistanceX = halfWidthA + halfWidthB;
float minDistanceY = halfHeightA + halfHeightB;
// If we are not intersecting at all, return (0, 0).
if (Math.Abs(distanceX) >= minDistanceX || Math.Abs(distanceY) >= minDistanceY)
return Vector2.Zero;
// Calculate and return intersection depths.
float depthX = distanceX > 0 ? minDistanceX - distanceX : -minDistanceX - distanceX;
float depthY = distanceY > 0 ? minDistanceY - distanceY : -minDistanceY - distanceY;
return new Vector2(depthX, depthY);
}
}
}
Alright, now let's dive into the class that will handle our player object. Let's create a new class file, and name it "Player". Inside this newly created class, let's run down the variables that will be used inside this class.
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace ScribblePlatformer
{
class Player
{
// This holds our texture to our player.
private Texture2D playerSprite;
/// <summary>
/// What level are we on, used for collision and interaction
/// with the level's entities.
/// </summary>
public Level Level
{
get { return level; }
}
Level level;
/// <summary>
/// Is the player alive, or dead?
/// </summary>
public bool IsAlive
{
get { return isAlive; }
}
bool isAlive;
/// <summary>
/// What position the player is in the world
/// </summary>
public Vector2 Position
{
get { return position; }
set { position = value; }
}
Vector2 position;
// what was the last tile we went thru
private float previousBottom;
/// <summary>
/// The player's movement velocity
/// </summary>
public Vector2 Velocity
{
get { return velocity; }
set { velocity = value; }
}
Vector2 velocity;
// Constants for controling horizontal movement
private const float MoveAcceleration = 14000.0f;
private const float MaxMoveSpeed = 2000.0f;
private const float GroundDragFactor = 0.58f;
private const float AirDragFactor = 0.65f;
// Constants for controlling vertical movement
private const float MaxJumpTime = 0.35f;
private const float JumpLaunchVelocity = -4000.0f;
private const float GravityAcceleration = 3500.0f;
private const float MaxFallSpeed = 600.0f;
private const float JumpControlPower = 0.14f;
// Input configuration
private const float MoveStickScale = 1.0f;
private const Buttons JumpButton = Buttons.A;
/// <summary>
/// Gets whether or not the player is on the ground.
/// </summary>
public bool IsOnGround
{
get { return isOnGround; }
}
bool isOnGround;
// Current user movement input.
private float movement;
// Jumping state
private bool isJumping;
private bool wasJumping;
private float jumpTime;
private Rectangle localBounds;
/// <summary>
/// Gets a rectangle which bounds this player in world space.
/// </summary>
public Rectangle BoundingRectangle
{
get
{
int left = (int)Math.Round(Position.X - Origin.X) + localBounds.X;
int top = (int)Math.Round(Position.Y - Origin.Y) + localBounds.Y;
return new Rectangle(left, top, localBounds.Width, localBounds.Height);
}
}
/// <summary>
/// Gets the texture's origin
/// </summary>
public Vector2 Origin
{
get { return new Vector2(playerSprite.Width / 2.0f, playerSprite.Height / 2.0f); }
}
.
.
.
To create the player in the level, we need to make a constructor that gives us the position the player is at, and the current level the player is on. Then we will load the Texture into our variable, and calculate the local bounds of the player (the bounding box inside the texture, since the player isn't drawn to the comlpete edge). Then we want to make the player alive.
/// <summary>
/// Constructors a new player.
/// </summary>
public Player(Level _level, Vector2 _position)
{
level = _level;
LoadContent();
Reset(_position);
}
/// <summary>
/// Loads the player sprite sheet and sounds.
/// </summary>
public void LoadContent()
{
// Load animated textures.
playerSprite = Level.Content.Load<Texture2D>("Sprites/Player/player");
// Calculate bounds within texture size.
// subtract 4 from width and height to remove a 2px buffer
// around the player.
int width = playerSprite.Width-4;
int left = (playerSprite.Width - width) / 2;
int height = playerSprite.Height-4;
int top = playerSprite.Height - height;
localBounds = new Rectangle(left, top, width, height);
}
/// <summary>
/// Resets the player to life.
/// </summary>
/// <param name="_position">The position to come to life at.</param>
public void Reset(Vector2 _position)
{
Position = _position;
Velocity = Vector2.Zero;
isAlive = true;
}
Now let's handle the input that the user gives to the player to make him move thruout the level. I am having it check for the arrow keys, the typical wasd layout, and of course, the 360 game pad.
/// <summary>
/// Gets player horizontal movement and jump commands from input.
/// </summary>
private void GetInput()
{
// Get input state.
GamePadState gamePadState = GamePad.GetState(PlayerIndex.One);
KeyboardState keyboardState = Keyboard.GetState();
// Get analog horizontal movement.
movement = gamePadState.ThumbSticks.Left.X * MoveStickScale;
// Ignore small movements to prevent running in place.
if (Math.Abs(movement) < 0.5f)
movement = 0.0f;
// If any digital horizontal movement input is found, override the analog movement.
if (gamePadState.IsButtonDown(Buttons.DPadLeft) ||
keyboardState.IsKeyDown(Keys.Left) ||
keyboardState.IsKeyDown(Keys.A))
{
movement = -1.0f;
}
else if (gamePadState.IsButtonDown(Buttons.DPadRight) ||
keyboardState.IsKeyDown(Keys.Right) ||
keyboardState.IsKeyDown(Keys.D))
{
movement = 1.0f;
}
// Check if the player wants to jump.
isJumping =
gamePadState.IsButtonDown(JumpButton) ||
keyboardState.IsKeyDown(Keys.Space) ||
keyboardState.IsKeyDown(Keys.Up) ||
keyboardState.IsKeyDown(Keys.W);
}
We are now handling the input, but the player will not be going anywhere, because we need to apply some simple physics to make the user move thruout the level. And we don't stop there, we need to check to see if the player is on a tile, platform, falling to his death, or running into a tile, in other words, we need to handle the collisions with our tilemap. Each of these will be handled in thier own seperate functions, and will be tied together in the player's Update function. (PS, don't worry about missing a function back to the level object, we will tackle that shortly.)
/// <summary>
/// Handles input, performs physics, and animates the player sprite.
/// </summary>
public void Update(GameTime _gameTime)
{
GetInput();
ApplyPhysics(_gameTime);
// Clear input.
movement = 0.0f;
isJumping = false;
}
/// <summary>
/// Updates the player's velocity and position based on input, gravity, etc.
/// </summary>
public void ApplyPhysics(GameTime _gameTime)
{
float elapsed = (float)_gameTime.ElapsedGameTime.TotalSeconds;
Vector2 previousPosition = Position;
// Base velocity is a combination of horizontal movement control and
// acceleration downward due to gravity.
velocity.X += movement * MoveAcceleration * elapsed;
velocity.Y = MathHelper.Clamp(velocity.Y + GravityAcceleration * elapsed, -MaxFallSpeed, MaxFallSpeed);
velocity.Y = DoJump(velocity.Y, _gameTime);
// Apply pseudo-drag horizontally.
if (IsOnGround)
velocity.X *= GroundDragFactor;
else
velocity.X *= AirDragFactor;
// Prevent the player from running faster than his top speed.
velocity.X = MathHelper.Clamp(velocity.X, -MaxMoveSpeed, MaxMoveSpeed);
// Apply velocity.
Position += velocity * elapsed;
Position = new Vector2((float)Math.Round(Position.X), (float)Math.Round(Position.Y));
// If the player is now colliding with the level, separate them.
HandleCollisions();
// If the collision stopped us from moving, reset the velocity to zero.
if (Position.X == previousPosition.X)
velocity.X = 0;
if (Position.Y == previousPosition.Y)
velocity.Y = 0;
}
/// <summary>
/// Calculates the Y velocity accounting for jumping and
/// animates accordingly.
/// </summary>
/// <remarks>
/// During the accent of a jump, the Y velocity is completely
/// overridden by a power curve. During the decent, gravity takes
/// over. The jump velocity is controlled by the jumpTime field
/// which measures time into the accent of the current jump.
/// </remarks>
/// <param name="_velocityY">
/// The player's current velocity along the Y axis.
/// </param>
/// <param name="_gameTime">The gametime.</param>
/// <returns>
/// A new Y velocity if beginning or continuing a jump.
/// Otherwise, the existing Y velocity.
/// </returns>
private float DoJump(float _velocityY, GameTime _gameTime)
{
// If the player wants to jump
if (isJumping)
{
// Begin or continue a jump
if ((!wasJumping && IsOnGround) || jumpTime > 0.0f)
{
jumpTime += (float)_gameTime.ElapsedGameTime.TotalSeconds;
}
// If we are in the ascent of the jump
if (0.0f < jumpTime && jumpTime <= MaxJumpTime)
{
// Fully override the vertical velocity with a power curve that gives players more control over the top of the jump
_velocityY = JumpLaunchVelocity * (1.0f - (float)Math.Pow(jumpTime / MaxJumpTime, JumpControlPower));
}
else
{
// Reached the apex of the jump
jumpTime = 0.0f;
}
}
else
{
// Continues not jumping or cancels a jump in progress
jumpTime = 0.0f;
}
wasJumping = isJumping;
return _velocityY;
}
/// <summary>
/// Detects and resolves all collisions between the player and his neighboring
/// tiles. When a collision is detected, the player is pushed away along one
/// axis to prevent overlapping. There is some special logic for the Y axis to
/// handle platforms which behave differently depending on direction of movement.
/// </summary>
private void HandleCollisions()
{
// Get the player's bounding rectangle and find neighboring tiles.
Rectangle bounds = BoundingRectangle;
int leftTile = (int)Math.Floor((float)bounds.Left / Tile.Width);
int rightTile = (int)Math.Ceiling(((float)bounds.Right / Tile.Width)) - 1;
int topTile = (int)Math.Floor((float)bounds.Top / Tile.Height);
int bottomTile = (int)Math.Ceiling(((float)bounds.Bottom / Tile.Height)) - 1;
// Reset flag to search for ground collision.
isOnGround = false;
// For each potentially colliding tile,
for (int y = topTile; y <= bottomTile; ++y)
{
for (int x = leftTile; x <= rightTile; ++x)
{
// If this tile is collidable,
TileCollision collision = Level.GetCollision(x, y);
if (collision != TileCollision.Passable)
{
// Determine collision depth (with direction) and magnitude.
Rectangle tileBounds = Level.GetBounds(x, y);
Vector2 depth = bounds.GetIntersectionDepth(tileBounds);
if (depth != Vector2.Zero)
{
float absDepthX = Math.Abs(depth.X);
float absDepthY = Math.Abs(depth.Y);
// Resolve the collision along the shallow axis.
if (absDepthY < absDepthX || collision == TileCollision.Platform)
{
// If we crossed the top of a tile, we are on the ground.
if (previousBottom <= tileBounds.Top)
isOnGround = true;
// Ignore platforms, unless we are on the ground.
if (collision == TileCollision.Impassable || IsOnGround)
{
// Resolve the collision along the Y axis.
Position = new Vector2(Position.X, Position.Y + depth.Y);
// Perform further collisions with the new bounds.
bounds = BoundingRectangle;
}
}
else if (collision == TileCollision.Impassable) // Ignore platforms.
{
// Resolve the collision along the X axis.
Position = new Vector2(Position.X + depth.X, Position.Y);
// Perform further collisions with the new bounds.
bounds = BoundingRectangle;
}
}
}
}
}
// Save the new bounds bottom.
previousBottom = bounds.Bottom;
}
And the last thing we need to do for our player, is to draw him. Let's make a simple Draw function that outputs the player's texture onto the screen.
/// <summary>
/// Draws the animated player.
/// </summary>
public void Draw(GameTime _gameTime, SpriteBatch _spriteBatch)
{
// yes I know this could be set once, but later on this will change when we get
// to animating our player.
Rectangle source = new Rectangle(0, 0, playerSprite.Width, playerSprite.Height);
// Draw the player.
_spriteBatch.Draw(playerSprite, position, source, Color.White, 0.0f, Origin, 1.0f, SpriteEffects.None, 0.0f);
}
Woo, that is a lot of code to swallow. We aren't done yet, we still need to add collision info to our tiles, and create some functions in our level to check for collisions.
Open up our Tile class, and let's add a simple enum to detect what type of tile we are dealing with. We will have 3 different tiles, passable, impassable, and platform. We are also going to edit the tile's constructor to pass in an enum value for what type of collision we want done when the player interacts with this tile. Here is what our Tile class should look like.
using Microsoft.Xna.Framework;
namespace ScribblePlatformer
{
/// <summary>
/// Controls the collision detection and response behavior of a tile.
/// </summary>
public enum TileCollision
{
/// <summary>
/// A passable tile is one which does not hinder player motion at all.
/// </summary>
Passable = 0,
/// <summary>
/// An impassable tile is one which does not allow the player to move through
/// it at all. It is completely solid.
/// </summary>
Impassable = 1,
/// <summary>
/// A platform tile is one which behaves like a passable tile except when the
/// player is above it. A player can jump up through a platform as well as move
/// past it to the left and right, but can not fall down through the top of it.
/// </summary>
Platform = 2,
}
public class Tile
{
// name of the sprite sheet
public string TileSheetName;
// the index of the rectangle for sourceRect for drawing.
public int TileSheetIndex;
public TileCollision Collision;
public const int Width = 64;
public const int Height = 64;
public static readonly Vector2 Size = new Vector2(Width, Height);
/// <summary>
/// Constructs a new tile.
/// </summary>
public Tile(string _tileSheetName, int _tileSheetIndex, TileCollision _collision)
{
TileSheetName = _tileSheetName;
TileSheetIndex = _tileSheetIndex;
Collision = _collision;
}
}
}
Now let's get to the last few edits, and open up the Level class. Let's add a variable to house our player. This should go right after we define our tileSheets and TileDefinitions. We also want to hold the starting position of our player that get's loaded from our level.
.
.
.
class Level : IDisposable
{
private Tile[,] tiles;
private Dictionary<string,Texture2D> tileSheets;
public List<Rectangle> TileDefinitions;
// Entities in the level.
public Player Player
{
get { return player; }
}
Player player;
// holds the starting point for the level
private Vector2 start;
.
.
.
Now we can create our player inside the level. We want to edit the LoadTile function to check for our staring point for the player. In our map, I am using a plus sign (+) to represent the starting point for our player. Our new LoadTile function should look like this.
private Tile LoadTile(char _tileType, int _x, int _y)
{
switch (_tileType)
{
// Blank space
case '.':
return new Tile(String.Empty, 0, TileCollision.Passable);
// Platform blocks
case 'B':
return LoadVarietyTile("Platforms", 0, 5);
case 'G':
return LoadVarietyTile("Platforms", 5, 5);
case 'O':
return LoadVarietyTile("Platforms", 10, 5);
case 'R':
return LoadVarietyTile("Platforms", 15, 5);
case 'Y':
return LoadVarietyTile("Platforms", 20, 5);
// Impassable block
case 'b':
return LoadVarietyTile("Blocks", 0, 5);
case 'g':
return LoadVarietyTile("Blocks", 5, 5);
case 'o':
return LoadVarietyTile("Blocks", 10, 5);
case 'r':
return LoadVarietyTile("Blocks", 15, 5);
case 'y':
return LoadVarietyTile("Blocks", 20, 5);
// player start point
case '+':
return LoadStartTile(_x, _y);
// Unknown tile type character
default:
throw new NotSupportedException(String.Format("Unsupported tile type character '{0}' at position {1}, {2}.", _tileType, _x, _y));
}
}
Notice that for the case +, it is calling a new function to load the "start tile". This tile is just a empty tile, but inside that function is where the magic happens and our player is born.
/// <summary>
/// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected.
/// </summary>
private Tile LoadStartTile(int _x, int _y)
{
if (Player != null)
throw new NotSupportedException("A level may only have one starting point.");
// player is drawn based on origin, so we need to compensate that.
// 48 is half the size of the player.
// (x * baseTileWidth) + halfofplayerwidth, (y * baseTileHeight) + (baseTileHeight - playerHeight) + halfofplayerheight
start = new Vector2((_x * 64) + 48 , (_y * 64) + 16);
player = new Player(this, start);
return new Tile(String.Empty, 0, TileCollision.Passable);
}
Now we have our player made, but he isn't going to respond to any of our inputs yet. We must call our player's Update function to have him play around in his world. So we need to create an Update function for our Level class.
/// <summary>
/// Updates all objects in the world, performs collision between them,
/// and handles the time limit with scoring.
/// </summary>
public void Update(GameTime _gameTime)
{
Player.Update(_gameTime);
}
Remember when I said to not worry about a few missing functions in Level just yet, well here they are. GetCollision checks to see if the player has collided with a tile at the given location, and GetBounds gets the bounding box of the specific tile.
/// <summary>
/// Gets the collision mode of the tile at a particular location.
/// This method handles tiles outside of the levels boundries by making it
/// impossible to escape past the left or right edges, but allowing things
/// to jump beyond the top of the level and fall off the bottom.
/// </summary>
public TileCollision GetCollision(int _x, int _y)
{
// Prevent escaping past the level ends.
if (_x < 0 || _x >= Width)
return TileCollision.Impassable;
// Allow jumping past the level top and falling through the bottom.
if (_y < 0 || _y >= Height)
return TileCollision.Passable;
return tiles[_x, _y].Collision;
}
/// <summary>
/// Gets the bounding rectangle of a tile in world space.
/// </summary>
public Rectangle GetBounds(int _x, int _y)
{
// 20 if platform
// 5 if block
if (_x < 0 || _y < 0 || _x >= Width || _y >= Height)
return new Rectangle(_x * Tile.Width, _y * Tile.Height, Tile.Width, Tile.Height);
if (tiles[_x, _y].Collision == TileCollision.Platform)
return new Rectangle(_x * Tile.Width, (_y * Tile.Height) + 20, Tile.Width, Tile.Height-20);
return new Rectangle(_x * Tile.Width, (_y * Tile.Height) + 5, Tile.Width, Tile.Height-5);
}
One last thing to do in our Level class, and that is to tell our player to draw itself. To do that, we just need to edit the Draw funciton and add 1 simple line.
public void Draw(GameTime _gameTime, SpriteBatch _spriteBatch)
{
DrawTiles(_spriteBatch);
player.Draw(_gameTime, _spriteBatch);
}
Now before you go and run this, we need to tell our game to update the level on every update call. So open up our Game1 class file and edit the Update function to call the level's Update function.
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="_gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime _gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// TODO: Add your update logic here
level.Update(_gameTime);
base.Update(_gameTime);
}
Now build and run, and you can have our little player jumping for joy around in the level.

In the next article, we are going to animate our player. To get a hold of the project as it should be by the end of this part, click here.
Stay tuned for the 3rd part of the "I Can Has Platformer" series...
February 25th, 2010 - 00:05
Someone has watched a little too much ATHF!!