Part 1 - Introduction


The original Tile Engine Tutorial Series was a huge hit back when XNA Resources was first launched back in 2006. Written for the Beta 1 release of XNA Game Studio. It was updated when the Beta 2 version was released, but has gone unmodified ever since. Each part of the original series has received well over 100,000 views. That said, I've learned a lot about XNA since those early days. For a while now, I've been talking about a multi-shaped tile engine (to support isometric and hexagonal tiles) and as I was putting it together I decided that it was well past time to revisit the whole Tile Engine series from the beginning.

What is a Tile Engine

Early computer and video game systems had very, very (VERY) little memory - the original Nintendo Entertainment System had 2kb of RAM, and 2kb of video RAM, plus 284 bytes of memory for objects and color palettes. If we call this roughly 4.5kb of memory, my 4GB desktop PC has almost 1 million times the amount of memory the NES had to work with.

The full display resolution on the NES was 256x240 pixels, and while our current bits-per-pixel standards don't apply to the NES, even at one bit per pixel (which isn't a good representation anyway) you have 7.5 kb of information just to fill up a single screen. So how do you build large game worlds when you don't have enough memory to hold a single screen? Lets look at another classic NES game:



The Legend of Zelda contains hundreds of screen-sized maps, allowing the player to more or less freely explore a large game world. The key to doing this can be seen by looking how the map is composed. Each Zelda map is divided into a 15x11 grid of "tiles", or small graphical elements that can be pieced together to create a larger image. You can see this in the screenshot above by looking at the trees. Notice how there is a small border where the tiles don't quite match up with each other in between each tree tile. Similarly, the entrance to the dungeon is an empty black square tile.

If we assume 1 full byte per tile, our 15x11 grid of tiles takes up 165 bytes to store a screen-sized map. The tile graphics themselves still need to be stored somewhere, but an individual tile is only stored in memory once and then drawn wherever and whenever it is needed to compose the map image.

Games like Zelda are actually slightly more advanced than earlier tile engines. Consider the following screenshot from Ultima 1:



Unlike the Zelda tile engine, the Ultima 1 tile engine uses more abstract tiles to differentiate terrain. Sure, the mountains blend together, and trees and grass are nicely spaced, but each individual tile represents a single type of terrain, where with Zelda there are "Transition" tiles, providing the "rounded corners" of the forests and their apparent diagonal edges. At the most basic, however, these two games operate on the same principal - break up a much larger world into an easily representable array of tiles and draw as necessary.

Getting Started

We will start off this series by constructing a simple tile system akin to the one we see in Ultima 1 above. We won't worry about transitions between terrain types right now, but will simply explore the basics of getting our engine up and running and drawing a simple map.

Begin by creating a new Window Game 4.0 project in Visual Studio 2010 called "Tile Engine". Our tile engine will function on any of the XNA supported platforms, but we will use a Windows project in this tutorial series for easy testing. In the Content project associated with your new game solution, create a folder called Textures. Create a subfolder under Textures called TileSets.

The first thing we will need to do is establish some basic parameters for our engine and our tile maps. To begin with, we will need some tiles to draw with. Here is a very simple set of tiles we will use for our first Tile Engine:



Tile Sets

Now is a good time to introduce the concept of a tile set. We could certainly create separate bitmap image files for each of the terrain tiles shown above. In XNA, we could load each of these images into it's own texture resource and draw them when we draw our map. That would all work fine, but there are a few disadvantages to doing so:

First, in a large game we would end up with a huge number of little bitmap files that we would need to name appropriately and organize as part of our game's Content project. We would need to individually read all of these little files into Texture2D objects in our game.

Second, there are speed advantages when drawing with the SpriteBatch class to switching texture files as few times as possible. By keeping all of our tiles on a single sheet (or a small number of sheets for a larger project) we can gain some drawing efficiency. This won't matter for our little testing program, but in your full fledge game it might make a visible performance difference.

Save the above image (called part1_tileset.png) into the Textures\TileSets folder of your Content project and include it as part of your Visual Studio solution. This will trigger XNA Game Studio to automatically convert the image into an .XNB file suitable for use at runtime.

Terminology

Before we delve too deeply, we need to establish a few basic terms. I will add other terminology throughout this series as we build more complex iterations of our projects, but for now, we will define the following items:
  • Tile - A standard sized bitmap image, part of a tile set, drawn as needed to the display to make up a larger image/map
  • Tile Set - One or more Tile images combined into a single bitmap
  • Cell - A portion of a tile-based map representing an area that is (for now) the size of a single tile
  • Map - A collection of Cells that forms a complete tile map

The difference between a Tile and a Cell is really what I want to point out here. A tile is what we draw (a picture that looks like dirt), while a cell is the definition of what we should be drawing (a pointer to the dirt tile) at a certain point on the map. As our tile engine evolves, Tiles will remain relatively simple entities, while Cells will grow in complexity to encompass multiple layers and additional information about the particular location on the map they represent.

Defining a Tile

We need to establish a few rules about what exactly a tile is so that we can build code to quickly provide the graphical information for a given tile to the SpriteBatch class for drawing. In order to do this, we will set the following guidelines for this initial version of the Tile Engine project:
  • All tiles will be the same size : 32 x 32 pixels
  • Tiles will be organized on the tile set image in such a way that the first tile begins at pixel 0,0. There will be no space between the individual tiles, and we will size the tile set image so that it's width and height are full tile increments.

Pretty simple rules. As we can see with our tile set image above, each of our tiles is the correct size, and the tile set image is exactly four tiles wide by one tile high. Establishing these parameters means that we can start numbering our tiles at 0 (for the upper left corner of the tile set) and locate any other tile by computing it's position based on the tile number. Tile 1 is one tile to the right of tile 0. Tile 2 is one tile to the right of tile 1, etc. As we will see in a later installment, we can use the same type of calculation when we have multiple rows of tiles on the same tile set image.

What this all boils down to is that we can use a single integer to locate any tile on the tile set. In fact, for our first version of the tile engine, we really only need a number between 0 and 3! The final rectangle that determines what will be drawn by the SpriteBatch class can be calculated on the fly given that information.

Right click on your "Tile Engine" project in Solution Explorer and add a new class called "Tile.cs" to the project. Add the following two directives to the top of the class file:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

These let C# know that we will be making use of items from these two namespaces and prevent us from having to spell them out whenever we want to use something like a Texture2D or a Rectangle.

Our first version of the Tile class will be pretty simple, so change the declaration of the class by adding "static" to the front of it, to read:
static class Tile

By making the class static, there will only ever be a single Tile reference. In other words, we don't need to create variables of type "Tile" and assign things to them. Setting up the Tile class this way creates a simplified way of working with it in these initial parts of the tutorial series. We won't be doing this long term (because it also means we could only have one tileset), but it saves some time while we are working on the basics.

Add the following to the declarations section of the Tile class:
static public Texture2D TileSetTexture;

We will place our texture image here during our game's LoadContent() method so that we can reference it later when we begin drawing.

Finally, add the following function to the Tile class:

static public Rectangle GetSourceRectangle(int tileIndex)
{
    return new Rectangle(tileIndex * 32, 0, 32, 32);
}

Again, this whole thing will be expanded on in the future, but here we can see that we will return a Rectangle based on the integer index of the tile we wish to draw. Tile 0 will return the Rectangle {0, 0, 32, 32}. Tile 1 will return {32, 0, 32, 32}, and so on.

Defining a Cell

Each of our tile maps will be made up of individual cells. Think of the whole map as an Excel spreadsheet, where each cell in the spreadsheet corresponds to a single square on the map.

In our example spreadsheet here, we have W representing water squares, S for sand, D for dirt, and G for grass. For the moment, this is really all of the information we need to construct a very basic tile engine.

With our spreadsheet layout, we can only store one piece of information about each square on the map. That is fine for now. We'll expand on that later in the series. We will actually recreate part of this spreadsheet in our code when we define our TileMap class.

Create a new class in your project called "MapCell.cs". We won't need to add any of the XNA "using" directives to this class, since it will only be representing the contents of the map cells and won't need to know anything about Rectangles, Textures or other items.

Add the following automatic property and constructor to the class:
public int TileID { get; set; }

public MapCell(int tileID)
{
    TileID = tileID;
}

That's all we will need to begin with. Declaring the property this way means that a hidden internal member variable will be created to hold the property value, and the get and set code will be automatically generated to simply pass values in and out of the variable. The constructor simply sets the property to the value passed in.

As I said above, this is one of the places where we will be making a lot of changes in later versions of the Tile Engine. By altering the definition of a MapCell we can build more complex map structures, with multiple tile layers that get drawn on top of each other, indicate if the kind of square the terrain represents (impassable terrain, a lava-type terrain that damages the player if they step on it, etc), build in spawn positions for active items in the game world, etc.

What we need to know for now is that when we draw our tile map, we will examine each cell that is currently visible on the screen and use it's cell data to determine what will be drawn at that location.

Defining a Tile Map

At it's most basic, a tile map is nothing more than a collection of map cells. We could use a number of different structures to represent our tile map in memory, and the initial version of the Tile Map Engine tutorials from way back in the day used a hard-coded two dimensional int array. We will use something a little more flexible, by creating two different structures.

First, we will create a MapRow, which will use the C# List collection class to string together a horizontal row of cells. We will then add these rows to another List collection that stores each row of the map, resulting in a two dimensional representation we can loop through much like we would an array.

Add a new class file to your project called TileMap.cs. We will include the MapRow class in this class file since it will be a very small extra class that we won't need to change very much. To do this, add the following inside the "namespace" area of the class file, but outside the declaration for the TileMap class itself:
class MapRow
{
    public List<MapCell> Columns = new List<MapCell>();
}

Next, lets declare the structure we will use to hold the tile map along with a couple of variable to determine the size of our initial maps. These should be placed inside the class definition for the TileMap class:
public List<MapRow> Rows = new List<MapRow>();
public int MapWidth = 50;
public int MapHeight = 50;

Our map will actually consist of a List of MapRow objects, which in turn are Lists of MapCells. By using a List of Lists, we create what is essentially a two-dimensional arrays similar to the spreadsheet example above. Lets create a simple constructor for our TileMap class that will initialize the map and create some temporary starting data:

        public TileMap()
        {
            for (int y = 0; y < MapHeight; y++)
            {
                MapRow thisRow = new MapRow();
                for (int x = 0; x < MapWidth; x++)
                {
                    thisRow.Columns.Add(new MapCell(0));
                }
                Rows.Add(thisRow);
            }

            // Create Sample Map Data
            Rows[0].Columns[3].TileID = 3;
            Rows[0].Columns[4].TileID = 3;
            Rows[0].Columns[5].TileID = 1;
            Rows[0].Columns[6].TileID = 1;
            Rows[0].Columns[7].TileID = 1;

            Rows[1].Columns[3].TileID = 3;
            Rows[1].Columns[4].TileID = 1;
            Rows[1].Columns[5].TileID = 1;
            Rows[1].Columns[6].TileID = 1;
            Rows[1].Columns[7].TileID = 1;

            Rows[2].Columns[2].TileID = 3;
            Rows[2].Columns[3].TileID = 1;
            Rows[2].Columns[4].TileID = 1;
            Rows[2].Columns[5].TileID = 1;
            Rows[2].Columns[6].TileID = 1;
            Rows[2].Columns[7].TileID = 1;

            Rows[3].Columns[2].TileID = 3;
            Rows[3].Columns[3].TileID = 1;
            Rows[3].Columns[4].TileID = 1;
            Rows[3].Columns[5].TileID = 2;
            Rows[3].Columns[6].TileID = 2;
            Rows[3].Columns[7].TileID = 2;

            Rows[4].Columns[2].TileID = 3;
            Rows[4].Columns[3].TileID = 1;
            Rows[4].Columns[4].TileID = 1;
            Rows[4].Columns[5].TileID = 2;
            Rows[4].Columns[6].TileID = 2;
            Rows[4].Columns[7].TileID = 2;

            Rows[5].Columns[2].TileID = 3;
            Rows[5].Columns[3].TileID = 1;
            Rows[5].Columns[4].TileID = 1;
            Rows[5].Columns[5].TileID = 2;
            Rows[5].Columns[6].TileID = 2;
            Rows[5].Columns[7].TileID = 2;

            // End Create Sample Map Data
        }

We are only creating the first 6 lines from the spreadsheet above, but if you want to go ahead and complete the whole thing, as you can see it is pretty simple (if tedious) to do. The TileIDs referenced above match up with the indexes on the TileSet image we are using. TileID 0 (the default for all of our tiles) is the water tile, tile 1 is dirt, tile 2 is grass, and tile 3 is sand.

Creating a Camera

In most game applications, the entire tile map will not be visible on the screen at one time. Usually you will have the map centered on, or following, the player's character as they play the game. While there are a number of different ways to do this (including tracking offsets like I did in the old Tile Engine series) the simplest way is by simply tracking a vector that indicates the upper left corner of the current viewing area. This vector will be referred to as our camera position.

Create a new class file called Camera.cs and add the following directive to the top of the file:

using Microsoft.Xna.Framework;


Update the definition of the Camera class to make the class static:

static class Camera


Add the Location vector to the camera class:

static public Vector2 Location = Vector2.Zero;


As with our other systems, we will begin with a very simple class that we will evolved later. This is why we are separating the camera into its own class now instead of simply using a Vector2 inside the main program. When we go to expand the functionality of our camera we won't have to refactor a lot of code to move it out later.

Drawing our Tile Map

In the Game1.cs file, add a declaration for a new TileMap instance and the size of our map on screen to the class' declaration area:

TileMap myMap = new TileMap();
int squaresAcross = 5;
int squaresDown = 5;


When we draw our map to the screen, we will draw a subset of the map equal to the number of squares specified above. In this case, the size of the map we display to the screen will be 5 by 5 squares.

In the LoadContent() method of the Game1 class, lets set up the Tile static class we will be using for our initial tile engine work:

Tile.TileSetTexture = Content.Load<Texture2D>(@"Textures\TileSets\part1_tileset");


In the Draw() method of Game1.cs, lets add the code needed to display our map to the screen. Place this right after the call to GraphicsDevice.Clear():

            spriteBatch.Begin();

            Vector2 firstSquare = new Vector2(Camera.Location.X / 32, Camera.Location.Y / 32);
            int firstX = (int)firstSquare.X;
            int firstY = (int)firstSquare.Y;

            Vector2 squareOffset = new Vector2(Camera.Location.X % 32, Camera.Location.Y % 32);
            int offsetX = (int)squareOffset.X;
            int offsetY = (int)squareOffset.Y;

            for (int y = 0; y < squaresDown; y++)
            {
                for (int x = 0; x < squaresAcross; x++)
                {
                    spriteBatch.Draw(
                        Tile.TileSetTexture,
                        new Rectangle((x * 32) - offsetX, (y * 32) - offsetY, 32, 32),
                        Tile.GetSourceRectangle(myMap.Rows[y + firstY].Columns[x + firstX].TileID),
                        Color.White);
                }
            }

            spriteBatch.End();


Go ahead and run the project, and you should see a 5 by 5 tile map in the upper left corner of the window. Right now, you can't move around, but we have all of the logic we need to support movement built into our drawing code already.

I've broken the firstSquare and squareOffset information out into separate variables (and then broken them down into their X and Y components) to make it easy to see what is happening here. The Camera.Location variable holds the point on the map that should be displayed in the upper left corner of the map display. We need to determine what square on the map this corresponds to, so we divide the camera's X and Y components by the size of a single tile (32 in our case). The result is a vector in map square coordinates that points to the tile that the camera is pointing at. When our camera is at (0,0) (the default) we divide 0 by 32 twice, and still get (0,0), so by default the upper left square in the map will be the square displayed in the upper left corner of the screen.

If we were to move the camera right by 32 pixels (X = 32), the result would be (32 / 32, 0 / 32) or (1, 0). This means the square directly to the right of the upper left square would be the starting square for our display.

Once we know what tile we want to begin drawing with, we need to know where to draw it. Once again, if our camera is at (0,0) we just want to draw our first tile at (0,0). But as our camera moves in increments of less than one tile, we need to shift where we begin drawing the tile further and further off the screen. If we are halfway through a tile to the right (Camera.Location = (16,0)) we want to shift everything we draw 16 pixels to the left so that the right part of the tile shows up in the upper left corner of the screen.

Now we just need a loop to draw each of the tiles we need to display onto the screen. In the SpriteBatch.Draw() call, the first rectangle parameter (new Rectangle((x * 32) - offsetX, (y * 32) - offsetY, 32, 32)) determines where on the screen the tile will be drawn. Each tile we draw is shifted to match it's location relative to the starting tile. This is why we multiply the x and y loop control values by 32. The first tile we draw on a row will by the 0th tile we have drawn, so 0*32 = 0. The second tile on the row will be 1*32=32, so shifted by 32 pixels. Each column works the same way. The offsetX and offsetY values move the tile drawing by the amount we calculated above to account for the camera being between whole tile markers.

The second rectangle (Tile.GetSourceRectangle(myMap.Rows[y + firstY].Columns[x + firstX].TileID)) comes directly from our Tile static class. Here we are using the first square values we calculated at the beginning of the Draw() method in order to look up the row and column information on the map. Note that since we are using a List of MapRow items, we look up the Y coordinate first (the row) and then the X coordinate (the column within the row). This notation is a little backwards from what we are used to, but we will add helper methods to simplify that kind of thing later on.

Moving the Camera

The last thing we are going to do in this initial installment of the tutorial series is to allow the camera to move around a bit. In the Update() method of the Game1 class, add the following before the base.Update() call:

    KeyboardState ks = Keyboard.GetState();
    if (ks.IsKeyDown(Keys.Left))
    {
        Camera.Location.X = MathHelper.Clamp(Camera.Location.X - 2, 0, (myMap.MapWidth - squaresAcross) * 32);
    }

    if (ks.IsKeyDown(Keys.Right))
    {
        Camera.Location.X = MathHelper.Clamp(Camera.Location.X + 2, 0, (myMap.MapWidth - squaresAcross) * 32);
    }

    if (ks.IsKeyDown(Keys.Up))
    {
        Camera.Location.Y = MathHelper.Clamp(Camera.Location.Y - 2, 0, (myMap.MapHeight - squaresDown) * 32);
    }

    if (ks.IsKeyDown(Keys.Down))
    {
        Camera.Location.Y = MathHelper.Clamp(Camera.Location.Y + 2, 0, (myMap.MapHeight - squaresDown) * 32);
    }


Since our Camera has no intelligence built in, we need to make sure we don't scroll off of the map area. This means that we use MathHelper.Clamp to keep the X and Y values within pre-defined ranges. The lower end of the range is self evident: we don't want to go below 0 in either the X or Y coordinate.

The maximum range needs a little explaining. because we will be drawing a 5 by 5 area of the map to the screen, we don't want the camera to get any closer to the edge of the map than 5 squares away. If it did, we would try to draw a square only to run off of the end of the map and cause us problems. We take the size of the map (50 in our case) minus the number of squares we are displaying (5) and multiply this by the size of each tile (32). This means the maximum value we will allow for our Camera's X or Y coordinate is ((50 - 5) * 32), or (45 * 32) or 1440.

Run your project and use the arrow keys to move around on the map. Notice that squares seem to "pop" in on the right and bottom edges. This is because we are always drawing a 5 by 5 tile area, but we are potentially offsetting our drawing calls by 31 pixels in each direction. When we have moved far enough that the camera enters a new square, we stop drawing the old (now off the screen) square and the new fifth tile pops in on the edge. This means that we will want to overdraw our desired map (extending a tile beyond what we actually want to see) and potentially cover up the extra tile parts with a nice screen overlay (or simply let them flow off the sides of the screen)

That's It for This Time

That will wrap up this initial installment of the new Tile Engine Tutorial series. You can download the code for this installment at the link below.

Upcoming entries in the series will expand on the basics we have set up here, and look at things like isometric and hex shaped tiles, multiple layers, and other goodies.




































 

 
 
Site Contents Copyright 2006 Full Revolution, Inc. All rights reserved.
This site is in no way affiliated with Microsoft or any other company.
All logos and trademarks are copyright their respective companies.
RSS FEED