
Introduction
Welcome to our first XNE Game Studio Express Beta 2 Tutorial! In this tutorial, we will be putting together the knowledge we have gained during the previous Tile Engine Tutorials to create a working Tile Engine Game Component that can be easily added to an XNA/GSE game to provide tile mapping functionality.
We'll be expanding on some of the features of our previous tile engine, as well as revising a few things we did before. I'll also provide a "Tile Studio" code generation script to allow us to use Tile Studio to create our tile maps.
What are Game Components?
One of the things that makes creating games with Game Studio Express easier than doing everything from scratch is the concept of reusable Game Components. Game Components are C# Classes that conform to a pre-defined XNA/GSE structure that can be plugged into your project to provide some form of game functionality.
In Beta 1, Game Components worked in a manner similar to Windows Forms controls, where you would drag and drop them from the toolbox onto your game's Designer window. In Beta 2 this design-time functionality has been removed because it proved to be difficult to work with as there was no way to specify the draw order if you had multiple components in your project.
Game Components have their own Update and Draw methods, which are called during the game's Update and Draw methods automatically.
Creating a Component
The first step is to create the project that we will use to develop our component. Since a component can't stand "on it's own", we'll use a test project as a development environment for our component. Start Game Studio Express and select File | New Project. Select "Windows Game (XNA)" from the project types. Give your project a name (Not the name of your component), and click "Ok". You should see the by-now familiar game shell.
Right click on the project in the Solution Explorer at the right edge of the screen and select "Add | New Item...". From the resulting menu, select "Game Component (XNA)". Give your component a name (so you should have something like "tileengine.cs" in the "Name" window) and click on "Add". Right click on your new component in the Solution Explorer and select "View Code" to open your new component's code page.
There are now two types of Game Components. The normal "GameComponent" contains an update routine but doesn't draw anything to the display. There is a new type of game component called DrawableGameComponent that allows for access to the display. Since that will be our primary function, we'll need to change the component type from the default to a DrawableGameComponent. Change the following line:
public partial class TileEngine : Microsoft.Xna.Framework.GameComponent
to
public partial class TileEngine : Microsoft.Xna.Framework.DrawableGameComponent
This won't automatically add our Draw routine to the template, but we can add it in manually later.
We also want to change the namespace our component resides in. It defaults to the name of the game you are working within, but I changed my namespace line to read:
namespace TileEngineComponent
This means I'll need to add "Using TileEngineComponent" to my Game's references.
Assembly References
At the top of our TileEngine.cs file is a collapsed code region called "Using Statements". Click on the little "+" symbol to expand this region. We'll need to add the following references (in addition to what is already there):
using System.IO;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
This will allow us to read and write files (System.IO), allow us to access the content pipeline (Microsoft.Xna.Framework.Content), and allow us to access the display (Microsoft.Xna.Framework.Graphics).
Defining Our Variables
Our Component is really it's own self-contained C# class. As such, we need to define what variables we are going to keep track of inside each instance of our component. This is important to understand, because we could theoretically create an XNA game with multiple Tile Map components on the screen at the same time. They won't have any relationship to each other. They will have their own tilesets, their own maps, their own coordinate system, etc, all running at the same time. We can actually do some nifty things with this, like have our main display showing the map we are playing on while a mini-display with simplified tiles shows the area around us as a "mini-map". More on this kind of stuff later.
Based on our previous Tile Map implementations, I've decided what variables I want to keep track of for my tile map. I'm placing these right after the class's declaration ("partial public class...") right before the class' constructor. Their placement isn't too critical as long as they are inside the class but outside any method declarations.
You'll notice that all of the variables I've declared here are prefixed with m_. This isn't really a necessity, but it is a common convention that member variables of a class are prefixed with m_ (which stands for "member"). This was actually an opportunity to use are really nice feature in the C# IDE under the Refactor menu called "Rename". The Refactor->Rename function searches your code for the hilighted variable/function name and replaces it with the new name you specify. It isn't just a simple search and replace though. It is smart about C# and will replace references to the variable without harming other code (it knows that a local variable in a function that might have the same name isn't the variable you are renaming, for example).
I've also included a simple class definitions to handle tile sets and tile animations, which I describe after the code here. The variables I'll be using are listed below. This can be placed right before the line that says "public TileEngine(Game game)". Each is commented as to what it does:
// The TileSet class holds information on an individual tileset that has
// been loaded for this instance of the Tile Engine.
private class TileSet
{
public Texture2D t2dTexture;
public string sFileName;
public int iVerticalSpacing;
public int iHorizontalSpacing;
public int iTileWidth;
public int iTileHeight;
public int iTilesPerRow;
}
// The TileAnimation class holds all of the information necessary to
// define a simple tile-based animation. A tile-based animation is a
// sequence of consecutive tiles that are shown at the designated
// framerate to product an animation.
private class TileAnimation
{
public int iStartFrame, iFrameCount, iCurrentFrame, iFrameRate;
}
// Local Instance Variables
// the t2dTileSets list holds our tilesets. Each tileset must be the
// same size, and all tiles are also the same size.
private List<TileSet> m_TileSets = new List<TileSet>();
// The taAnimations list holds all animated sequences. When tiles are
// output, they are compared with the animated tile sequences and
// animated appropriately if necessary.
private List<TileAnimation> m_taAnimations = new List<TileAnimation>();
private int m_iTileWidth, m_iTileHeight; // Width and Height of a single tile
private int m_iTileSetCount; // Number of Active Tilesets
private int m_iMapWidth, m_iMapHeight; // Map Width and Height
private int[,] m_iMap, m_iMapTrans, m_iMapObj, m_iMapData;// The currently active game map
private int m_iMapX, m_iMapY, m_iMapSubX, m_iMapSubY; // Current Map Location
private int m_iScreenX, m_iScreenY; // Where we are drawing on the screen
private int m_iScreenWidth, m_iScreenHeight; // How may tiles wide and high are we drawing
private bool m_bDrawBase = true; // Will we draw the Base layer?
private bool m_bDrawTrans = true; // Will we draw the Trans layer?
private bool m_bDrawObj = true; // Will we draw the Obj layer?
private bool m_bDrawFirstRowTiles = true; // Draw Top Row Tiles on Trans/Obj layers?
private SpriteBatch m_spriteBatch; // Sprite Batch object for drawing
private int m_iTileToDraw; // Used in Drawing loop to hold current tile
private int m_iXLoc, m_iYLoc; // Used in Drawing loop to track tile locations
private ContentManager m_content; // Content Manager Instance
What are these Lists and Classes for?
The List is a .NET Collection object. It can be used to handle strongly typed lists of objects much like an array would be. Unlike an array, it is very easy to expand the list (using the .Add method). List items can be accessed by index number just like an array and could potentially be sorted if we wanted to (we won't be doing that as the index order will be important when we reference tiles from the tilesets later on).
One of the features we are going to add to the component that our stand-alone engines didn't have is the ability to handle multiple tilesets. As such, we need a way to store information about those tilesets. The "TileSet" class holds this information and is populated by the "AddNewTileset" method we will look at below. One of the lists we've defined will be used to hold instances of our TileSet class. You can fit a LOT of tiles in at 2048x2048 texture (that is the maximum recommended size of an XNA texture based on graphics card limitations.) If we are using 48x48 tiles like we were in our previous engine (the tile size would actually be 2016x2016 so that tiles line up evenly with the end of the image) we would have 1764 tiles per tileset, and 48x48 is a very large tile size.
In our code, we will assume that there are a maximum of 9999 tiles in a single tileset. Tile #10000 is the first tile on Tile Set #2, Tile #20000 is the first tile on Tile Set #3, etc.
Our second list is used to define tile animations. You will recall in our old tile engine that we defined tile 108 to start a 7 frame animation. Obviuosly we don't want to hard-code something like this into our Component, so we will allow the game developer to designate tile animations by adding them to the engine through a method that we'll create later. I ended up having to use a List of Classes instead of a List of Structs because the Structs were uneditable when in a list. (Don't know why, but that's the way it works!)
Properties
If you've ever developed a Windows Forms application you'll know that once you drag something like a Command Button from the toolbox to your designer, you have a bunch of settable properties in the properties window on the lower right. Things like Caption, Width, Height, Left, Top, Visible, etc.
We will be creating these same kinds of properties for our component. As of Beta 2, we no longer have a Design-Time interface to our components, so we no longer drag and drop components onto our game's Designer, but our properties are accessed through code just like they were in Beta 1.
We could have made our variables above public instead of private and allowed the game developer to set them directly, but there are some good reasons for not doing it that way. What is this public/private stuff anyway? A private variable (or method for that matter) can only be accessed by other members of the class. A public variable (or method) is accessable by the rest of our game. Anything we want to expose upwards to our game should be public, while the internal workings of our class should be private. There is also a third type of declaration, protected, which are visible to the class and any subclasses.
In general, we want our properties to be public so that the game using the component can interface with them, while we make our member variables private so that we know that nothing outside of our own code can modify them directly.
When we define properties for our component they are exposed just like the properties of a Command Button or Text Box. We can give them friendly names and set our internal variables accordingly. Perhaps the best reason to use properties instead of direct variable acces though is that we can take actions when a property is get or set.
A property is defined much like a function except that it has two special sections. The "get" section is executed whenever something reads the property. The "set" section is executed whenever the property is set. Lets look at an example property here:
(We're going to use this propertly later, but don't add it to your code just yet... I'll give you the whole section to add at one time later):
public int Left
{
// The Left property determines the left pixel position of the
// tile engine drawing area on the display.
get { return this.m_iScreenX; }
set { this.m_iScreenX = value; }
}
We can see that when something reads MyCoolTileMap.Left, the property simple returns the value of m_iScreenX (the keyword this references the executing instance of the object). If we were to say "MyCoolTileMap.Left = 50;" the m_iScreenX value would be set to the value passed in (50 in this case).
The cool part here is that get and set are functions on their own. We could do just about anything when a property is get or set. You may not use this feature in the get portion of a property too often, but it can be really, really useful in sets. You can check for out of range values, take other actions based on the value being set, etc.
Here is the full set of properties we will be using for our Tile Engine Component. These can be placed right after the variable declarations above:
public int Left
{
// The Left property determines the left pixel position of the
// tile engine drawing area on the display.
get { return this.m_iScreenX; }
set { this.m_iScreenX = value; }
}
public int Top
{
// The Top property determines the top pixel position of the
// tile engine drawing area on the display.
get { return this.m_iScreenY; }
set { this.m_iScreenY = value; }
}
public int Width
{
// The Width property determines how many tiles wide will be
// drawn by the tile engine. Note that this property is in TILES
// and not in PIXELS
get { return this.m_iScreenWidth; }
set { this.m_iScreenWidth = value; }
}
public int Height
{
// the Height property determines how many tiles high will be
// drawn by the tile engine. Note that this property is in TILES
// and not in PIXELS
get { return this.m_iScreenHeight; }
set { this.m_iScreenHeight = value; }
}
public int TileWidth
{
// Determines the width of an individual tile in pixels.
get { return this.m_iTileWidth; }
set { this.m_iTileWidth = value; }
}
public int TileHeight
{
// Determines the height of an individual tile in pixels.
get { return this.m_iTileHeight; }
set { this.m_iTileHeight = value; }
}
public int MapX
{
// Determines the X map coordinate. X=0 is the left-most tile on
// the map. The X coordinate represents the X value of the left-most
// displayed map tile.
get { return this.m_iMapX; }
set { this.m_iMapX = value; }
}
public int MapY
{
// Determines the Y map coordinate. Y=0 is the top-most tile on
// the map. The Y coordinate represents the Y value of the left-most
// displayed map tile.
get { return this.m_iMapY; }
set { this.m_iMapY = value; }
}
public int MapWidth
{
// The MapWidth property is read-only since it is determined
// by the map that is loaded.
get { return this.m_iMapWidth; }
}
public int MapHeight
{
// The MapHeight property is read-only since it is determined
// by the map that is loaded.
get { return this.m_iMapHeight; }
}
public bool BaseVisible
{
get { return this.m_bDrawBase; }
set { this.m_bDrawBase = value; }
}
public bool TransVisible
{
get { return this.m_bDrawTrans; }
set { this.m_bDrawTrans = value; }
}
public bool ObjVisible
{
get { return this.m_bDrawObj; }
set { this.m_bDrawObj = value; }
}
public bool DrawFirstRowTilesOnTransLayers
{
get { return this.m_bDrawFirstRowTiles; }
set { this.m_bDrawFirstRowTiles = value; }
}
All pretty standard stuff. Like most Windows Forms controls, the Left and Top properties determine where the tile engine will appear on the screen. The Width and Height are different in that they are in Tiles and not Pixels.
Also note the "DrawFirstRowTilesOnTransLayers" property. This is linked to the m_bDrawFirstRowTiles. If this value is false the first row of tiles on our tileset will NOT be drawn if they appear on the Transition and Object layers (the two layers drawn with transparency turned on). If it is set to true (the default), all tiles will be drawn on all layers. If we are going to load a map from our previous Tile Engine Tutorials, we'll need to set this to False but in general (especially if you use the Tile Studio export script at the bottom of this tutorial) you will probably leave this set to true.
This behavior is actually a hold over from our previous tutorials. In them we didn't draw the first row tiles on the upper layers because we used those tiles for reference while making our maps. This isn't so important anymore, but the option is here in case you want to use it.
Stuff That's Different
It should be noted that one thing we aren't doing with this tile engine component is worring about displaying an avatar. That will be left for the game developer to do, as we don't want to limit the functionality of our Tile Engine by building in a single type of player avatar. There are a few good sprite components out there that can be used to do this.
Similarly we won't be using the "walkability" data included in our maps, as we are concerned only with displaying the map on the screen. Again, it will be up to the developer to determine where players can and can't go and what happens to them when they get there. They will have access to the underlying map data, however, so information that is "baked into" the maps Data layer (renamed from Walkable) can be used by the game logic.
Startup Code
Game Components are now provided with an "Initialize" method that will be called automatically when the instance is created. We don't need to do a whole lot in our initialization, but we do need to set a couple of values. Update the initialize method to look like the following::
public override void Initialize()
{
// Start off disabled/non-visible since we need to load tilesets and
// maps before activating.
this.Enabled = false;
this.Visible = false;
// Set the tileset count to 0
m_iTileSetCount = 0;
m_content = new ContentManager(Game.Services);
base.Initialize();
}
One of the big changes in Beta 2 is that game components are provided with the Enabled property. If Enabled is true, the component's Update method will be called as part of the game's update routine. If it is false, the Update method won't be called.
Similarly, DrawableGameComponents get a Visible property. Just line Enabled, Visible determines if the component's Draw method will be called as part of the game's Draw cycle.
In both cases we want to have these values disabled when we start our class. We don't want to try to draw anything before we have loaded a tileset and map, which is up to the game coder to do first.
Next, we set iTileSetCount to 0, indicating that we haven't loaded any tilesets yet.
After that, we create our instance of ContentManager (the Content Pipeline) so that we can access the game's content from our component.
Finally, we call our base class Initialize method for anything else the DrawableGameComponent does behind the scenes.
Accessing the Display
Another big change in Beta 2 is the way components get access to the Graphics Device. Now, the game will call the LoadGraphicsContent method of the Component with a pre-defined GraphicsDevice object available. We can use this to set up our SpriteBatch object. Create the LoadGraphicsContent method as follows:
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
{
m_spriteBatch = new SpriteBatch(GraphicsDevice);
}
}
The "loadAllContent" boolean is passed to determine if we are loading for the first time (and thus loading everything including automatically managed resources) or if we are reloading content from a loss of the display. Any automatically managed content will be handled for us automatically, but anything we load as manually managed content will need to be reloaded here.
Maps and TileSets
Lets add some code now to load maps and tilesets into our component. It doesn't really matter which we do first, so lets start with maps.
This routine is slightly modified from our Tutorial 4 code to load the map from disk. In fact, maps saved with the Tutorial 4 Editor should load just fine with this routine:
public void LoadMap(string sFileName)
{
// Load a map from a text file. This is the format used in the
// old Tile Engine Tutorials.
StreamReader srReader;
try
{
srReader = File.OpenText(sFileName);
int iReadMapVersion = Convert.ToInt32(srReader.ReadLine());
m_iMapHeight = Convert.ToInt32(srReader.ReadLine());
m_iMapWidth = Convert.ToInt32(srReader.ReadLine());
m_iMap = new int[m_iMapHeight, m_iMapWidth];
m_iMapTrans = new int[m_iMapHeight, m_iMapWidth];
m_iMapObj = new int[m_iMapHeight, m_iMapWidth];
m_iMapData = new int[m_iMapHeight, m_iMapWidth];
for (int y = 0; y < m_iMapHeight; y++)
{
for (int x = 0; x < m_iMapWidth; x++)
{
m_iMap[y, x] = Convert.ToInt32(srReader.ReadLine());
m_iMapTrans[y, x] = Convert.ToInt32(srReader.ReadLine());
m_iMapObj[y, x] = Convert.ToInt32(srReader.ReadLine());
m_iMapData[y, x] = Convert.ToInt32(srReader.ReadLine());
}
}
srReader.Close();
}
catch
{
throw;
}
}
Here we attempt to open and read the passed filename. If we run into any problems we'll throw the exception back up to the caller to be handled (so LoadMap should always be called withing a try...catch block so we can deal with files not existing, etc.)
Our map data (in this format) is stored as a line representing the map file version, followed by the map height and then the map width. Each group of 4 lines after that represent a single map square, with each of the layers represented by a line (Map, Trans, Obj, and Data).
To load our tilesets, we need a new method:
public void AddTileset(string sFileName, int iTileWidth, int iTileHeight, int iHorizontalSpacing, int iVerticalSpacing)
{
// Load a new tileset from a file and add it to the tileset list.
// Each tileset *must* be the same size, and all tiles must be the
// same size.
TileSet NewTileSet = new TileSet();
try
{
NewTileSet.t2dTexture = m_content.Load<Texture2D>(sFileName);
NewTileSet.sFileName = sFileName;
NewTileSet.iTileWidth = iTileWidth;
NewTileSet.iTileHeight = iTileHeight;
NewTileSet.iHorizontalSpacing = iHorizontalSpacing;
NewTileSet.iVerticalSpacing = iVerticalSpacing;
NewTileSet.iTilesPerRow = NewTileSet.t2dTexture.Width /
(NewTileSet.iTileWidth + NewTileSet.iHorizontalSpacing);
m_TileSets.Add(NewTileSet);
m_iTileSetCount++;
}
catch
{
throw;
}
}
Here we set up a new TileSet object and use the Content Manager to load the texture. Again, we should call this method from inside a Try...Catch block because we could end up with missing files, etc. We set the variables that were passed in from the developer and then use the .Add method of the List to add the tileset to our collection for use on the map.
A few interesting things to note about our tilesets:
- Our tileset images don't need to be the same size. The parameters of the tileset are set up on a per-tileset basis. We can also have tilesets with and without space between the tiles (I added support for this because, in my search for demo tilesets on the internet about a 50/50 mix of them had and didn't have space between the tiles.)
- Technically our tiles don't have to be the same size. If we had a tileset of normal 48x48 tiles, a tileset of extra wide 96x48 tiles, a set of extra tall 48x96 tiles, and a set of large 96x96 tiles, we could use them all, but we would have to be careful about creating our map. We would want to place the tile in the upper left corner of the multiple-tile block and then set the other tiles in the block to -1 (we don't draw negative numbered tiles). We would also have to "overdraw" our map on the top and left edges since once the block went out of the drawing range the extended parts of the block that might still be on the screen would disappear as well.
In general though you would probably want to keep all of your tilesets the same size with the same tile size inside them for simplicity's sake. Also, it should be noted that the Tile Studio export script at the end of this tutorial only works for single tileset maps because of the way tiles are output by Tile Studio. It would be possible to create a new export script and a new map reader method that would import more complex maps if we need to do so.
Moving Around on the Map
The developer will need some way to position the map so that the area the developer wants is displayed. Currently the MapX and MapY properties could be used to do this in whole tile chunks, but we'll create a couple of functions to help make things easier to work with:
public void SetMapLocation(int x, int y, int subx, int suby)
{
// Sets the current map location, providing the X,Y coordintate
// as well as the Sub-Tile X and Y positions.
m_iMapX = x;
m_iMapY = y;
m_iMapSubX = subx;
m_iMapSubY = suby;
}
public void ScrollByPixels(int x, int y)
{
// Move the map by X pixels horizontally and Y pixels vertically.
// Accounts for moving off of a tile and onto another as well as
// moving off of the end of the map by looping around to the other
// side.
m_iMapSubX += x;
m_iMapSubY += y;
while (m_iMapSubX >= m_iTileWidth)
{
m_iMapSubX -= m_iTileWidth;
m_iMapX++;
}
while (m_iMapSubY >= m_iTileHeight)
{
m_iMapSubY -= m_iTileHeight;
m_iMapY++;
}
while (m_iMapSubX < 0)
{
m_iMapSubX += m_iTileWidth;
m_iMapX--;
}
while (m_iMapSubY < 0)
{
m_iMapSubY += m_iTileHeight;
m_iMapY--;
}
if (m_iMapX >= m_iMapWidth)
{
m_iMapX -= m_iMapWidth;
}
if (m_iMapX < 0)
{
m_iMapX += m_iMapWidth;
}
if (m_iMapY >= m_iMapHeight)
{
m_iMapY -= m_iMapHeight;
}
if (m_iMapY < 0)
{
m_iMapY += m_iMapHeight;
}
}
The first function, SetMapLocation, is fairly simple. It takes four parameters and sets the corresponding four local variables to the passed values. We would probably use this function at the start of a level or something like that to initially position the screen.
The second function scrolls the map by the specified number of pixels horizontally and vertically. It accounts for moving between tiles and moving off of the edge of the map by wrapping around to the opposite edge (that's what all of the while and if blocks are for). That's one important thing to keep in mind about our tile engine. The map will wrap in all directions. We'll use that feature to our advantage in some situations and need to keep it in mind in others when designing our maps.
Altering the Map Data
We'll need to allow the game developer some way of editing the data on the map. Blocks may get destroyed, doors opened, etc. For this, we add a few helper functions:
public void EditMap(int x, int y, int iBase, int iTrans, int iObj, int iData)
{
m_iMap[y, x] = iBase;
m_iMapTrans[y, x] = iTrans;
m_iMapObj[y, x] = iObj;
m_iMapData[y, x] = iData;
}
public int GetMapBase(int x, int y)
{
return m_iMap[y, x];
}
public int GetMapTrans(int x, int y)
{
return m_iMapTrans[y, x];
}
public int GetMapObj(int x, int y)
{
return m_iMapObj[y, x];
}
public int GetMapData(int x, int y)
{
return m_iMapData[y, x];
}
Using these functions we can examine any of the layers of the map and also set the values of the map layers at any tile.
Animated Tile Sequences
You'll recall that we have a second List collection declared in our variable area for handling Tile animations. By a tile animation, I mean that we will be able to define ranges of tiles that are cycled, in order, to produce an animation effect on a tile.
We will need some helper functions to support our animated tiles, so lets do those now:
public void AddTileAnimation(int iStartFrame, int iFrameCount, int iFrameRate)
{
// Define a new tileset animation. Tiles in an animation must
// be consecutive and must all reside on the same tileset.
TileAnimation thisAnimation = new TileAnimation();
thisAnimation.iStartFrame = iStartFrame;
thisAnimation.iFrameCount = iFrameCount;
thisAnimation.iFrameRate = iFrameRate;
thisAnimation.iCurrentFrame = 0;
m_taAnimations.Add(thisAnimation);
}
This first function allows the game developer to specify an animation sequence. For our purposes, all tiles in an animation must follow a couple of rules:
- All tiles must be consecuitive in the order the animation will be played
- All tiles must be on the same TileSet
To define an animation sequence, we need a starting frame (iStartFrame), the number of frames in the animation (iFrameCount), and how many update cycles each frame is displayed for (iFrameRate). Setting iFrameRate to a higher value makes the animation play more slowly (if you use the default Update cycle of 1/60th of a second, setting iFrameRate to 60 should give you 1 frame per second of animation).
Our function simply takes all of this information and pops it into the taAnimations collection we've created so we can reference it later to animate our tiles. In order to do that animation, lets look at the next two helper functions:
private bool IsAnimatedTile(int iTile)
{
foreach (TileAnimation thisAnimation in m_taAnimations)
{
if (thisAnimation.iStartFrame == iTile)
{
return true;
}
}
return false;
}
private int GetAnimatedFrame(int iTile)
{
foreach (TileAnimation thisAnimation in m_taAnimations)
{
if (thisAnimation.iStartFrame == iTile)
{
return thisAnimation.iStartFrame + (thisAnimation.iCurrentFrame / thisAnimation.iFrameRate);
}
}
return 0;
}
We'll use these two functions during our Draw routine to properly draw animated tiles. The first function (IsAnimatedTile) returns true if the passed tile number is the starting frame of any of our defined animations. It is important to note that we only check the starting frames. If we have an animation sequence of tiles 51, 52, 53, 54, and 55, any map square containing tile 51 will be animated. Tiles 52-55 would be displayed as static tiles.
The second function gets the current animation frame tile number based on the animation list. We aren't modifying the map data, but rather keeping track of the frame we should be on and returning the appropriate tile for the current frame.
Next, we need something we can call during the Update routine to update our animation frames:
private void UpdateAnimationFrames()
{
for (int x = 0; x < m_taAnimations.Count; x++)
{
m_taAnimations[x].iCurrentFrame++;
if (m_taAnimations[x].iCurrentFrame > ((m_taAnimations[x].iFrameCount * m_taAnimations[x].iFrameRate)
+ (m_taAnimations[x].iFrameRate - 1)))
{
m_taAnimations[x].iCurrentFrame = 0;
}
}
}
Nothing too complicated... Every time UpdateAnimationFrames is called, we loop through each of the Animations (we can't use foreach here because foreach creates a "copy" of the object from the collection and it can't be modified). We increment the current frame for the animation and check to see of we should loop around to the beginning.
One More Helper Function
I know we've done a whole lot of stuff without anything visible on the display yet. We need just one more helper function before we can implement our Draw method, and here it is:
private Rectangle GetTileRectangle(int iTileNumber)
{
// Returns a rectangle representing the location on the tileset that
// contains the requested tile.
// The 10000's digit determines the tileset number. The remainder
// determines the tile within the set, so seperate out the remainder
// to determine where to pull the rectangle from.
int iTile = iTileNumber % 10000;
int iTileSet = iTileNumber / 10000;
// Return a rectangle representing a location of a tile on the tileset
return new Rectangle(
(iTile % m_TileSets[iTileSet].iTilesPerRow) * m_TileSets[iTileSet].iTileWidth,
(iTile / m_TileSets[iTileSet].iTilesPerRow) * m_TileSets[iTileSet].iTileHeight,
m_TileSets[iTileSet].iTileWidth,
m_TileSets[iTileSet].iTileHeight
);
}
We'll use this when drawing a tile to get a source rectangle for the SpriteBatch.Draw command. We are using the 10000's place digits to specify tilesets, so we drop that portion of the number (with the % operator) to get our "tile within the tileset" number. From there it is a simple matter of using the tile width and height to create a rectangle just like we did in the previous Tile Engine tutorials.
Lets Draw Something!
Here is the whole Draw routine. It is very similar to the Draw method from our previous tutorials. Some of the variable names have changed slightly, and we add a few more features such as:
- Conditional Layer Drawing : Before each drawing segment, we check the bDrawXXX variables to see if we are drawing that particular layer.
- Wrap Arounds : This version of the engine wraps the tiles around from the beginning of the map if there aren't enough tiles to fill the screen. We can actually use this to great advantage as you will see in the examples later on. We can compensate for this by limiting how far we scroll the map in our game code and/or by padding some extra tiles around the outside edges.
- Animated Tiles : We check the tiles we are drawing to see if they are animated. We can now animate tiles on any layer (in the Tutorial 4 Engine we were only animating tiles on the first layer, and only a pre-defined hard coded set of tiles).
- Negative Tiles : We are now checking to make sure that the tiles we are drawing are not negative. Tile Studio uses -1 to represent an area where there is no tile. I'll be providing a Tile Studio TSD file to let maps be exported from Tile Studio into the format we've been using.
- bDrawFirstRowTiles : This is a hold-over from the previous tutorials. In those tutorials we decided that we wouldn't draw tiles on the first row of the tileset on the Trans and Object layers. The reasons we were doing this go out the window when we have a nicer editor like Tile Studio. I've provided the option here just in case we want to load maps from the old editor.
Now, on to the method itself:
public override void Draw(GameTime gameTime)
{
m_spriteBatch.Begin(SpriteBlendMode.None);
// Draw the base layer of the map
if (m_bDrawBase)
{
for (int y = 0; y < m_iScreenHeight; y++)
{
for (int x = 0; x < m_iScreenWidth; x++)
{
m_iXLoc = x + m_iMapX;
m_iYLoc = y + m_iMapY;
// Account for map wrap-arounds
if (m_iXLoc >= m_iMapHeight)
{
m_iXLoc -= m_iMapWidth;
}
if (m_iXLoc < 0)
{
m_iXLoc += m_iMapWidth;
}
if (m_iYLoc >= m_iMapHeight)
{
m_iYLoc -= m_iMapHeight;
}
if (m_iYLoc < 0)
{
m_iYLoc += m_iMapHeight;
}
m_iTileToDraw = m_iMap[m_iYLoc, m_iXLoc];
if (IsAnimatedTile(m_iTileToDraw))
{
m_iTileToDraw = GetAnimatedFrame(m_iTileToDraw);
}
if (m_iTileToDraw >= 0)
// Only draw tiles > 0 since TileStudio uses -1
// to indicate an empty tile.
{
// Draw the tile. We divide the tile number by 10000 to
// determine what tileset the tile is on (0-9999=tileset 0,
// 10000-19999=Tileset 2, etc)
m_spriteBatch.Draw(m_TileSets[(m_iTileToDraw / 10000)].t2dTexture,
new Rectangle(((x * m_iTileWidth) + m_iScreenX) - m_iMapSubX,
((y * m_iTileHeight) + m_iScreenY) - m_iMapSubY,
m_TileSets[(m_iTileToDraw / 10000)].iTileWidth,
m_TileSets[(m_iTileToDraw / 10000)].iTileHeight),
GetTileRectangle(m_iTileToDraw),
Color.White);
}
}
}
}
m_spriteBatch.End();
m_spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
// Draw transitions and Objects layers of the map
for (int y = 0; y < m_iScreenHeight; y++)
{
for (int x = 0; x < m_iScreenWidth; x++)
{
Rectangle recDest = new Rectangle(((x * m_iTileWidth) + m_iScreenX) - m_iMapSubX,
((y * m_iTileHeight) + m_iScreenY) - m_iMapSubY,
m_TileSets[m_iTileToDraw / 10000].iTileWidth,
m_TileSets[m_iTileToDraw / 10000].iTileHeight);
m_iXLoc = x + m_iMapX;
m_iYLoc = y + m_iMapY;
if (m_iXLoc >= m_iMapHeight)
{
m_iXLoc -= m_iMapWidth;
}
if (m_iXLoc < 0)
{
m_iXLoc += m_iMapWidth;
}
if (m_iYLoc >= m_iMapHeight)
{
m_iYLoc -= m_iMapHeight;
}
if (m_iYLoc < 0)
{
m_iYLoc += m_iMapHeight;
}
if (m_bDrawTrans)
{
m_iTileToDraw = m_iMapTrans[m_iYLoc, m_iXLoc];
if (IsAnimatedTile(m_iTileToDraw))
{
m_iTileToDraw = GetAnimatedFrame(m_iTileToDraw);
}
if (m_iTileToDraw >= 0 && ((m_iTileToDraw >
m_TileSets[m_iTileToDraw / 10000].iTilesPerRow) || m_bDrawFirstRowTiles))
{
m_spriteBatch.Draw(m_TileSets[m_iTileToDraw / 10000].t2dTexture,
recDest, GetTileRectangle(m_iTileToDraw),
Color.White);
}
}
if (m_bDrawObj)
{
m_iTileToDraw = m_iMapObj[m_iYLoc, m_iXLoc];
if (IsAnimatedTile(m_iTileToDraw))
{
m_iTileToDraw = GetAnimatedFrame(m_iTileToDraw);
}
if (m_iTileToDraw >= 0 && ((m_iTileToDraw >
m_TileSets[m_iTileToDraw / 1000].iTilesPerRow) || m_bDrawFirstRowTiles))
{
m_spriteBatch.Draw(m_TileSets[m_iTileToDraw / 10000].t2dTexture,
recDest, GetTileRectangle(m_iTileToDraw),
Color.White);
}
}
}
}
m_spriteBatch.End();
base.Draw(gameTime);
}
Really there isn't much difference here from what we've been doing all along, so I'll point you to the previous Tile Engine tutorials for a more detailed explanation of the tile drawing routine.
Are We Done Yet?
Almost! we just need to put the call to update our animation frames into the Update routine:
public override void Update(GameTime gameTime)
{
UpdateAnimationFrames();
base.Update(gameTime);
}
But... But... It Still Doesn't Do Anything!
Now that we've finished our component, we need to actually do something with it. Remember that we created a game shell to work on our component in? Lets use that to do some quick testing:
Open the code view for the game you created as a shell for building the Tile Engine component. At the top, add:
using TileEngineComponent;
This will provide us access to the component from our game.
Next, we need to set up a few resources for Content Manager. Using the Solution Explorer, right-click on project and select Add | New Folder. Call the folder Content. Open Windows explorer and put your Tileset image in the folder, along with the text map. I have made these resources available here for simplicity.
Now, back in solutions explorer, right click on the Content folder and select "Add Existing Item". Select All File Types and add the tileset image and the map file.
For the map file (which won't be managed by Content Manager) select "Copy Always" in the "Copy to Output Directory" property.
Next, we'll declare our instance of the Tile Engine. This can be placed right below the existing lines that declare the GraphicsDeviceManager and ContentManager objects:
TileEngine tilemap;
Next, we'll modify our Initialize method to set up the instance:
protected override void Initialize()
{
tilemap = new TileEngine(this);
this.Components.Add(tilemap);
tilemap.Left=0;
tilemap.Top=0;
tilemap.Width=18;
tilemap.Height=14;
tilemap.TileWidth=48;
tilemap.TileHeight=48;
tilemap.DrawFirstRowTilesOnTransLayers=false;
base.Initialize();
}
Here we set up our component. this.Components.Add lets the game know that this is an active component that should be considered for updates and draws. We'll be placing the component so it covers the whole of the default windowed mode screen (0,0 and 18 tiles wide by 14 tiles high with our 48x48 pixel tiles).
Next, during our LoadGraphicsContent method, we can load the resources we need to make the component work:
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
{
// TODO: Load any ResourceManagementMode.Automatic content
tilemap.LoadMap(@"content\map000.txt");
tilemap.AddTileset(@"content\fulltileset_alpha", 48, 48, 0, 0);
tilemap.AddTileAnimation(108, 7, 4);
tilemap.Enabled = true;
tilemap.Visible = true;
}
// TODO: Load any ResourceManagementMode.Manual content
}
Guess what? That's all we need to do! If you run the project now you should have a tile map displayed on the screen.
We can add the following to the Game's Update method to simply scroll the map during each update:
tilemap.ScrollByPixels(2, 1);
Download the Component
I've made the final TileEngine.cs file available as a download.
What Else Can I Do?
I've prepared a number of sample projects using the Tile Engine Component. It doesn't need to be just for RPG games! None of these samples are anything close to fleshed out games. They don't even include input support, but they demonstrate how to set up the tile engine component and put it to use.
(Note: I have the first demo up here now, and more will follow in the future)
Scrolling Background Demo
This simple project demonstrates how easy it is to create a nice effect using our newly created Tile Engine Component. The project consists of two instances of the Tile Engine Component. The settings for each of the instances are nearly identical. They are both placed at 0,0 on the screen. They are both set to a width of 2x1 tiles.
The first instance loads a single tile texture with a tile size of 1920x480 pixels and the second loads a single tile image that is 1680x480 (stars.dds) which is mostly transparent.
The TileEngine objects are initialized in the game's Initialize method, and then set up in LoadGraphicsContent. The Update routine simply scrolls both tilemaps at different rates.
Note the DrawOrder property of the components. The numbers themselves aren't important, but lower numbered components are drawn to the screen before higher numbered components, so in this case our mostly transparent starfield has a higher draw order than the background image.
The overall effect is a background image that scrolls at one rate with an overlayed starfield that scrolls at a faster rate.

Tile Studio
I've created a simple Tile Studio Code Generation Script to export Tile Studio maps in the map format we've been using. It's got a few limitations though. Using Tile Studio based maps you can only use one Tile Set, and you can't set anything on the MapData layer. Here is the TileStudio script:
; --------------------------------------------------------
; Tile Studio Script for XNA Tile Engine Component
; from www.xnaresources.com
; --------------------------------------------------------
#file <ProjectName>.map
#tileset
#map
1\n<MapHeight>\n<MapWidth>\n
#mapdata
<TSBackTile>\n<TSMidTile>\n<TSFrontTile>\n<MapCode>\n
#end mapdata
#end map
#end tileset
#end file