Part 2 - Larger Tile Sets and Map Layers


Now that we have a basic engine up and running, we need to start giving it some additional flexibility. Currently, our implementation of a tileset is limited to a single row of fixed-size images on a single texture. We will start off this section by expanding on our implementation of tilesets a bit to allow for more tiles to be defined for our game.

Begin by adding the following tileset image to your content project. You may remember this image from the later versions of the original Tile Engine tutorial series.



We will use this tileset for the remainder of our square-based work on the Tile Engine series.

Open up Tile.cs and modify the TileWidth and TileHeight values for 48 by 48 pixel tiles:

static public int TileWidth = 48;
static public int TileHeight = 48;


Next, lets modify the GetSourceRectangle method to translate the tile index to a coordinate on the larger tile image:

static public Rectangle GetSourceRectangle(int tileIndex)
{
    int tileY = tileIndex / (TileSetTexture.Width / TileWidth);
    int tileX = tileIndex % (TileSetTexture.Width / TileWidth);

    return new Rectangle(tileX * TileWidth, tileY * TileHeight, TileWidth, TileHeight);
}


We use the width of the texture, combined with the width of a tile, to determine the number of tiles on a single row on the texture image. Using this information, we can translate a tileIndex into a rectangle anywhere on the texture.

This new layout means we can have a larger image with more tiles on it. The width of the image must be an even multiple of the width of a single tile, and there is no spacing between tiles on the image.

We need to update a few things in the Game1 class, because in Part 1, we hardcoded values into the Update and Draw methods that specified the size of a map tile. Go back to the Game1.Update() method and change the "32"s at the end of each of the Camera.Location lines to either Tile.TileWidth or Tile.TileHeight, as appropriate. The Update method should now look like this:

        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

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

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

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

            if (ks.IsKeyDown(Keys.Down))
            {
                Camera.Location.Y = MathHelper.Clamp(Camera.Location.Y + 2, 0, 
                    (myMap.MapHeight - squaresDown) * Tile.TileHeight);
            }
            // TODO: Add your update logic here

            base.Update(gameTime);
        }


We need to make a similar change in the Draw() code where we calculate the "first" tiles and the offsets:

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

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


Again, we are just replacing the "32" hardcoded values with our TileWidth and TileHeight properties.

If you run your project at this point, you will simply see the older tiles replaced with our new ones, which are considerably larger, though the image still doesn't fill the screen area:



Layers


As our engine is currently implemented, each cell on the map contains one, and only one, tile texture. So far, this hasn't really been an issue, because we are using tile images that occupy the entire area of the tile.

However, when it comes to making transitions between two different types of terrain, we have a couple of different options. The first would be to create transition tiles for every pair of terrain types possible in our engine. This is actually a pretty common approach, and not a bad way to go if the number of different terrain types you are working with is relatively small. In our first example tileset, we have four different types of terrain (water, sand, dirt, and grass). If we were to create transitions for all of the potential pairs, we would need water/sand, water/dirt, water/grass, sand/dirt, sand/grass, and dirt/grass). If we had 10 different types of terrain, we would end up needing 45 different transition sets to cover all of the possible combinations.

You can reduce the art-construction workload further by defining rules about what terrain types can be next to each other (ie, don't worry about creating a desert/ice field transition set, or a desert/ocean set, etc since they probably won't be placed next to each other).

Another option is to create tiles that contain one type of terrain's transition to transparency. Then these tiles can be drawn on top of any terrain type and vastly reduce the number of transition sets that need to be created to one per terrain type.

In order to draw these onto the map, we will revise the structure of our MapCell class, and update our drawing routine to account for the new structure.

Open up MapCell.cs, and add the following List variable:

public List<int> BaseTiles = new List<int>();


By storing a line of tile IDs, we can stack any number of tile images on the same space. While we are here, modify the TileID property of MapClass to look like this:

public int TileID
{
    get { return BaseTiles.Count > 0 ? BaseTiles[0] : 0; }
    set
    {
        if (BaseTiles.Count > 0)
            BaseTiles[0] = value;
        else
            AddBaseTile(value);
    }
}


When we as a MapCell about it's TileID, we will assume that we are asking for the first base tile. The get portion of the property uses a inline evaluation to determine what value to return to the caller. The expression to the left of the ? is evaluated. If it is true, the expression to the left of the : is returned (BaseTiles[0] in this case. If the expression is false, the second value (0) is returned.

Similarly, when setting we make sure there is at least one tile in the BaseTiles list and set it to the passed value. If there isn't, we create one with the passed value. Of course, we will need to add the AddBaseTile helper function to our MapCell class:

public void AddBaseTile(int tileID)
{
    BaseTiles.Add(tileID);
}

Because we allow the TileID property to set the base tile if it doesn't exist already, we don't need to modify our constructor at this point. The passed TileID value will end up as the first entry in the baseTiles list.

Drawing Multiple Layers


When we draw our tiles, we want to draw each of the tiles in our tile list on top of each other when a MapCell is drawn. Lets modify the Draw() method of the Game1 class to account for these layers. In the Draw() method, locate the call to spriteBatch.Draw() which draws the tiles currently displayed on the map. Update it to look like this:

foreach (int tileID in myMap.Rows[y + firstY].Columns[x + firstX].BaseTiles)
{
    spriteBatch.Draw(
        Tile.TileSetTexture,
        new Rectangle(
            (x * Tile.TileWidth) - offsetX, (y * Tile.TileHeight) - offsetY, 
            Tile.TileWidth, Tile.TileHeight),
        Tile.GetSourceRectangle(tileID),
        Color.White);
}


Essentially, we are wrapping the spriteBatch.Draw() call with a foreach loop that runs through each of the items in the BaseTiles list for the current MapCell. Determining the row and column information has been moved out of the Draw() call itself and into the header of the foreach loop.

As each cell of the map is processed, each entry in the cell's BaseTiles list will be drawn, in the order they were added to the list.

In order to see this in action, lets place some "fringe" transitions around the water area that currently exists on our sample map. Go back to the TileMap class and lets modify the constructor by adding the following at the end of the method:

Rows[3].Columns[5].AddBaseTile(30);
Rows[4].Columns[5].AddBaseTile(27);
Rows[5].Columns[5].AddBaseTile(28);

Rows[3].Columns[6].AddBaseTile(25);
Rows[5].Columns[6].AddBaseTile(24);

Rows[3].Columns[7].AddBaseTile(31);
Rows[4].Columns[7].AddBaseTile(26);
Rows[5].Columns[7].AddBaseTile(29);

Rows[4].Columns[6].AddBaseTile(104);


Again, we are just hardcoding some values here, but we can see that these tiles will be added on a layer above the water tiles that already exist in these position. Lets make one last change while we are at it so that our map covers the entire area of the game windows. Open up Game1.cs and change the values for squaresAcross and squaresDown:

int squaresAcross = 18;
int squaresDown = 11;


Run your project, and you should now have a nicely bordered lake with a rock in the middle of it. There is no reason we couldn't have placed a rock on one of the "shore" pieces as well. Go ahead and modify the TileMap class to add transition tiles to the other terrain areas and smooth things out.



As you can see, getting XNA to draw the tile map can be quite a bit easier than laying out the tiles on the screen. Down the road, we will take a look at using existing tile map editors, and perhaps put together a functional map editor of our own, but I haven't decided which way to go with the series yet.

Further Reading


There is quite a bit of good information out on the net about tile engines, and this article has long been an interesting one to me. It isn't XNA related, but it explains a system for having the tile engine automatically draw transitions based on the surrounding tile terrain types. I haven't implemented anything like this in the current engine because we are not going to stick with a square tile base after this installment, but it certainly takes a lot of work out of drawing your tile maps, since you don't have to worry about transitions at all during map creation.

In the next installment, we will modify our engine to support hexagonal maps. Updating to support this kind of map will lay the groundwork for transforming our engine into an isometric tile engine as well.




































 

 
 
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