Article: I Can Has Platformer? (Part 1)
by Casey Young
This is the first part of a series on how to create a simple platformer game. Each article in this series will go through one or more simple aspects that make up a platformer. By the end of the series, you should have a basic platformer to build off of.
One day while I was slaqing, someone came up to me and asked "I can has a platformer?" I told this young lad, that "Yes, yes you can has! And I will show you the ways of the platformer." I have designed list of articles to show this young lad, and to you. This is Part 1 of "I Can Has Platformer", and will cover creating the project and making a simple tilemap.
First off, let's create a new XNA 3.1 project. I named this project as "ScribblePlatformer" because that is my style of drawing. You can name it anything your heart desires, but in all my code and these articles, I am going to reference this as "ScribblePlatformer".
After you got this mighty fine project created, go ahead and download these 2 sprite sheets I have made and tuck them into the Content project under a folder called "Tiles".
Now I bet the reason why I named ScribblePlatformer the way I did is clear as mud!
With the current assets downloaded and loaded into the project, now let's get to some of the code. Add a new Class File to the project and name it "Tile". Our Tile class will hold the name of the spritesheet the tile resides, and also the index of the rectangle on that spritesheet (this will make more sense when we get to drawing). We also want to hold the size of the tile. Here is what I defined in my Tile class file.
using Microsoft.Xna.Framework;
namespace ScribblePlatformer
{
public class Tile
{
// name of the sprite sheet
public string TileSheetName;
// the index of the rectangle for sourceRect for drawing.
public int TileSheetIndex;
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)
{
TileSheetName = _tileSheetName;
TileSheetIndex = _tileSheetIndex;
}
}
}
This is all nice and good, but we need some way to represent the tiles to make a level to make any of this worth wild. Before I spit out what the level file will look like, I want to go over the syntax I have laid out for the level for this part of the series. Notice the blocks and platforms each have 5 colors, and each color has 5 different designs. The way the level will work, we will just tell what color we want, and on load, it will randomly choose a design for that said color. The file that holds the level information will be straight text, comma delimited. Each line represents a row of the tile map. Each block will be represented by 1 character. Blue will be represented as b, Green will be represented as g, and if you can't guess the pattern, the rest will be o, r, and y for Orange, Red, and Yellow respectively. Like the blocks, platforms will be the same, except they will be in caps, so B, G, O, R, and Y. To represent a blank tile, as in nothing there, a .(period) will be used. Now with that knowledge, let's add a plain text file to the Content Project in ScribblePlatformer. Name this text file "level1.txt" and put it in a directory called "Levels". Inside copy the following and save it.
.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,. .,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,. .,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,. .,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,. .,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,. .,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,. .,.,.,.,.,.,.,.,.,O,Y,R,.,.,.,.,.,.,.,. .,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,. .,.,.,.,R,B,G,.,.,.,.,.,.,.,B,G,O,.,.,. .,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,. r,r,b,b,g,g,o,o,y,y,r,b,g,o,y,r,b,g,o,y
Now we have a level to work with, and know the basics of the layout of the level itself. Let's create a new Class File in the ScribblePlatformer project called "Level". We are going to inherit from the IDisposable class to clean up the resources we did when we unload the level.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
namespace ScribblePlatformer
{
class Level : IDisposable
{
}
}
There will be a handful of private members, as well as some public properties for our level. First let's create a 2-dimensional array for the tiles in the level, and a few collections for the spritesheets and source rectangles.
private Tile[,] tiles; private Dictionary<string,Texture2D> tileSheets; public Dictionary<int, Rectangle> TileDefinitions;
Since we are going to load up content inside this level, we should have a ContentManager object.
public ContentManager Content
{
get { return content; }
}
ContentManager content;
The last few objects are just some helper objects to help keep the code semi organized and used for selecting a tile pattern for a specific color.
private const int TileWidth = 64;
private const int TileHeight = 64;
private const int TilesPerRow = 5;
private const int NumRowsPerSheet = 5;
private Random random = new Random(1337);
public int Width
{
get { return tiles.GetLength(0); }
}
public int Height
{
get { return tiles.GetLength(1); }
}
Now let's get to the constructor of the class. We want to pass in the filename of the level we want to load, and the IServiceProvider from our main Game class. This is so we can create a ContentManager to load up the resources of the specific level. Inside, we want to load up the 2 spritesheets, populate a collection of source rectangles, and then read the level file and load the tiles into our 2d array.
public Level(IServiceProvider _serviceProvider, string _path)
{
// Create a new content manager to load content used just by this level.
content = new ContentManager(_serviceProvider, "Content");
// load the textures
tileSheets = new Dictionary<string,Texture2D>();
tileSheets.Add("Blocks", Content.Load<Texture2D>("Tiles/Blocks"));
tileSheets.Add("Platforms", Content.Load<Texture2D>("Tiles/Platforms"));
// create a collection of source rectangles.
TileDefinitions = new Dictionary<int, Rectangle>();
for (int i = 0; i < TilesPerRow * NumRowsPerSheet; i++)
{
Rectangle rectTile = new Rectangle(
(i % TilesPerRow) * TileWidth,
(i / TilesPerRow) * TileHeight,
TileWidth,
TileHeight);
TileDefinitions.Add(i, rectTile);
}
LoadTiles(_path);
}
To load up the tiles, we want to read in the level file and then parse through it. Then on each tile, we want to create the tile for each spot in the array.
private void LoadTiles(string _path)
{
// Load the level and ensure all of the lines are the same length.
int width;
List<string> lines = new List<string>();
using (StreamReader reader = new StreamReader(_path))
{
string line = reader.ReadLine();
string[] tileEntries = line.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
width = tileEntries.Count();
while (line != null)
{
lines.Add(line);
tileEntries = line.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
if (tileEntries.Count() != width)
throw new Exception(String.Format("The length of line {0} is different from all preceeding lines.", lines.Count));
line = reader.ReadLine();
}
}
// Allocate the tile grid.
tiles = new Tile[width, lines.Count];
// Loop over every tile position,
for (int y = 0; y < Height; ++y)
{
for (int x = 0; x < Width; ++x)
{
// to load each tile.
string[] tileEntries = lines[y].Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
char tileType = tileEntries[x].ToCharArray()[0];
tiles[x, y] = LoadTile(tileType, x, y);
}
}
}
private Tile LoadTile(char _tileType, int _x, int _y)
{
switch (_tileType)
{
// Blank space
case '.':
return new Tile(String.Empty, 0);
// 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);
// Unknown tile type character
default:
throw new NotSupportedException(String.Format("Unsupported tile type character '{0}' at position {1}, {2}.", _tileType, _x, _y));
}
}
private Tile LoadVarietyTile(string _tileSheetName, int _colorRow, int _variationCount)
{
int index = random.Next(_variationCount);
// get index on tile to rect dictionary
int tileSheetIndex = _colorRow + index;
return new Tile(_tileSheetName,tileSheetIndex);
}
Now we have all the tiles loaded into the array, all that is left now is to draw the tiles onto the screen. We are going to create a simple draw function that will draw all the objects in the level. Right now all we have are the blocks and platforms. So our draw function is pretty small. I split this up into 2 functions so later on when we add more entities to the level, we can draw them at different times and keep the main draw function nice and tidy.
public void Draw(GameTime _gameTime, SpriteBatch _spriteBatch)
{
DrawTiles(_spriteBatch);
}
private void DrawTiles(SpriteBatch _spriteBatch)
{
// For each tile position
for (int y = 0; y < Height; ++y)
{
for (int x = 0; x < Width; ++x)
{
// If there is a visible tile in that position
if(tileSheets.ContainsKey(tiles[x, y].TileSheetName))
{
// Draw it in screen space.
Vector2 position = new Vector2(x, y) * Tile.Size;
_spriteBatch.Draw(tileSheets[tiles[x, y].TileSheetName], position, TileDefinitions[tiles[x, y].TileSheetIndex], Color.White);
}
}
}
}
Last thing, since we made this inherit IDisposable, we must define the function Dispose. We just want to unload all the content loaded in this level, so the function is quite small.
public void Dispose()
{
Content.Unload();
}
With that, we are done messing with the Level class for this article. To see the fruits of our labor, lets turn to the Game1.cs file and add an instance of the Level class and tell it to load and draw. Let's change the screen resolution to 1280x720, and set the Framerate to stay close to 60fps. Game1's object and constructor should look to similar to the following.
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
private Level level;
private const int TargetFrameRate = 60;
private const int BackBufferWidth = 1280;
private const int BackBufferHeight = 720;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth = BackBufferWidth;
graphics.PreferredBackBufferHeight = BackBufferHeight;
Content.RootDirectory = "Content";
// Framerate differs between platforms.
TargetElapsedTime = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / TargetFrameRate);
}
In LoadContent, we want to load up the level object with our level we made. I have made a simple function to keep it nice and clean.
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
LoadLevel();
}
private void LoadLevel()
{
// Find the path of the next level.
string levelPath;
// Loop here so we can try again when we can't find a level.
while (true)
{
// Try to find the next level. They are sequentially numbered txt files.
levelPath = "Levels/level1.txt";
levelPath = Path.Combine(StorageContainer.TitleLocation, "Content/" + levelPath);
if (File.Exists(levelPath))
break;
}
// Unloads the content for the current level before loading the next one.
if (level != null)
level.Dispose();
// Load the level.
level = new Level(Services, levelPath);
}
All that is left is now to tell our level object to draw itself. We do this in the Draw function in Game1.
protected override void Draw(GameTime _gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
level.Draw(_gameTime, spriteBatch);
spriteBatch.End();
base.Draw(_gameTime);
}
Compile and run ScribblePlatformer and you should get something that looks like this.
This pretty much wrap's up this part of the "I Can Has Platformer" series. In the next part, I will touch on adding a player character to move around and how he interacts with the tilemap. To download the wonderful project as it stands so far, click here.
Stay tuned for the 2nd part of the "I Can Has Platformer" series...



January 7th, 2010 - 08:28
Hi there, it’s a great article but I have a question about your code.
Why do you store the tiles definitions in a Dictionary and not in a List ?
January 7th, 2010 - 15:36
When I first started the program, I was thinking of using a naming system for the definitions. Later, I switched to a simple number system, and never changed it to say a List. The code probably has a lot of re-factoring and improvements, but the main point to get out of the series is a simple walk through on creating a platformer game.
January 10th, 2010 - 22:31
Cool! Keep up the good work!
January 12th, 2010 - 18:48
Thanks for this tutorial. I manually typed in all the code which helped me tremendously in actually learning from the tutorial. I am eagerly anticipating the second part.
February 18th, 2010 - 00:08
Great, however when I try to run it, nothing happens. No errors, just nothing at all loads?
February 22nd, 2010 - 04:56
@Sadness, want to drop me a line about what is going on? My email is my handle @ gmail. Or you can bug me on irc in the #xna channel on Efnet.