

Note: This tutorial has been updated for Beta 2 of Game Studio Express
IntroductionIn our first Tile Set Tutorial we put together a simple Tile-Based map engine that you might find in a simple RPG game. At the completion of the tutorial, we had a tile map composed of 48x48 pixel tiles that could be navigated using the arrow keys on the keyboard.
In this, the second tutorial in this series, we will spruce up the tile engine a little, expanding on the concepts from the first tutorial. The goals for part two are:
-
Upgrade our Tile handling so we don't have to deal with dozens of individual graphics files
-
Make the map scroll smoothly instead of jumping a full tile at one time
-
Overlay a "Game Screen" onto the map
|
|
Improved Tile HandlingAs you can imagine, as your map gets more detailed, you will end up having a LOT of tiles. It would quickly get out of control to manage all of your tiles as individual image files, so we need to come up with a better way.
We will stick with our four basic tiles for this tutorial, Grass, Water, Rock, and Dirt. However, I've also added "edge" tiles to transition between each of the four types. This increases the number of tiles we need to make a decent looking map by quite a bit.
Using your favorite image editor, create an image that is 576x480 pixels. This will be a single DirectX surface that we will use to hold all of our tiles. We will programatically disect the image when drawing tiles so that we use it in 48x48 pixel pieces just as before.
Depending on your number of base tiles, the number of "transition" tiles you need to create may be quite large. For each pair of tile types you wish to transition between, you will need 12 transition tiles. Creating all of this artwork is time consuming, and I opted to have someone who can draw do it for me!.
My resulting Tile Set image looks like this (Note that there are some tiles here we won't be using yet, and only the Grass/Road and Grass/Water sets have all the corner pieces we need:

As before, we will need to add this resource to our project, so use Solution Explorer to create a "content" and "content\textures" folder and then use Windows Explorer to place the texture file in the textures folder. Right click on the Textures folder in Solution Explorer and select Add -> Existing Item and add the tileset image. Next, we will need to modify our declarations somewhat.
First, we will be removing our t2dTiles array, since we will be using a single image now. In your existing Tile Set Engine Tutorial project, remove the following from the variable declarations beneath the Game1 constructor:
// An array of "Texture2D" objects to hold our Tile Set
Texture2D[] t2dTiles = new Texture2D[4];
Next, we will need to add the definition for our Tile Set image and a few variables that describe our Tile Set that we will use later to perform calculations when drawing the map:
Texture2D t2dTileSet;
// Tileset Size Information
int iTileSetWidth = 576;
int iTileSetHeight = 480;
int iTileSetXCount = 12;
int iTileSetYCount = 10;
Initialization
We'll update our Initialization method to resize the display window. We're doing this because in Beta 1 the display window defaulted to 640x480, and Beta 2 defaults to a larger window. Rather than update all of the graphics content for these tutorials as I convert them to Beta 2, we are simply going to resize back to 640x480. Edit your Initialize method so it looks like this:
protected override void Initialize()
{
// TODO: Add your initialization logic here
this.graphics.PreferredBackBufferHeight = 480;
this.graphics.PreferredBackBufferWidth = 640;
this.graphics.ApplyChanges();
base.Initialize();
}
Here we tell XNA what resolution we want and use ApplyChanges to change the window the that size.
Loading Resources
Now we need to update our LoadGraphicsContent method to fill our new Tile Set image and take out the individual tile images. Modify your LoadGraphicsContent method so it looks like this:
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
{
// TODO: Load any ResourceManagementMode.Automatic content
t2dTileSet = content.Load<Texture2D>(@"content\textures\fulltileset");
spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
}
// TODO: Load any ResourceManagementMode.Manual content
}
We now have an image in memory that contains all of our tiles on a single Texture. The next step is to modify our Draw method to reflect the new way we are storing our tiles. Locate your existing map drawing portion of the Draw method and replace it with the following:
// Draw the map
for (int y = 0; y < iMapDisplayHeight; y++)
{
for (int x = 0; x< iMapDisplayWidth; x++)
{
int iTileToDraw = iMap[y + iMapY, x + iMapX];
Rectangle recSource = new Rectangle((iTileToDraw % iTileSetXCount) * iTileWidth,
(iTileToDraw / iTileSetXCount) * iTileHeight,
iTileWidth, iTileHeight);
spriteBatch.Draw(t2dTileSet,
new Rectangle(((x*iTileWidth)+iMapDisplayOffsetX),
((y*iTileHeight) + iMapDisplayOffsetY),
iTileWidth, iTileHeight),
recSource,
Color.White);
}
}
The loop here is still the same, however we have added two new lines and modified the spriteBatch.Draw call to use the new information generated above. For clarity's sake, first we determine the tile number we are going to use and store it in an integer variable called iTileToDraw. This is done the same way we were looking up the tile previously. We are using the constants we defined earlier to create a new Rectangle object that represents where on the source image (our Tile Set) we want to draw from.
Since we have set up our tiles in a 12x10 Grid, we can use the % operator (returns the remainder of an integer division) to calculate the location of the tile we want within the Tile Set.
Finally, the spriteBatch.Draw method can be called with different parameters (called overloading) than what we used last time around. This time, instead of supplying a width and height after the destination rectangle, we are providing the entire rectangle we wish to copy from (recSource).
Of course, we will need a new map array to show off our fancy tiles... Replace your iMap array declaration with this:
// Our simple integer-array based map
int[,] iMap = new int[iMapHeight, iMapWidth] {
{ 0, 0,27,72,72,72,72,26,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0,30,25,25,25,25,31,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 0, 0, 0,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{12,12,12,12,12,12,12,12,23, 1,22,12,12,12,12,17, 0, 0, 0, 0},
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,14, 0, 0, 0, 0},
{13,13,13,13,13,13,13,13,21, 1,20,13,13,21, 1,14, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 0, 0, 0,15, 1,14, 0, 0,15, 1,14, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 0, 0, 0,15, 1,14, 0, 0,15, 1,14, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 0, 0, 0,15, 1,14, 0, 0,15, 1,22,12,12,12,12},
{ 0, 0, 0, 0, 0, 0, 0, 0,15, 1,14, 0, 0,15, 1, 1, 1, 1, 1, 1},
{24,29, 0, 0, 0, 0, 0, 0,15, 1,14, 0, 0,18,13,13,13,13,13,13},
{72,34,24,24,29, 0, 0, 0,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{72,72,72,72,34,29, 0, 0,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{72,72,72,72,72,26, 0, 0,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{72,72,72,72,72,26, 0, 0,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{72,72,72,72,72,34,29, 0,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{72,72,72,72,72,72,26, 0,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{72,72,72,72,72,72,26, 0,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{72,72,72,72,72,72,26, 0,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{72,72,72,72,72,72,26, 0,15, 1,14, 0, 0, 0, 0, 0, 0, 0, 0, 0},
};
All of this has the effect of pulling out a 48x48 piece of our Tile Set for each tile on the screen and drawing it to the map. If you run the project now, it should look pretty much the same as when we started (but with your beautiful new tiles!)
Smooth ScrollingWe still jump an entire tile at a time in a single frame, which makes things kinda jumpy. For our first implementation of smooth scrolling, we're still going to move in whole-tile increments, but we will do it 2 pixels at a time. In other words, pressing an arrow key will still move you one tile in that direction, but we will "animate" the movement so that it transitions smoothly.
We will need to add a few more definitions to our variable declaration area (beneath the Game1 constructor). Add the following declarations:
// Sub-tile coordinates for Smooth Scrolling
int iMapXOffset = 0;
int iMapYOffset = 0;
// Determines how fast the map will scroll (pixels per arrow key press)
int iMapXScrollRate = 2;
int iMapYScrollRate = 2;
// Variable to determine if we are still moving from the previous directional command
int iMoveCount = 0;
// What direction are we moving (0=up, 1=down, 2=left, 3=right)
int iMoveDirection = 0;
We'll use iMapXOffset and iMapYOffset to track where within our 48x48 tile we are when smooth scrolling. The iMapXScrollRate and iMapYScrollRate determine how quickly we scroll (2 pixels per update loop in this case). The iMoveCount and iMoveDirection will be used to determine what we should be doing during an Update loop.
We'll also need to make a small change to our Draw code. Update your spriteBatch.Draw command like this:
spriteBatch.Draw(t2dTileSet,
new Rectangle(((x*iTileWidth)+iMapDisplayOffsetX)-iMapXOffset,
((y*iTileHeight) + iMapDisplayOffsetY) - iMapYOffset,
iTileWidth, iTileHeight),
recSource,
Color.White);
You'll see that we've added "-iMapXOffset" and "-iMapYOffset" to the destination rectangle. Running the code now will produce exactly the same result as before, because we still need to set these two variables when we detect a movement key.
In order to do this, we need to change our Update method fairly drastically. Here is the entire new method:
protected override void Update()
{
// The time since Update was called last
float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
fTotalElapsedTime += elapsed;
// Read and save the current Keyboard State
ksKeyboardState = Keyboard.GetState();
// Check to see if the Escape key has been pressed. Exit the program if so.
if (ksKeyboardState.IsKeyDown(Keys.Escape))
{
this.Exit();
}
// If we AREN'T in the process of completing a smooth-scroll move...
if (iMoveCount <= 0)
{
// Check to see if an arrow key is pressed. If so, set the
// iMoveDirection to indicate the direction we will be moving in,
// and the iMoveCount to how many times we need to execute.
if (ksKeyboardState.IsKeyDown(Keys.Up))
{
iMoveDirection = 0;
iMoveCount = iTileHeight+iMapYScrollRate;
}
if (ksKeyboardState.IsKeyDown(Keys.Down))
{
iMoveDirection = 1;
iMoveCount = iTileHeight + iMapYScrollRate;
}
if (ksKeyboardState.IsKeyDown(Keys.Left))
{
iMoveDirection = 2;
iMoveCount = iTileHeight + iMapXScrollRate;
}
if (ksKeyboardState.IsKeyDown(Keys.Right))
{
iMoveDirection = 3;
iMoveCount = iTileHeight + iMapXScrollRate;
}
} else {
// If we ARE in the middle of a smooth-scroll move, update the
// Offsets and decrement the move count.
if (iMoveDirection == 0) { iMapYOffset -= iMapYScrollRate;
iMoveCount -= iMapYScrollRate; }
if (iMoveDirection == 1) { iMapYOffset += iMapYScrollRate;
iMoveCount -= iMapYScrollRate; }
if (iMoveDirection == 2) { iMapXOffset -= iMapXScrollRate;
iMoveCount -= iMapXScrollRate; }
if (iMoveDirection == 3) { iMapXOffset += iMapXScrollRate;
iMoveCount -= iMapXScrollRate; }
// If we move off of a tile, change our map location to the next tile
if (iMapXOffset < 0) { iMapXOffset = iTileWidth; iMapX--; }
if (iMapXOffset > iTileWidth) { iMapXOffset = 0; iMapX++; }
if (iMapYOffset < 0) { iMapYOffset = iTileHeight; iMapY--; }
if (iMapYOffset > iTileWidth) { iMapYOffset = 0; iMapY++; }
// If we move off of the side of the map, "snap" back (player won't see a move at all)
if (iMapX < 0) { iMapX = 0; iMapXOffset = 0; iMoveCount = 0;}
if (iMapX > iMapWidth - iMapDisplayWidth) { iMapX = iMapWidth - iMapDisplayWidth;
iMapXOffset = iTileWidth; iMoveCount=0; }
if (iMapY < 0) { iMapY = 0; iMapYOffset = 0; iMoveCount=0;}
if (iMapY > iMapHeight - iMapDisplayHeight) { iMapY = iMapHeight - iMapDisplayHeight;
iMapYOffset = iTileHeight; iMoveCount = 0; }
}
// TODO: Add your game logic here
// Let the GameComponents update
base.Update(gameTime);
}
Time for the step-by-step breakdown. The first few lines are unchanged, except that we are no longer checking for a minimum elapsed time before moving. On my computer, doing the smooth scrolling pretty much takes care of the "zoom across the map" problem. Note however, that this isn't the best way to handle the problem. In a real game, we should still be setting a movement speed and possibly adjusting the iMapXScrollRate and iMapYScrollRate variables in accordance with the speed of the user's PC so that things always run at the same rate.
We still get the Keyboard state just as before, and still check for the "Escape" key.
if (iMoveCount <= 0)
{
// Check to see if an arrow key is pressed. If so, set the
// iMoveDirection to indicate the direction we will be moving in,
// and the iMoveCount to how many times we need to execute.
if (ksKeyboardState.IsKeyDown(Keys.Up))
{
iMoveDirection = 0;
iMoveCount = iTileHeight+iMapYScrollRate;
}
We have now defined two "states" that we can be in when the update loop is called. We are either moving or waiting for player input. If we are moving, iMoveCount will be greater than zero.
If we are waiting, we check each of the movement keys as before. Instead of actually moving the map, however, we set iMoveDirection and iMoveCount to place us into a moving state.
} else {
// If we ARE in the middle of a smooth-scroll move, update the
// Offsets and decrement the move count.
if (iMoveDirection == 0) { iMapYOffset -= iMapYScrollRate;
iMoveCount -= iMapYScrollRate; }
if (iMoveDirection == 1) { iMapYOffset += iMapYScrollRate;
iMoveCount -= iMapYScrollRate; }
if (iMoveDirection == 2) { iMapXOffset -= iMapXScrollRate;
iMoveCount -= iMapXScrollRate; }
if (iMoveDirection == 3) { iMapXOffset += iMapXScrollRate;
iMoveCount -= iMapXScrollRate; }
When in a moving state (ie, iMoveCount > 0) we update the map offsets (our sub-tile location) by adding or subtracting the Scroll Rate value based on the direction we are moving. We also reduce iMoveCount by the number of pixels we have moved. As this Update repeats, it will eventually bring iMoveCount back to 0 or less, thus changing us out of a moving state and back to a waiting state.
// If we move off of a tile, change our map location to the next tile
if (iMapXOffset < 0) { iMapXOffset = iTileWidth; iMapX--; }
if (iMapXOffset > iTileWidth) { iMapXOffset = 0; iMapX++; }
if (iMapYOffset < 0) { iMapYOffset = iTileHeight; iMapY--; }
if (iMapYOffset > iTileWidth) { iMapYOffset = 0; iMapY++; }
If the offset value moves off of a tile in either direction (either by being greater than the tile width/height or less than zero) we update our map location by adding or subtracting one to iMapX or iMapY. You'll recall that in the previous version of the tile engine we were directly modifying iMapX and iMapY when movement keys were pressed. Updating them here accomplishes the same thing (moving around the map) based on the smooth scrolling offsets.
The rest of the update routine is more or less the same as the last version. We check to see if we would be going off the map in any direction and correct for it just as before.
Running your project and using the movement keys should scroll you smoothly around the map. However, you will also notice that the outside border of the map jumps around a lot, with "extra" columns disappearing with each move. This is because we are actually overdrawing the area we need slightly and when the offsets are equal to zero we don't need to do that.
Overlaying a "Game Screen"In order to mask the "jerkiness" we see above, we need to paint over the map with a "game screen" which has a cut out that our map can be seen through. In order to do this, we will need an image of the game screen that includes transparency.
 |
Here is our "Game Screen", created in Photoshop and saved as a .PNG file. In order to use it in XNA, however, we need to use the DirectX Texture Tool (Part of the DirectX SDK to convert it into a DDS file.
Fire up your DirectX Texture Tool and select "New Texture..." from the file menu. Create it as a 640x480 texture (The size of the GSE's default game window. Make sure that the texture format includes an alpha channel (I used A8R8B8G8). Hit OK and you should end up with an empty texture window.
Select "Open onto this surface..." from the File menu and open the .PNG file. It should open onto your window. Select Save As from the file menu and save it to your project directory.
|
Through the Solution Explorer pane, add the "gamescreen.dds" file to your project and set it to "Copy Always".
Next we will need to create a Texture2D object for the game screen and load it in the LoadResources method. Add the following line right below our existing declaration of the tileset Texture2D object:
Texture2D t2dGameScreen;
In the LoadResources method, add the following line right after the load for the tileset:
t2dGameScreen = content.Load<Texture2D>(@"content\textures\gamescreen");
Next, we need to extend the number of tiles we are drawing by 2 in each direction. This isn't strictly related to what we are doing here, but in the next part of this tutorial series when we add a "player avatar", we will need a "center" tile that the avatar occupies.
In the declarations section, locate the iMapDisplayWidth and iMapDisplayHeight variable declarations and change them from six to eight:
// How many tiles should we display at a time
int iMapDisplayWidth = 8;
int iMapDisplayHeight = 8;
Now all that is left is to actually draw the game screen. We want it to be drawn after the map in each frame, so we will place the call right before the spriteBatch.End(); line in the Draw procedure:
spriteBatch.Draw(t2dGameScreen, new Rectangle(0, 0, 640, 480), Color.White);
That's it! Run your project and you should now have a smooth scrolling map overlayed by a game screen.


Coming Up Next...That's it for this round. Up next, we will be looking at adding a player avatar to the screen and detecting where the player should be able to walk. We will also look at adding a second layer of tiles to provide and "objects" layer to hold things like trees and rocks independantly of the underlying terrain.
|