Article: I Can Has Platformer? (Part 5)
by Casey Young
Welcome back to the fifth part of my series on how to create a simple platformer game. In this article, we give our hero something to collect.
Please refer to my fourth article to grab the final project before continuing this part, you will need that code to edit along with to make things work. =D
From the last part, we had our hero meet his nemisis, MuffinMan. Our hero is a little bored with him, and wants something else to do. Let's have our hero collect things throughout the level shall we?
Save this file under Content/Sprites, in a new directory called Collectables. The sprite sheet should be transparent with the sprites in white. I made a black background so you could see them on the page.
Now with the new sprite sheet saved and loaded into our project, let's start creating the Collectable class. The Collectable class will be very similar to the Enemy class. Start off by creating a new class file named Collectable, and have it inherit our AnimatedSprite class. Let's start off with the public properties and private fields of the file. Notice that everything should be very similar to our Player and Enemy class, except for one thing, colorOffset. This field will give our collectables a different color when we draw them, so we don't have to add more sprite sheets for each color.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace ScribblePlatformer
{
class Collectable : AnimatedSprite
{
private string currentAnim = "Idle";
private Rectangle localBounds;
bool isAlive;
Level level;
Vector2 position;
private Color colorOffset;
/// <summary>
/// Is the collectable alive, or has been collected?
/// </summary>
public bool IsAlive
{
get { return isAlive; }
}
/// <summary>
/// What level are we on, used for collision and interaction
/// with the level's entities.
/// </summary>
public Level Level
{
get { return level; }
}
/// <summary>
/// What position the collectable is in the world
/// </summary>
public Vector2 Position
{
get { return position; }
set { position = value; }
}
/// <summary>
/// Gets a rectangle which bounds this collectable 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);
}
}
When we want to create a new Collectable, we want to tell our system what type of collectable it is. This is exactly what the Enemy class did, but this time we are going to use it.
/// <summary>
/// Constructs a new Collectable.
/// </summary>
public Collectable(Level _level, Vector2 _position, string _collectable)
: base(32, 32, 4)
{
level = _level;
position = _position;
isAlive = true;
LoadContent(_collectable);
}
[/chsharp]
Notice something different? What is this base() call? If you recall, our AnimatedSprite class was hard coded for sprite sheets with frames of 96x96 and 4 frames per row. We want this to be more dynamic per entity we add to the game. We are going to pause on the Collectable class here and modify the AnimatedSprite class, and add the base() to the Player and Enemy class's constructors. First up, let's open up the AnimatedSprite class and add 3 new parameters for the constructor.
[csharp]
public AnimatedSprite(int _frameWidth, int _frameHeight, int _framesPerRow)
{
FrameWidth = _frameWidth;
FrameHeight = _frameHeight;
framesPerRow = _framesPerRow;
SpriteTextures = new List<Texture2D>();
SpriteAnimations = new Dictionary<string, Animation>();
}
This allows us to change the frame width/height and frames per row for each different instance of the AnimatedSprite class. Now let's add the base() call to Player and Enemy like so:
/// <summary>
/// Constructors a new player.
/// </summary>
public Player(Level _level, Vector2 _position) : base(96, 96, 4)
{
level = _level;
LoadContent();
Reset(_position);
}
/// <summary>
/// Constructs a new Enemy.
/// </summary>
public Enemy(Level _level, Vector2 _position, string _enemy)
: base(96, 96, 4)
{
level = _level;
position = _position;
isAlive = true;
isCompletelyDead = false;
LoadContent(_enemy);
}
Alright, now let's get back to the Collectable class. We need to load the content, and since we are wanting to use the colorOffset to change the colors of the collectables, we will have 2 different values for the collectable type in our level.
/// <summary>
/// Loads the player sprite sheet.
/// </summary>
public void LoadContent(string _collectable)
{
string sheetString = string.Empty;
switch (_collectable)
{
case "s":
sheetString = "Sprites/Collectables/scribbles";
colorOffset = Color.Black;
break;
case "S":
sheetString = "Sprites/Collectables/scribbles";
colorOffset = Color.Gold;
break;
default:
sheetString = "Sprites/Collectables/scribbles";
colorOffset = Color.White;
break;
}
// Load animated textures.
Texture2D tex = Level.Content.Load<Texture2D>(sheetString);
if(!SpriteTextures.Contains(tex))
SpriteTextures.Add(tex);
Animation anim = new Animation();
anim.LoadAnimation("Idle", 0, new List<int>
{
0,
1,
2,
3
}, 16, true);
SpriteAnimations.Add("Idle", anim);
// Calculate bounds within texture size.
// subtract 4 from height to remove a 2px buffer
// around the collectable.
int width = FrameWidth;
int left = (FrameWidth - width) / 2;
int height = FrameHeight - 4;
int top = FrameHeight - height;
localBounds = new Rectangle(left, top, width, height);
SpriteAnimations[currentAnim].ResetPlay();
}
For a collectable, we don't have a death animation, just the single Idle animation. We do want to know when the collectable was collected, so we can do things later on, like add to a score, play a sound, etc. This is where the OnKilled function comes into play. Right now, we just want to say it is not alive anymore.
/// <summary>
/// Called when the player has collected us.
/// </summary>
public void OnKilled()
{
isAlive = false;
}
The last two things left for our Collectable class is to animate it and telling it to draw. The Update function is very bare, it only tells the the animation to update.
/// <summary>
/// Animate the collectable
/// </summary>
public void Update(GameTime _gameTime)
{
SpriteAnimations[currentAnim].Update(_gameTime);
}
The Draw function is very similar to the previous class's Draw function. We want to draw our collectable with the correct frame of animation and with the correct color tint.
/// <summary>
/// Draws the animated enemy.
/// </summary>
public void Draw(GameTime _gameTime, SpriteBatch _spriteBatch)
{
Rectangle source = GetFrameRectangle(SpriteAnimations[currentAnim].FrameToDraw);
// Draw the enemy.
_spriteBatch.Draw(SpriteTextures[0], position, source, colorOffset, 0.0f, Origin, 1.0f, SpriteEffects.None, 0.0f);
}
[csharp]
And that is the Collectable class. Very similar to our Enemy class, except for the collectables don't move, and we added a tint to the collectable when drawn. There is just a few things left to do. We need to tell the Level to load up a collectable, and implement the collision detections. Let's load up the Level1.txt file and make some spawn points for some collectables. We are going to use the letter 's' for a black scribble, and 'S' for a golden one.
[text]
.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.
.,.,.,.,.,.,.,.,.,.,.,e,.,.,.,.,.,.,.,.
.,.,.,.,.,.,.,.,.,r,b,g,o,y,.,.,.,.,.,.
.,b,.,e,.,.,s,.,.,.,.,.,.,.,.,S,.,.,.,.
.,r,y,o,G,B,R,.,.,.,.,.,.,.,.,s,.,.,.,.
.,.,.,.,.,.,.,.,.,.,S,.,.,.,.,s,.,.,.,.
.,.,.,.,.,.,.,.,.,O,Y,R,.,.,.,s,.,.,.,.
.,.,.,.,.,s,.,.,.,.,.,.,.,.,.,s,.,.,.,.
.,.,.,.,R,B,G,.,.,.,g,.,.,.,B,G,O,.,.,.
.,+,.,.,.,.,.,.,.,.,b,.,.,.,.,.,.,.,e,.
r,r,b,b,g,g,o,o,y,y,r,b,g,o,y,r,b,g,o,y
[/text]
If we try to run this right now, our Level will complain that it doesn't know what to do with the value of 's' or 'S'. Let's load up the Level class and make it aware of the new collectables. First off, let's add two lists of collectables one for active, and another for removing from active.
[csharp]
private List<Collectable> collectables = new List<Collectable>();
private List<Collectable> collectedCollectables = new List<Collectable>();
In the LoadTile function, we need to add a case for 's' and 'S', and have it load up a collectable. So add this case to the switch.
// Collectables spawns case 's': return LoadCollectableTile(_x, _y, "s"); case 'S': return LoadCollectableTile(_x, _y, "S");
Now with that added, we need to implement this new function. It creates a collectable, adds it to the collectables list, and creates a blank tile on the map.
/// <summary>
/// Instantiates an collectable and puts it in the level.
/// </summary>
private Tile LoadCollectableTile(int _x, int _y, string _collectable)
{
Vector2 position = new Vector2((_x * 64) + 48, (_y * 64) + 20);
collectables.Add(new Collectable(this, position, _collectable));
return new Tile(String.Empty, 0, TileCollision.Passable);
}
In the Update function, we want to update the collectables. The updated Update function should look something like this:
/// <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);
if (Player.IsCompletelyDead)
Player.Reset(start);
UpdateEnemies(_gameTime);
UpdateCollectables(_gameTime);
}
The new Update function has a new call in it we need to implement. Inside this new function, we are going to tell each collectable to update, and check if he has been collected by our hero. If there are any collectables that have been collected, we want to remove them from the main update list.
/// <summary>
/// Animates each enemy and allow them to kill the player.
/// </summary>
private void UpdateCollectables(GameTime _gameTime)
{
foreach (Collectable collectable in collectables)
{
collectable.Update(_gameTime);
if (player.IsAlive && collectable.IsAlive)
{
// Touching an enemy instantly kills the player
if (collectable.BoundingRectangle.Intersects(Player.BoundingRectangle))
{
OnCollectableCollected(collectable);
}
}
}
if (collectedCollectables.Count > 0)
{
foreach (Collectable collectable in collectedCollectables)
{
collectables.Remove(collectable);
}
collectedCollectables.Clear();
}
}
We are almost done here, we now need to implent the new function that we are calling in UpdateCollectables, OnCollectableCollected.
/// <summary>
/// Called when an collectable is collected.
/// </summary>
private void OnCollectableCollected(Collectable _collectable)
{
_collectable.OnKilled();
collectedCollectables.Add(_collectable);
}
The last thing to do is tell our active collectables to draw themselves. To do this, we need to modify the Draw function and call Draw for each collectable in the list.
public void Draw(GameTime _gameTime, SpriteBatch _spriteBatch)
{
DrawTiles(_spriteBatch);
player.Draw(_gameTime, _spriteBatch);
foreach (Collectable collectable in collectables)
collectable.Draw(_gameTime, _spriteBatch);
foreach (Enemy enemy in enemies)
enemy.Draw(_gameTime, _spriteBatch);
}
That should do it. Our hero now has some Scribbles to collect! Build the project and run it.
In the next article, we are going to add some UI features, like a HUD for scores and lives. To get a hold of the project as it should be by the end of this part, click here.
Stay tuned for the 6th part of the "I Can Has Platformer" series...
September 5th, 2010 - 21:26
Wow. Really fast turn around on the last two articles (did you plan it that way?).
Really good stuff for a beginner’s platformer guide, and it’s nice that it doesn’t have all of the overhead of the starter kit.
Keep ‘em coming!
September 19th, 2010 - 01:49
Great tutorial, really, I’ve been searching for a tile based 2d platformer(specially collision detection) for over a year but the other ones that I’ve found were all incomplete work in progress. Thanx a lot sir.
I have not finished the 5 parts yet(just finished part 2) but I’m looking forward to for sure. By the way, if you have time it would be great to have a part 6 on scrolling.
September 20th, 2010 - 14:57
@Jean It is in the pipeline to come, just won’t be in Part 6. If everything stays on track, it will be around part 7 or 8… =D
December 12th, 2010 - 02:49
Hi,
I’ve find your tutorials very helpful but i got a question to ask…
I tried changing the gravity so it’s horizontal making the ground ther right side of the screen, but I’m finding some difficulties with the collision detection for the player… can u give me any advice on how to solve that problem?
anyway Thx alot for the tutorials!!!
December 22nd, 2010 - 22:18
Does anyone know how you would make the game exit or close when collide with the collectable or enemy?