Part 8 - Player Character on an Isometric Map - Part 1


In Part 4 of the series, we looked at creating an isometric game map with multiple layers and height levels, and in Part 7 we introduced a method to map a pixel to a map cell. In this installment we will look at creating a player-controlled character to add to the map and wander around. You'll want to begin with the code base at the end of Part 7, which can be downloaded here.

The Character


I know that I have mentioned in the past that I can't draw. At all. So I'm going to make use of graphics by Reiner "Tiles" Prokein over at http://www.reinerstilesets.de/. Reiner provides a huge selection of 2D and 3D graphics free for both commercial and non-commercial use.

After browsing through the character sprites, I've settled on "Vlad Sword" from this page. I downloaded the bitmap version of this file, resized all of the images to 48x48 pixels, and ran the "Walking" images through a simple little sprite sheet builder program I put together, opened that up in an image editor and set the background color to transparent, and ended up with an animation sheet for Vlad:



In your Content project, create a new folder under Textures called Characters, and add the image above to the folder.

At this point, we are going to borrow a bit from the Sprite Engine Tutorial series. Since there is not much point in simply repeating all of the same information here that is in that series, we will simply lift the FrameAnimation.cs, SpriteAnimation.cs, and MobileSprite.cs files out of the final Sprite Engine project and place them in a folder called "Sprites" within our Tile Engine project. Open each of these CS files and change the "namespace" from "SpriteTesting4" to "TileEngine". Your Solution Explorer should now look something like this (I left off the TileSets as the end of this image):



We need to make a couple of small updates to the SpriteAnimation class. First, add two new properties to the class:

public Vector2 DrawOffset { get; set; }
public float DrawDepth {get; set; }


In the constructor for the SpriteAnimation class, set DrawOffset to Vector2.Zero, and DrawDepth to 0f by default:

DrawOffset = Vector2.Zero;
DrawDepth = 0.0f;


Finally, modify the Draw() method to add the DrawOffset to the position where the sprite is drawn and take the DrawDepth property into account, and also utilize the location of our Camera. Here is the new Draw() method:

        public void Draw(SpriteBatch spriteBatch, int XOffset, int YOffset)
        {
            if (bAnimating)
                spriteBatch.Draw(t2dTexture,
                    Camera.WorldToScreen(v2Position) + v2Center + DrawOffset + new Vector2(XOffset,YOffset),
                    CurrentFrameAnimation.FrameRectangle, colorTint,
                    fRotation, v2Center, 1f, SpriteEffects.None, DrawDepth);
        }


What we have done here is provide a way for us to treat the sprite as if it is in a particular position while, in reality, drawing it in a slightly different position. The reason we want to do this is that we want to keep track of where the feet of our character are located, not the upper left corner of the sprite rectangle that the character is contained within.

By including the Draw offset, we can use the SpriteAnimation's Position property to stand in for a useful location instead of constantly adding an offset to our comparison code to figure out where the sprite really is standing.

Adding the Character



Begin by creating a declaration for the character in the Game1.cs file's declaration area:

SpriteAnimation vlad;


Now, in the LoadContent() method, lets set up everything we need to create the sprite for the player's character:

            vlad = new SpriteAnimation(Content.Load<Texture2D>(@"Textures\Characters\T_Vlad_Sword_Walking_48x48"));

            vlad.AddAnimation("WalkEast", 0, 48*0, 48, 48, 8, 0.1f);
            vlad.AddAnimation("WalkNorth", 0, 48*1, 48, 48, 8, 0.1f);
            vlad.AddAnimation("WalkNorthEast", 0, 48*2, 48, 48, 8, 0.1f);
            vlad.AddAnimation("WalkNorthWest", 0, 48*3, 48, 48, 8, 0.1f);
            vlad.AddAnimation("WalkSouth", 0, 48*4, 48, 48, 8, 0.1f);
            vlad.AddAnimation("WalkSouthEast", 0, 48*5, 48, 48, 8, 0.1f);
            vlad.AddAnimation("WalkSouthWest", 0, 48*6, 48, 48, 8, 0.1f);
            vlad.AddAnimation("WalkWest", 0, 48*7, 48, 48, 8, 0.1f);

            vlad.AddAnimation("IdleEast", 0, 48 * 0, 48, 48, 1, 0.2f);
            vlad.AddAnimation("IdleNorth", 0, 48 * 1, 48, 48, 1, 0.2f);
            vlad.AddAnimation("IdleNorthEast", 0, 48 * 2, 48, 48, 1, 0.2f);
            vlad.AddAnimation("IdleNorthWest", 0, 48 * 3, 48, 48, 1, 0.2f);
            vlad.AddAnimation("IdleSouth", 0, 48 * 4, 48, 48, 1, 0.2f);
            vlad.AddAnimation("IdleSouthEast", 0, 48 * 5, 48, 48, 1, 0.2f);
            vlad.AddAnimation("IdleSouthWest", 0, 48 * 6, 48, 48, 1, 0.2f);
            vlad.AddAnimation("IdleWest", 0, 48 * 7, 48, 48, 1, 0.2f);

            vlad.Position = new Vector2(100, 100);
            vlad.DrawOffset = new Vector2(-24, -38);
            vlad.CurrentAnimation = "WalkEast";
            vlad.IsAnimating = true;


Again, all of this is pretty standard stuff from the Sprite Engine tutorial... We are creating an instance of the SpriteAnimation class and assigning named animations to frame sequences within the sprite sheet. Our character will begin at position 100,100 on the map, and we are specifying that the drawing of the sprite should be displaced by (-24, -38) pixels. If we were to measure our sprite, we would find that the characters feet are at about 24, 38 in each frame, so what we are really doing here is specifying the position we will use when determining where the character is standing (based on our Picking code from Part 7).

In the Draw() method, before the code that draws the hilight under the mouse cursor, add a line to draw the player:

vlad.Draw(spriteBatch, 0, 0);


Now, in the Update() method, lets modify the way we handle input. Go ahead and remove the existing code from the method. Here is the entire new method:

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

            Vector2 moveVector = Vector2.Zero;
            Vector2 moveDir = Vector2.Zero;
            string animation = "";

            KeyboardState ks = Keyboard.GetState();

            if (ks.IsKeyDown(Keys.NumPad7))
            {
                moveDir = new Vector2(-2, -1);
                animation = "WalkNorthWest";
                moveVector += new Vector2(-2, -1);
            }

            if (ks.IsKeyDown(Keys.NumPad8))
            {
                moveDir = new Vector2(0, -1);
                animation = "WalkNorth";
                moveVector += new Vector2(0,-1);
            }

            if (ks.IsKeyDown(Keys.NumPad9))
            {
                moveDir = new Vector2(2, -1);
                animation = "WalkNorthEast";
                moveVector += new Vector2(2,-1);
            }

            if (ks.IsKeyDown(Keys.NumPad4))
            {
                moveDir = new Vector2(-2, 0);
                animation = "WalkWest";
                moveVector+= new Vector2(-2,0);
            }

            if (ks.IsKeyDown(Keys.NumPad6))
            {
                moveDir = new Vector2(2, 0);
                animation = "WalkEast";
                moveVector += new Vector2(2,0);
            }

            if (ks.IsKeyDown(Keys.NumPad1))
            {
                moveDir = new Vector2(-2, 1);
                animation = "WalkSouthWest";
                moveVector += new Vector2(-2, 1);
            }

            if (ks.IsKeyDown(Keys.NumPad2))
            {
                moveDir = new Vector2(0, 1);
                animation = "WalkSouth";
                moveVector += new Vector2(0,1);
            }

            if (ks.IsKeyDown(Keys.NumPad3))
            {
                moveDir = new Vector2(2, 1);
                animation = "WalkSouthEast";
                moveVector += new Vector2(2,1);
            }

            if (moveDir.Length() != 0)
            {
                vlad.MoveBy((int)moveDir.X, (int)moveDir.Y);
                if (vlad.CurrentAnimation!=animation)
                  vlad.CurrentAnimation=animation;
            }
            else
            {
                vlad.CurrentAnimation = "Idle" + vlad.CurrentAnimation.Substring(4);
            }

            vlad.Update(gameTime);

            base.Update(gameTime);
        }


We don't simply want to scroll the map around directly with the arrow keys anymore. Instead, we can now use the numeric keypad to march Vlad around the screen in any of eight different directions. When a direction is selected, we create a vector pointing in that direction to eventually update Vlad's position with, and also select an animation to use. We only actually update the CurrentAnimation value if it is different than what it already is. This way, if we hold down a direction key, Vlad will play through the animation in that direction instead of starting over at the first frame of the animation during each Update() cycle.

If Vlad has stopped moving, we use the "Idle" animations by adding the portion of the name of the current animation minus the first four characters ("Walk") meaning we will end up with values like "IdleNorth", "IdleSouthWest", etc. This allows Vlad to continue facing a particular direction when he stops moving instead of always snapping back to a pre-determined facing.

Go ahead and execute your code. You should now have Vlad standing in the upper left portion of the display, and be able to walk around with the numeric keypad. Right away, though, you will notice that there are a couple of issues that need to be resolved:

  • You can wander Vlad right off the edges of the map/screen.
  • Vlad does not interact with terrain at all. He simply floats over top of the screen, passing above elevated terrain as if it wasn't there

We will tackle these issues one at a time, beginning with screen scrolling. Jump back to your Game1.cs file's Update() method, and add the following right before the call to vlad.Update(baseTime):

            float vladX = MathHelper.Clamp(
                vlad.Position.X, 0 + vlad.DrawOffset.X, Camera.WorldWidth);
            float vladY = MathHelper.Clamp(
                vlad.Position.Y, 0 + vlad.DrawOffset.Y, Camera.WorldHeight);

            vlad.Position = new Vector2(vladX, vladY);


This will keep Vlad within the bounds of the game world, but it won't scroll the screen as he approaches the edges. In order to account for that, lets add the following code after what we just placed above:

            Vector2 testPosition = Camera.WorldToScreen(vlad.Position);

            if (testPosition.X < 100)
            {
                Camera.Move(new Vector2(testPosition.X - 100, 0));
            }

            if (testPosition.X > (Camera.ViewWidth - 100))
            {
                Camera.Move(new Vector2(testPosition.X - (Camera.ViewWidth - 100), 0));
            }

            if (testPosition.Y < 100)
            {
                Camera.Move(new Vector2(0, testPosition.Y - 100));
            }

            if (testPosition.Y > (Camera.ViewHeight - 100))
            {
                Camera.Move(new Vector2(0, testPosition.Y - (Camera.ViewHeight - 100)));
            }


As Vlad approaches the edges of the screen, we attempt to move the camera to keep him 100 pixels away from the edge. This will allow him to actually approach closer than 100 pixels if the camera is clamped and not allowed to move any further in that direction. Go ahead and start your project again and run around on the map.

Now to handle height transitions (at least partially). If we look up the position of Vlad on the map, we can get the map cell he currently resides in. We can then check the number of height tiles that exist at that location and adjust Vlad's height accordingly. Go to the call to Vlad.Draw() in the Game1.cs file's Draw() method and replace it with the following:

Point vladStandingOn = myMap.WorldToMapCell(new Point((int)vlad.Position.X, (int)vlad.Position.Y));
int vladHeight = myMap.Rows[vladStandingOn.Y].Columns[vladStandingOn.X].HeightTiles.Count * Tile.HeightTileOffset;
vlad.Draw(spriteBatch, 0, -vladHeight);


We get the map cell where Vlad is standing, and look up it's HeightTiles.Count. By multiplying this by the Tile.HeightTileOffset value, we get the number of pixels Vlad needs to be elevated to appear to walk on top of the existing tile images. Note that Vlad can walk through the tall grass without getting bumped upward because the grass was added to the "Topper Tiles" list, and don't figure into the height of the tile.



We will wrap this installation of the tutorial series here. In Part 9, we will look at:

  • Smoothly transitioning Vlad between levels on the map so he doesn't "Jump" to new heights when crossing tiles
  • Identify locations on the map where Vlad can't walk and prevent him from entering them





































 

 
 
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