Part 9 - Player Character on an Isometric Map - Part 2


In Part 8, we added Vlad the Sword-Wielding Warrior to our Isometric tile map and got him running around on the screen. In this installment, we will refine Vlad's movement so that he transitions smoothly between height areas on the map and implement places on the map that Vlad cannot walk. Our base code for this installment will be the final project from Part 8.

We will implement two methods of blocking Vlad's movement. The first will be map cells that are simply declared as unwalkable. Perhaps they are lakes, walls, deadly lava, etc.

In order to implement these blocks, lets start by modifying the MapCell class to include a Walkable property. Add this under the current declarations for the various lists of tiles:

public bool Walkable { get; set; }


We will also need to initialize Walkable to true during the constructor for the MapCell class:

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


So, unless we change something, any given map cell will default to allowing Vlad to walk over it. Lets modify the constructor of the TileMap class to set a few tiles as non-walkable. Add the following near the end of the constructor:

            Rows[15].Columns[5].Walkable = false;
            Rows[16].Columns[6].Walkable = false;


Here we are simply setting the Walkable property for the two ground tiles that contain the small lake on our map to false. Now, we need to implement the actual code that will prevent Vlad from entering the square. We will stay in the TileMap for the moment and add a couple of helper functions to our class to make retrieving map cells based on a point in the world a bit easier. Add the following methods at the end of the class file:

        public Point WorldToMapCell(Vector2 worldPoint)
        {
            return WorldToMapCell(new Point((int)worldPoint.X, (int)worldPoint.Y));
        }

        public MapCell GetCellAtWorldPoint(Point worldPoint)
        {
            Point mapPoint = WorldToMapCell(worldPoint);
            return Rows[mapPoint.Y].Columns[mapPoint.X];
        }

        public MapCell GetCellAtWorldPoint(Vector2 worldPoint)
        {
            return GetCellAtWorldPoint(new Point((int)worldPoint.X, (int)worldPoint.Y));
        }


Up to now, we have returned the map location associated with a world point, and the first of these new methods is just a shortcut so we can use a Vector2 instead of converting manually to a Point value. For checking walkability, however, we are actually concerned with the map cell itself, and not its location in the world. GetCellAtWorldPoint uses the existing methods to look up the location of a map point and goes a step further, determining what actual map cell is at that point and returning it. Again, we have a second version that simply allows us to use a Vector2 instead of a Point.

In order to prevent Vlad from moving, head over to the Game1 class and navigate to the Update() method. Right after all of the IsKeyDown... checking, and before we check the length of the moveDir vector, add the following:

            if (myMap.GetCellAtWorldPoint(vlad.Position + moveDir).Walkable == false)
            {
                moveDir = Vector2.Zero;
            }


We use our new helper method to peek at the square Vlad would move into if the current movement were allowed to take place. If the square Vlad is moving into is not walkable, we cancel out the movement by setting moveDir to Vector2.Zero.


Vlad contemplates the lake - that he can't walk in anymore.


This will keep Vlad from walking into the lake, but he can still scale our highest mountains as if they weren't even there. The second kind of movement we want to restrict is movement into a walkable square that is too high above Vlad's feet for him to comfortably step upwards. In order to do this, we will need to implement some kind of slope detection system. This is also how we will smoothly transition between height levels on the map.

The technique we will use to do this should be pretty familiar. We are already using a variation of it in the current engine. If we look at the tiles in our tileset, it is obvious that there are tiles that are designed to be slopes, such as (I cut out the ones in this range that are elevated flat tops):



Examining these tiles, we can see that in our isometric tile set there are eight distinct types of slopes, divided into two types. The first type, such as the second and third tiles above, slope from one full side of the tile towards the opposite side. The second type, such as the first and fourth tiles) slope from a corner of the tile towards the opposite corner.

We can use this information to generate a "Slope Map" that will work just like our MouseMap image we use for identifying locations on the map. I took the existing MouseMap into an image editing program and arranged eight copies in a line. I then masked out the areas of the actual tile and drew gradients in each of the eight directions. The darker the location on the gradient, the higher the point on the tile. Here is what the slope maps look like:



Save the image above into the Textures\TileSets folder in your Content project and include it as a resource in your game.

We will need to modify the MapCell class once again, this time adding a property to identify the Slope Map associated with a map cell:

public int SlopeMap { get; set; }


Also, just as before, we need to initialize our new property in the constructor of the MapCell class. Add the following after the initialization for the Walkable property:

SlopeMap = -1;


Our slope maps will be numbered from 0 to 7, so a -1 indicates that there is no slope data associated with this cell - in other words, the cell is flat.

Head back over to the TileMap class, and lets add a definition for a Texture2D object to hold the Slope Map image above. This can be placed right under the declaration for the MouseMap.

private Texture2D slopeMaps;


Modify the constructor of the TileMap class to allow a slope map image to be passed. We'll set the variable we created above to this texture. Here are the first few lines of the constructor (I've left out all of the map building stuff that is below this:

public TileMap(Texture2D mouseMap, Texture2D slopeMap)
        {
            this.mouseMap = mouseMap;
            this.slopeMaps = slopeMap;


Go ahead over to the Game1 class an modify the declaration of our tile map in the LoadContent() method to include the slope map:

            myMap = new TileMap(
                Content.Load<Texture2D>(@"Textures\TileSets\mousemap"),
                Content.Load<Texture2D>(@"Textures\TileSets\part9_slopemaps"));


Back in the constructor for the TileMap class, lets add some tile definitions to create a small hill we can walk on. At the bottom of the constructor, add:

            Rows[12].Columns[9].AddHeightTile(34);
            Rows[11].Columns[9].AddHeightTile(34);
            Rows[11].Columns[8].AddHeightTile(34);
            Rows[10].Columns[9].AddHeightTile(34);

            Rows[12].Columns[8].AddTopperTile(31);
            Rows[12].Columns[8].SlopeMap = 0;
            Rows[13].Columns[8].AddTopperTile(31);
            Rows[13].Columns[8].SlopeMap = 0;

            Rows[12].Columns[10].AddTopperTile(32);
            Rows[12].Columns[10].SlopeMap = 1;
            Rows[13].Columns[9].AddTopperTile(32);
            Rows[13].Columns[9].SlopeMap = 1;

            Rows[14].Columns[9].AddTopperTile(30);
            Rows[14].Columns[9].SlopeMap = 4;


Here, we define four height tiles that for the top of the hill. Around the edges, we add Topper tiles (which don't add to the height of the map cell like the height tiles do). We indicate which of the slope maps correspond to each of the topper tiles we added (SlopeMap 0 rises from the southwest edge of the tile to the northeast edge, SlopeMap 1 rises from the southeast edge to the northwest, and Slopemap 4 rises from the south corner to the north corner).


A hill to climb!


Go ahead an execute your project at this point and you will see a small hill off to the right of the other terrain we have created. If you walk Vlad over to the hill, you will notice that he does not currently rise on the slopes and instead bounces up to the hilltop just as before. Lets add that smooth movement into our system.

Still in the TileMap class, add the following method at the end of the class file:

        public int GetSlopeMapHeight(Point localPixel, int slopeMap)
        {

            Point texturePoint = new Point(slopeMap * mouseMap.Width + localPixel.X, localPixel.Y);

            Color[] slopeColor = new Color[1];

            if (new Rectangle(0, 0, slopeMaps.Width, slopeMaps.Height).Contains(texturePoint.X, texturePoint.Y))
            {
                slopeMaps.GetData(0, new Rectangle(texturePoint.X, texturePoint.Y, 1, 1), slopeColor, 0, 1);

                int offset = (int)(((float)(255 - slopeColor[0].R) / 255f) * Tile.HeightTileOffset);

                return offset;
            }

            return 0;
        }


Given a pixel on a slop map, and a give slope map index, we use the same technique to determine the color of the pixel at that position that we did for Isometric Picking. One change to note here is that instead of units we are simply using Color objects. This simplifies things a bit, as we don't have to bitmask off the colors we want. Really, this is what we should have done for the Isometric Picking tutorial, and I'd like to say we didn't because you needed to learn how to use Uints and how XNA represents color data... But the truth is, I just didn't know you could do it this way at the time

Anyway, since we are using a greyscale slope gradient we could use any of the R, G, or B components of the color to determine how "dark" the color is (since they will all be the same number). In this case, we are using the red value (the R member of the Color class) - which returns a byte from 0 to 255.

By subtracting this number from 255 and then dividing the result by 255, we get a number between 0 and 1. A fully white pixel will have 255 for the R component. 255 - 25 = 0, divided by 255 = 0. A fully black pixel will have 0 for the R component. 255 - 0 = 255, divided by 255 = 1. We then multiply this number by the Tile.HeightTileOffset value, ending up with a value between 0 and the full HeightTileOffset. We return this number as the height at this point on the slope map.

We need a couple more helper functions to make this easier to work with, so lets add them now:

        public int GetSlopeHeightAtWorldPoint(Point worldPoint)
        {
            Point localPoint;
            Point mapPoint = WorldToMapCell(worldPoint, out localPoint);
            int slopeMap = Rows[mapPoint.Y].Columns[mapPoint.X].SlopeMap;

            return GetSlopeMapHeight(localPoint, slopeMap);
        }

        public int GetSlopeHeightAtWorldPoint(Vector2 worldPoint)
        {
            return GetSlopeHeightAtWorldPoint(new Point((int)worldPoint.X, (int)worldPoint.Y));
        }


Here we finally make use of the out parameter of the WorldToMapCell() method that has been hanging around since we first added the method. In the past, we've thrown this value away because we didn't need it. Out parameters can be used when a method needs to return more than a single return value. In our case, the primary thing we are returning from WorldToMapCell() is the map cell corresponding to a world coordinate, but using the out parameter we are also passing back the location within that cell that the world coordinate applies to.

By combining both outputs of WorldToMapCell(), we are able to pass both the local point and the slope map for this map cell to the GetSlopeMapHeight method and return the final value to the method's caller. As before, we have a helper overload that accepts a Vector2 instead of a Point to allow us to call the method more easily.

As it turns out, actually integrating the slopes into our code is pretty easy... Really only one line of code in addition to what we already have, but since we will be interested in reusing some of that code in a few minutes, lets refactor what we have now to vertically position Vlad into a more general function.

In the TileMap class, add the following two helper methods at the end of the file:

        public int GetOverallHeight(Point worldPoint)
        {
            Point mapCellPoint = WorldToMapCell(worldPoint);
            int height = Rows[mapCellPoint.Y].Columns[mapCellPoint.X].HeightTiles.Count * Tile.HeightTileOffset;
            height += GetSlopeHeightAtWorldPoint(worldPoint);
            
            return height;
        }

        public int GetOverallHeight(Vector2 worldPoint)
        {
            return GetOverallHeight(new Point((int)worldPoint.X, (int)worldPoint.Y));
        }


This is very similar to the code we currently have in the Game1 class that calculates Vlad's height, except that we additionally call GetSlopeHeightAtWorldPoint() and add that to the height we will be returning. Now jump back over to Game1 and locate the code in the Draw() method where we draw Vlad. Remove the two lines above that (Point vladStandingOn... and int vladHeight...). Additionally, change the draw call that draws Vlad to:

vlad.Draw(spriteBatch, 0, -myMap.GetOverallHeight(vlad.Position));


Go ahead and run your project and wander up and down the ramps we have built!

But we have two more problem we need to fix... If you walk Vlad over to the small hill, you will notice that he can still scale the highest mountains instantly because we don't check to see how much vertical movement we will allow Vlad in a single Update() frame. Secondly, even when he is behind a terrain feature, Vlad kinda looks like he is standing on top of it anyway. This is because we have not yet taken into account the draw depth we need to place Vlad at.

In order to account for the draw depth, go to the Game1 class and down to the Draw() method. Add the following line right after the declaration for the depthOffset float before the loop that draws the tile map:

Point vladMapPoint = myMap.WorldToMapCell(new Point((int)vlad.Position.X, (int)vlad.Position.Y));


Scroll down a bit, and add the following right after the last portion of the loop that draws the Topper tiles (but still inside the loop that draws the map):

                    if ((mapx == vladMapPoint.X) && (mapy == vladMapPoint.Y))
                    {
                        vlad.DrawDepth = depthOffset - (float)(heightRow+2) * heightRowDepthMod;
                    }


At the top of the loop, we store Vlad's map cell position. As we are processing the loop, we check to see if we are currently drawing the cell Vlad is standing in. If we are, we assign Vlad's DrawDepth property to the current draw depth, plus two increment of heightRowDepthMod. If we only add one, Vlad will appear to be underneath portions of adjoining tiles when standing on slopes.

In order to prevent Vlad from becoming too proficient of a mountain climber, we simply need to check his position before and after a potential move and see if the height difference between the two positions is too high. In the Update() method, right after the code we added to zero out the moveDir vector if the square Vlad wants to move into is not walkable, add the following:

if (Math.Abs(myMap.GetOverallHeight(vlad.Position) - myMap.GetOverallHeight(vlad.Position + moveDir)) > 10)
{
    moveDir = Vector2.Zero;
}


We are doing pretty much the same thing with this check as we did with the walkability check. We get the height of Vlad's current position, and the height of his new position if we allow him to move. If the difference between these two positions is greater than 10, we zero out the moveDir vector. This will prevent Vlad from climbing tall cliffs, and also prevent him from leaping off of them as well.

Go ahead and execute your project. You should now have a fairly functional character that can roam the map.


Playing hide-and-seek with Vlad


That wraps up this installment of the tutorial series! As usually, I don't know exactly when the next part will arrive, but there will be additional installments in the future. For now, grab the code for this part here:









































 

 
 
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