Introduction

Welcome to the third installment of our Tile Engine Tutorial. We will be working from the Solution we created in Part 2.

In both of our previous examples, the map we have been working with has been a very simple single layer of tile numbers. In this installment, we will be focusing on expanding the map's capabilities quite a bit. By the end, we should have:

  • A multiple-layer map containing:
    • A "Base" layer composed of tiles with no transparency
    • A "Transition" layer composed used to overlay transitional tiles
    • An "Objects" layer that can be used to place items on the map
    • A "Walkable" layer that defines where on the map the player can an can't go
  • A simple example of tile animation (taking advantage of the Base and Transitional layers mentioned above
  • An overlayed player Avatar

Multi-Layered Map

In Part 2, we talked about transition tiles, and I said that we needed a set of 12 tiles for every terrain pair that we wanted to transition between. In our single layer map, this means that having more than a few types of terrain means we needs A LOT of tiles:

Terrain Types Transition Sets Needed Transition Tiles
2 1 12
3 3 36
5 10 120
10 45 540


As you can see, this gets out of hand pretty quickly! What we need is a better way.

The "better way" we are going to explore here is a multi-layered map. In the diagram at the right, we see the three layers we will be using in this tutorial.

The bottom, or "Base" layer of tiles will be drawn without alpha blending to save a bit on processing. This layer will contain only whole-tile terrain types.

Our transition layer will be composed only of transition tiles. These tiles can be created with a single terrain type as the base and transparency as terrain that is being "overlayed".


By doing this, we now only need a single set of 12 transition tiles for each terrain type. Instead of the 540 transition tiles needed for 10 terrain types in the table above, we only need 120 of them. In order to gain us a little more speed, we are also going to only draw tiles on the Transition and Object layer if they contain something other than a base-layer tile.

Our New Tile Set

Of course, all of this calls for yet another Tile Set image. This time around we need to create it with transparency and make it into a DDS (Direct Draw Surface) instead of just leaving it as a .JPG file like we did before. Here is the new Tile Set I've put together:



A few things to note here. For the purposes of this tutorial, I'm considering the first row of tiles (Tiles 0 thru 11) to be "base" tiles. All that really means is that tiles 0 thru 11 will NOT BE DRAWN if they are found on the Transition or Object layers. This is actually very convenient for us now, since we don't have a "map maker" program yet, in that we can copy our base map layer and use the underlying tile numbers as a reference point when putting in transition and object tiles. This won't be needed when we finally get a map making program working (Hint, that's the main focus of Part 4, because wait until you see what our in-line map has grown into!)

In this particular tileset image, I don't have enough room to make 12 full transition sets, but the 7 terrain types I have here will suffice for now. In reality, I would really want to make a few different Tile Set pages. I would also keep all of my objects on a different tileset page, etc. In a future installment I plan on covering how to do this (it's actually very easy to do, and similar to what we did in Part 1 with an array of Texture2D objects. We can use the Integer Division (/) and Modulus (%) operators to find the tile page we should be looking at based on the number of tiles on a page.

You will also see a few "object layer" tiles on the image above. Rocks and bushes, etc. Again, these are drawn with transparency so that we don't have to worry about what they are being placed over.

Finally, the bottom row of the Tile Set merits some further explanation. I said at the top of this tutorial that one of our goals would be to have a simple animated-tile example. That's what these 8 tiles are for. I took the water tile from the base row and copied it to tile #108. Then I made 7 more copies, each one shifted 6 pixels to the left (In photoshop, Filter->Other->Offset, 6 pixels horizontal, wrap edges). This gives me 8 tiles where the image is shifted 1/8th of a tile to the right each time. We are going to use that to "animate" our water.

New Game Screen

Now that we are making our map more complex, we need a bit more viewing area. In order to do this, we are going to replace the current "gamescreen.dds" file with a new one, and change the iMapDisplayWidth and iMapDisplayHeight variables.



On With the Coding

Download the DDS Version of the Tile Set image above (or create your own!) add them to your "content\textures" directory and add them to the project. Remember that we no longer have to worry about copying them at runtime, as Content Pipeline handles all of that for us. You could also download the PNG version above and convert it to a DDS yourself using the DirectX Texture Tool (576x480).

Do the same with the new Game Screen image above.

Don't forget to remove the old items from your content\textures directory so you don't get build errors later on! (Actually as long as the files are there, you won't get errors, but it doesn't make sense to leave content built into your system that you don't use in the game).

Change your LoadGraphicsContent method to reflect the new name of the tileset image. It should look like this now:

        protected override void LoadGraphicsContent(bool loadAllContent)
        {
            if (loadAllContent)
            {
                // TODO: Load any ResourceManagementMode.Automatic content
                t2dTileSet = content.Load<Texture2D>(@"content\textures\fulltileset_alpha");
                t2dGameScreen = content.Load<Texture2D>(@"content\textures\gamescreen");
                spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
            }
            // TODO: Load any ResourceManagementMode.Manual content
        }

Next we'll expand the map drawing area to fill the newly enlarged "hole" in the game screen. In your variable declarations area, locate the iMapDisplayWidth and iMapDisplayHeight variables and set them as follows:
        // How many tiles should we display at a time
        int iMapDisplayWidth = 14;
        int iMapDisplayHeight = 8;

Finally, since we are moving the display withing our window slightly, we need to update iMapDisplayOffsetX and iMapDisplayOffsetY and set them both to 5.
        // How far from the Upper Left corner of the display do we want our map to start
        int iMapDisplayOffsetX = 5;
        int iMapDisplayOffsetY = 5;

In order to have a multi-layered map, we'll need to define the multiple layers. The map definition is getting pretty ugly now, so this will be the last time we do this. In part 4 we will put together a simple map editor so we can create and store our map on disk. This will also allow us to expand the size of the map beyond the current 20x20 tile area since we don't have to worry about keeping it readable in our code anymore.

Until then, though, here is the new definition of our map:
        // Our map's base terrain layer.  These should be tiles 0 thru 11, which are full
        // square tiles.  The base map layer is drawn without alpha blending.
        int[,] iMap = new int[iMapHeight, iMapWidth] { 
                             {000,000,000,108,108,108,108,000,000,003,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,003,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,003,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,003,000,000,000,000,000,000,000,000,000,000}, 
                             {003,003,003,003,003,003,003,003,003,003,003,003,003,003,003,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,003,000,000,000,000,003,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,003,000,000,000,000,003,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,003,000,000,000,000,003,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,003,000,000,000,000,003,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,003,000,000,000,000,003,003,003,003,003,003}, 
                             {000,000,000,000,000,000,000,000,000,003,000,000,000,000,000,000,000,000,000,000}, 
                             {108,000,000,000,000,000,000,000,000,003,000,000,000,000,000,000,000,000,000,000}, 
                             {108,108,108,108,000,000,000,000,000,003,000,000,000,000,000,000,000,000,000,000}, 
                             {108,108,108,108,108,000,000,000,000,003,000,000,000,000,000,000,000,000,000,000}, 
                             {108,108,108,108,108,108,108,108,108,004,108,108,108,108,108,108,108,000,000,000}, 
                             {108,108,108,108,108,108,108,108,108,004,108,108,108,108,108,108,108,000,000,000}, 
                             {108,108,108,108,108,108,108,108,108,004,108,108,108,108,108,108,108,000,000,000}, 
                             {108,108,108,108,108,000,000,000,000,003,000,000,000,000,108,108,108,000,000,000}, 
                             {108,108,108,108,108,000,000,000,000,003,000,000,000,000,108,108,108,000,000,000}, 
                             {108,108,108,108,108,000,000,000,000,003,000,000,000,000,108,108,108,000,000,000}, 
        };
        // Our map's Transition layer.  This layer is drawn using Alpha Blending, and any
        // tile with a value of 11 or less WILL NOT BE DRAWN.  They can be filled in here
        // for visulization's sake.
        int[,] iMapTrans = new int[iMapHeight, iMapWidth] { 
                             {000,000,000,016,012,012,017,000,038,001,039,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,038,001,039,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,038,001,039,000,000,000,000,000,000,000,000,000}, 
                             {036,036,036,036,036,036,036,036,041,001,040,036,036,036,036,045,000,000,000,000}, 
                             {001,001,001,001,001,001,001,001,001,001,001,001,001,001,001,039,000,000,000,000}, 
                             {037,037,037,037,037,037,037,037,043,001,042,037,037,043,001,039,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,038,001,039,000,000,038,001,039,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,038,001,039,000,000,038,001,039,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,038,001,039,000,000,038,001,040,036,036,036,036}, 
                             {000,000,000,000,000,000,000,000,038,001,039,000,000,038,001,001,001,001,001,001}, 
                             {000,000,000,000,000,000,000,000,038,001,039,000,000,046,037,037,037,037,037,037}, 
                             {019,000,000,000,000,000,000,000,038,001,039,000,000,000,000,000,000,000,000,000}, 
                             {022,013,013,019,000,000,000,000,038,001,039,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,022,019,000,000,000,038,001,039,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,022,013,013,013,013,001,013,013,013,013,013,013,019,000,000,000}, 
                             {000,000,000,000,000,000,000,000,062,001,063,000,000,000,000,000,014,000,000,000}, 
                             {000,000,000,000,020,012,012,012,012,001,012,012,012,012,021,000,014,000,000,000}, 
                             {000,000,000,000,014,000,000,000,038,001,039,000,000,000,015,000,014,000,000,000}, 
                             {000,000,000,000,014,000,000,000,038,001,039,000,000,000,015,000,014,000,000,000}, 
                             {000,000,000,000,014,000,000,000,038,001,039,000,000,000,015,000,014,000,000,000}, 
        };
        // Our map's Object layer.  This layer is drawn using Alpha Blending, and any
        // tile with a value of 11 or less WILL NOT BE DRAWN.  They can be filled in here
        // for visulization's sake.
        int[,] iMapObjects = new int[iMapHeight, iMapWidth] { 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,105,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,096,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,097,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,098,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,100,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,062,000,063,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,104,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,062,000,063,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
        };

Go ahead and replace your current map declaration with the code above. You will notice a few changes from last time. Cosmetically, I've made everything a three-digit value so that all of the columns line up. Again, we won't need to worry about this after part 4.

The big change, however, is that we now use three arrays to hold our map data instead of one. The X,Y coordinates in each array represent the same location on the map in all three arrays. (Note that a 3-dimensional array could have been used as well, but for our in-line example here it would have been really, really hard to work with.

The other thing you might notice is that most of the tiles in iMapTrans and iMapObjects are "000". Recall that I said above that we won't draw anything in these two layers with a tile value less than 12. You might also notice that in iMapTrans, there are a series of "001"s that follow the same course as the 001 tiles in iMap. This is what I mentioned above about using the fact that we aren't drawing 0-11 to our advantage in the in-line example by making it easier to place iMapTrans tiles by having the underlying terrain tiles still represented.

Simple Sprite Animation


Before we go ahead and update our Draw code to manage the three layers, lets go ahead and add a few variables to our declarations section to handle our simple animated water example. Somewhere in your declarations area, add the following:
        // Water Animation
        int iTileAnimationFrame = 0;         // The current frame of animation we are on.
        int iTileAnimationFrameCount = 7;    // The total number of frames in our animation
        int iTileAnimationStartFrame = 108;  // The beginning frame of our animation
        float fAnimationTime = 0.0f;         // How much time has elapsed since we last animated
        float fAnimationDelay = 0.1f;        // How much time needs to elapse before we animate
I'm just introducing the concept of animation here, so we're starting off simple. There will be only one kind of animated tile in this example, and it will only be base layer tiles of tile #108 (the first of the animated water tiles). The iTileAnimationFrameCount and iTileAnimationStartFrame define the starting point and number of iterations to make (minus 1) to complete the animation. While in the Update loop, we will revive our speed-limiting concept from Part 1 to make sure our animation plays at a reasonable pace by accumulating time in fAnimationTime until it passes fAnimationDelay. Then we will update iTileAnimationFrame and reset fAnimationTime.

In order to accomplish this, we need to modify our Update method a bit. I'm not going to include the whole method this time, as it is getting a bit large, and we are only making two changes to the code. In your Update method, look for the line "fTotalElapsedTime += elapsed;" and add the following right after it:
            fAnimationTime += elapsed;
Scroll down a bit, and find "base.Update(gameTime);" and add the following right above it:
            if (fAnimationTime >= fAnimationDelay)
            {
                iTileAnimationFrame++;
                if (iTileAnimationFrame > iTileAnimationFrameCount)
                {
                    iTileAnimationFrame = 0;
                }
                fAnimationTime = 0.0f;
            }
Together, these two sections of code will be responsible for tracking the animation frame we are currently on. Each run through update increases fAnimationTime, and when it gets larger than fAnimationDelay, we increment iTileAnimationFrame. If the frame goes past the number of frames in the animation, it is reset to zero.

Updating the Draw Code

Our Draw method will change quite a bit, so I'm including the whole thing here:
        protected override void Draw()
        {
            if (!graphics.EnsureDevice())
            {
                return;
            }
            int iTileToDraw;
            graphics.GraphicsDevice.Clear(Color.Black);
            graphics.GraphicsDevice.BeginScene();
            // TODO: Add your drawing code here
            spriteBatch.Begin(SpriteBlendMode.None);
            // Draw the map
            for (int y = 0; y < iMapDisplayHeight; y++)
            {
                for (int x = 0; x < iMapDisplayWidth; x++)
                {
                    iTileToDraw = iMap[y + iMapY, x + iMapX];
                    if (iTileToDraw == iTileAnimationStartFrame)
                    {
                        iTileToDraw += iTileAnimationFrame;
                    }
                    Rectangle recSource = new Rectangle((iTileToDraw % iTileSetXCount) * iTileWidth,
                                                        (iTileToDraw / iTileSetXCount) * iTileHeight,
                                                        iTileWidth, iTileHeight);
                    spriteBatch.Draw(t2dTileSet,
                                     new Rectangle(((x * iTileWidth) + iMapDisplayOffsetX) - iMapXOffset,
                                     ((y * iTileHeight) + iMapDisplayOffsetY) - iMapYOffset,
                                     iTileWidth, iTileHeight),
                                     recSource,
                                     Color.White);
                }
            }
            spriteBatch.End();
            spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
            // Draw transitions layer of the map
            for (int y = 0; y < iMapDisplayHeight; y++)
            {
                for (int x = 0; x < iMapDisplayWidth; x++)
                {
                    Rectangle recDest = new Rectangle(((x * iTileWidth) + iMapDisplayOffsetX) - iMapXOffset,
                                         ((y * iTileHeight) + iMapDisplayOffsetY) - iMapYOffset,
                                         iTileWidth, iTileHeight);
                    iTileToDraw = iMapTrans[y + iMapY, x + iMapX];
                    if (iTileToDraw > 11)
                    {
                        Rectangle recSource = new Rectangle((iTileToDraw % iTileSetXCount) * iTileWidth,
                                                            (iTileToDraw / iTileSetXCount) * iTileHeight,
                                                            iTileWidth, iTileHeight);
                        spriteBatch.Draw(t2dTileSet,recDest,recSource,Color.White);
                    }
                    
                    iTileToDraw = iMapObjects[y + iMapY, x + iMapX];
                    if (iTileToDraw > 11)
                    {
                        Rectangle recSource = new Rectangle((iTileToDraw % iTileSetXCount) * iTileWidth,
                                                            (iTileToDraw / iTileSetXCount) * iTileHeight,
                                                            iTileWidth, iTileHeight);
                        spriteBatch.Draw(t2dTileSet,recDest,recSource,Color.White);
                    }
                }
            }
            spriteBatch.Draw(t2dGameScreen, new Rectangle(0, 0, 640, 480), Color.White);
            spriteBatch.End();
            // Let the GameComponents draw
            DrawComponents();
            graphics.GraphicsDevice.EndScene();
            graphics.GraphicsDevice.Present();
        }

Lets go through this step by step:
            spriteBatch.Begin(SpriteBlendMode.None);
            // Draw the map
            for (int y = 0; y < iMapDisplayHeight; y++)
            {
                for (int x = 0; x < iMapDisplayWidth; x++)
                {
                    iTileToDraw = iMap[y + iMapY, x + iMapX];
                    if (iTileToDraw == iTileAnimationStartFrame)
                    {
                        iTileToDraw += iTileAnimationFrame;
                    }
                    Rectangle recSource = new Rectangle((iTileToDraw % iTileSetXCount) * iTileWidth,
                                                        (iTileToDraw / iTileSetXCount) * iTileHeight,
                                                        iTileWidth, iTileHeight);
                    spriteBatch.Draw(t2dTileSet,
                                     new Rectangle(((x * iTileWidth) + iMapDisplayOffsetX) - iMapXOffset,
                                     ((y * iTileHeight) + iMapDisplayOffsetY) - iMapYOffset,
                                     iTileWidth, iTileHeight),
                                     recSource,
                                     Color.White);
                }
            }
            spriteBatch.End();
The first change in this portion is that our Base tile layer will be drawn with the SpriteBlendMode.None parameter (the first line). This tells XNA not to worry about transparency for this spriteBatch execution. Theoretically this should be a little faster (I haven't tested it though).

The second change is the if statement after finding iTileToDraw. We check to see if it is our special animated tile and, if so, add the current animation frame to iTileToDraw. The underlying map doesn't change, but the temporary variable we are using to determine the tile number to draw does.
            spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
            // Draw transitions layer of the map
            for (int y = 0; y < iMapDisplayHeight; y++)
            {
                for (int x = 0; x < iMapDisplayWidth; x++)
                {
                    Rectangle recDest = new Rectangle(((x * iTileWidth) + iMapDisplayOffsetX) - iMapXOffset,
                                         ((y * iTileHeight) + iMapDisplayOffsetY) - iMapYOffset,
                                         iTileWidth, iTileHeight);
                    iTileToDraw = iMapTrans[y + iMapY, x + iMapX];
                    if (iTileToDraw > 11)
                    {
                        Rectangle recSource = new Rectangle((iTileToDraw % iTileSetXCount) * iTileWidth,
                                                            (iTileToDraw / iTileSetXCount) * iTileHeight,
                                                            iTileWidth, iTileHeight);
                        spriteBatch.Draw(t2dTileSet,recDest,recSource,Color.White);
                    }
                    
                    iTileToDraw = iMapObjects[y + iMapY, x + iMapX];
                    if (iTileToDraw > 11)
                    {
                        Rectangle recSource = new Rectangle((iTileToDraw % iTileSetXCount) * iTileWidth,
                                                            (iTileToDraw / iTileSetXCount) * iTileHeight,
                                                            iTileWidth, iTileHeight);
                        spriteBatch.Draw(t2dTileSet,recDest,recSource,Color.White);
                    }
                }
            }
            spriteBatch.Draw(t2dGameScreen, new Rectangle(0, 0, 640, 480), Color.White);
            spriteBatch.End();
Now we execute another spriteBatch.Begin. This time with the SpriteBlendMode.AlphaBlend back in place, so this time we are going to take transparency into account when drawing our sprites.

The loop used here is the same as before. Since we are drawing both the iMapTrans and iMapObjects layers with transparency, we don't need to use a third loop as long as we draw the iMapObjects (the topmost layer) first. The internals of the loop are basically two copies of our old draw loop, with the addition of a conditional to only draw anything if the iTileToDraw variable is greater than 11. I've also created a new calculation of the Destination rectangle outside the draw loop, since we will potentially be drawing multiple tiles to the same location in this loop. In the first block we use iMapTrans to determine iTileToDraw, and in the second we use iMapObjects.

Abusing the Objects layer

As you can see running the program, there are some bushes and rocks scattered about drawn from the objects layer. However, we can also make use of the objects layer to "cheat" a bit.

Running the project now will give you a nice new multi-layered map. If you are using the map and tiles included in this tutorial, you should be able to scroll around and find a place where a bridge crosses the flowing river. If you look at this area on the iMapObjects layer you will see tiles "062" and "062" surrounding the bridge in two places. Both of these places are where the water, shore (grass) and bridge tiles meet to form a corner. We are using the object layer in this case to stack three terrain tiles to form a terrain feature we wouldn't otherwise be able to create.

To see what I mean, in the iMapTrans layer on the 4th row from the bottom, change the hilighted 012's to 000's:

{000,000,000,000,020,012,012,012, 012,001, 012,012,012,012,021,000,014,000,000,000},


Run the program and scroll down to the map and you will see that the shoreline no longer meshes with the bridge.

Player Avatar


Our first run at a player avatar will be fairly simple. We have a one-tile-sized image that we will be placing on the center tile of our display to represent the player. At the moment, it won't be animated or anything.


You might notice that I'm using a full size tile sheet for the player avatar. This is because in a later installment of this series we will be replacing the simple avatar we are using here with an animated avatar and we'll require a lot of images for that.

As with our other image resources, this one will need to be converted to a DDS and added to our project and set to Copy Always. We will also need to add a declaration for the surface in our variable declarations area:
        Texture2D t2dPlayerAvatar;

And update our LoadGraphicsContent to load the image:
        protected override void LoadGraphicsContent(bool loadAllContent)
        {
            if (loadAllContent)
            {
                // TODO: Load any ResourceManagementMode.Automatic content
                t2dTileSet = content.Load<Texture2D>(@"content\textures\fulltileset_alpha");
                t2dGameScreen = content.Load<Texture2D>(@"content\textures\gamescreen");
                t2dPlayerAvatar = content.Load<Texture2D>(@"content\textures\playeravatars");
                spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
            }
            // TODO: Load any ResourceManagementMode.Manual content
        }

We will also add a couple of variable declarations to track where on the display our player avatar is being displayed:
        // Player Avatar Information
        int iPlayerAvatarXOffset = 6;
        int iPlayerAvaterYOffset = 3;

These two variable store how many tiles from the upper left corner of the display that we will draw the player avatar. In order to do the actual drawing, we'll need to add a few lines to our Draw method. Right before the spriteBatch.Draw call for the gamescreen, add the following:
            // Draw the Player Avatar
            spriteBatch.Draw(t2dPlayerAvatar, new Rectangle(((iPlayerAvatarXOffset * iTileWidth) + iMapDisplayOffsetX),
                                 ((iPlayerAvaterYOffset * iTileHeight) + iMapDisplayOffsetY),
                                 iTileWidth, iTileHeight), new Rectangle(0, 0, 48, 48), Color.White);
This should be pretty standard by now. Just pulling the 48x48 tile out of the t2dPlayerAvatar surface and drawing it to the screen, taking the sub-tile offsets into account (so that the avatar always stays centered instead of being locked to the upper left corner of it's tile and then jumping after the move is complete. To see what I mean, take out the +iMapDisplayOffsetX and +iMapDisplayOffsetY variables in the command above and run the program.)

Now we should have a little knight on our screen representing our player. Currently we can't go to the "edge" of the map, as your avatar will always stay in the center. We'll work on code to change that in a later installment.

"Walkability"


As things stand right now, our little knight can walk on water and over the boulders on the map. In order to prevent this, we need to establish a way to determine where the player can and can't walk.

We'll do this by adding another map layer that will contain data about where the player can walk. For our initial implementation we'll use 0 if you can walk there and 1 if you can't. If you want to get fancy, you could use other values to indicate "special" conditions, such as the player takes 5 HP damage when walking here, or moves at half speed, or has a chance to contract swamp disease, etc.

Add the following below the other map layers you have in your declarations area:
        int[,] iMapWalkable = new int[iMapHeight, iMapWidth] { 
                             {000,000,000,001,001,001,001,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {001,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {001,001,001,001,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {001,001,001,001,001,000,000,001,000,000,000,000,000,000,000,000,000,000,000,000}, 
                             {001,001,001,001,001,001,001,001,001,000,001,001,001,001,001,001,001,000,000,000}, 
                             {001,001,001,001,001,001,001,001,001,000,001,001,001,001,001,001,001,000,000,000}, 
                             {001,001,001,001,001,001,001,001,001,000,001,001,001,001,001,001,001,000,000,000}, 
                             {001,001,001,001,001,000,000,000,000,000,000,000,000,000,001,001,001,000,000,000}, 
                             {001,001,001,001,001,000,000,000,000,000,000,000,000,000,001,001,001,000,000,000}, 
                             {001,001,001,001,001,000,000,000,000,000,000,000,000,000,001,001,001,000,000,000}, 
        };

Here we simply have "000" for tiles that can be walked on and "001" for tiles that can't. Next we need to edit our Update method to take the walkability layer into account. This only requires a change to the wating state code, so replace that portion of the Update code with this:
                // 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))
                {
                    if (iMapWalkable[(iMapY+iPlayerAvaterYOffset) - 1, (iMapX+iPlayerAvatarXOffset)] == 0)
                    {
                        if (iMapY > 0)
                        {
                            iMoveDirection = 0;
                            iMoveCount = iTileHeight + iMapYScrollRate;
                        }
                    }
                }
                if (ksKeyboardState.IsKeyDown(Keys.Down))
                {
                    if (iMapWalkable[(iMapY+iPlayerAvaterYOffset) + 1, (iMapX+iPlayerAvatarXOffset)] == 0)
                    {
                        if (iMapY < (iMapHeight - iMapDisplayHeight))
                        {
                            iMoveDirection = 1;
                            iMoveCount = iTileHeight + iMapYScrollRate;
                        }
                    }
                }
                if (ksKeyboardState.IsKeyDown(Keys.Left))
                {
                    if (iMapWalkable[(iMapY+iPlayerAvaterYOffset), (iMapX+iPlayerAvatarXOffset) - 1] == 0)
                    {
                        if (iMapX > 0)
                        {
                            iMoveDirection = 2;
                            iMoveCount = iTileHeight + iMapXScrollRate;
                        }
                    }
                }
                if (ksKeyboardState.IsKeyDown(Keys.Right))
                {
                    if (iMapWalkable[(iMapY+iPlayerAvaterYOffset), (iMapX+iPlayerAvatarXOffset) + 1] == 0)
                    {
                        if (iMapX < (iMapWidth - iMapDisplayWidth))
                        {
                            iMoveDirection = 3;
                            iMoveCount = iTileHeight + iMapXScrollRate;
                        }
                    }
                }
            }
If we break down what is going on here, all we have done is add an if statement to each of the IsKeyDown conditions. We check to see of the location on the map that the player is going to end up in if we allow them to move is a zero. If it is, we go ahead and initiate the move as before. If it isn't, we don't do anything.

We are also correcting a small bug that wasn't apparent before the addition of the player avatar and walkability. Our code that handles when we run off the end of the map was acting a little funky by leaving the X and Y coordinates one off if you walked to the right or bottom edge of the map. now we prevent this movement in all four directions with another if statement wrapper.

To finish fixing this little problem, replace the code block for "snaping" back with the following:
                // 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 = 0; iMoveCount = 0;
                }
                if (iMapY < 0) { iMapY = 0; iMapYOffset = 0; iMoveCount = 0; }
                if (iMapY > iMapHeight - iMapDisplayHeight)
                {
                    iMapY = iMapHeight - iMapDisplayHeight;
                    iMapYOffset = 0; iMoveCount = 0;
                }

With our new check on movement, this shouldn't ever happen anyway, but if we end up in a situation where we are somehow off the map, this will correct it. The change from the previous version of this code is the setting of iMapYOffset and iMapXOffset to 0 instead of the width/height of the tiles.

Fire it up!

If you run your code now, you should have a functioning multi-layer map with animated water and a player avatar that checks for walkability as your move around!


Coming Up...

In part 4, we will be focusing on how we handle the map and creating a simple map editor based on our tile engine. Then we can stop in-lining our map and open up larger maps and the possibility to link multiple maps































 
 
 
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