Part 3 - Hexagonal Maps


Hex Maps have always been popular in pen-and-paper games, particularly war and role-playing games. Back in the Visual Basic 5 days, I put together a simple hex-mapping program to draw RPG maps, but the performance of the mapper left quite a bit to be desired.

When XNA appeared on the scene, I had the opportunity to update my old mapping software, and I put together a WinForms/XNA application to create hex maps that I've used for a couple of different RPGs over the years. The hex engine we are going to cover in this tutorial is the pretty much the base of what I put together for my mapping software.

How is a Hex Map Different


The obvious answer to that question, of course, is that a hex map uses six-sided hexagons instead of four-sided squares to represent cells on the map. From a game rules standpoint, this means that all distances can be measured in "hexes", because the distance from the center of any one hex to the center of any of it's neighbor hexes is always the same. There are no "diagonal" directions that are further than the squares in the cardinal directions.

For us, however, we will still be representing the game map as a 2-dimensional array of MapCell objects. What will change is the way we display this grid. Consider the illustration below:



In order to draw the 2D square grid as hexagons, we spread each row out, leaving enough room for the edge of one hex to fit between the corners of the tiles in a row. Every other row we shift to the right by an amount to allow it to align with the row above it and fill those gaps. In order to do this, lets modify our Tile definition to add a few more parameters.

When we draw our tile map, instead of using the TileWidth and TileHeight to determine how the tiles are positioned on the screen in relation to each other, we will use TileStepX and TileStepY instead. The last new value, OddRowXOffset determines how far to the right we push every other row of tiles, resulting in the staggered pattern we see above.

A Hex Tile Set



Of course, to create a hex tile map, we will need a hex tile sheet to work with. This is a simple sheet which just contains hexes filled with colors to represent various terrain types, in keeping with the idea of creating pen-and-paper RPG style maps.



Go ahead and add the tileset to your project. In the LoadContent() method of the Game1 class, load this tileset instead of the one from Part2:

Tile.TileSetTexture = Content.Load(@"Textures\TileSets\part3_tileset");


Here is a close up view of an individual tile, with the measurements we will need to set the values in our Tile class:



Updating the Tile Class


We will need to modify the Tile class to account for our tile sizes and to introduce our new spacing variables. Our tiles in this set are 33 pixels wide and 27 pixels high, so our first step is to update our TileWidth and TileHeight variables appropriately:

static public int TileWidth=33;
static public int TileHeight=27;


Note that even though we have hex-shaped tiles, they are still contained within rectangles. The "Magic Pink" color surrounding each tile (RGB 255, 0, 255) is automatically treated as transparent by the XNA Framework's texture content processor.

Let add our new spacing variables right below the TileWidth and TileHeight values. Horizontally, tiles will be spaced equal to the width of the tile image (33) plus the width of the top side of the tile itself (19), or 52 pixels. Vertically, the will be spaced so that the slanted edge of one tile aligns with it's neighbor, or the 14 pixels indicated in the diagram above.

static public int TileStepX=52;
static public int TileStepY = 14;


Finally, every other row will be offset to align the edges of the tiles properly. The amount we need to push each odd row to the side is equal to the width of the top edge of the tile (19) plus the empty space along the top right (7) for a total of 26 pixels:

static public int OddRowXOffset = 26;


That takes care of the Tile class, so now we need to update the rest of our engine to utilize these new values.

Note: You can make these updates to your engine and still support square maps just fine. In the case of a square map, TileStepX is equal to the TileWidth, TileStepY is equal to the TileHeight, and OddRowXOffset is equal to zero.

Cleaning Up the Map


We added some tiles on an upper layer in Part 2 of this series, and since we don't have nearly as many tiles in this tileset (nor tiles drawn to be placed over things) they will just look weird if we leave them in there. Go into your TileMap class and remove (or comment out) all of the lines near the end of the file that call "AddBaseTile()". There is no reason you can't use layering with the Hex maps just like you did with square maps, but we're going to skip that for now. We'll revisit layering extensively in Part 4 when we look at isometric maps.

Modifying the Draw Code


We need to update our Draw() method in the Game1 class to use our new variables, both in the actual SpriteBatch.Draw() calls and in the calculations to determine what tile should be displayed where. In the declaration for "firstSquare", replace TileWidth and TileHeight with TileStepX and TileStepY:

Vector2 firstSquare = new Vector2(Camera.Location.X / Tile.TileStepX, Camera.Location.Y / Tile.TileStepY);


Similarly, update the declaration for squareOffset:

Vector2 squareOffset = new Vector2(Camera.Location.X % Tile.TileStepX, Camera.Location.Y % Tile.TileStepY);


Inside the first for loop (but above the second for loop) we will create a variable to track what row we are displaying:

int rowOffset = 0;
if ((firstY + y) % 2 == 1)
    rowOffset = Tile.OddRowXOffset;


Now, lets update the SpriteBatch.Draw() call itself:

spriteBatch.Draw(
    Tile.TileSetTexture,
    new Rectangle(
        (x * Tile.TileStepX) - offsetX + rowOffset, 
        (y * Tile.TileStepY) - offsetY,
        Tile.TileWidth, Tile.TileHeight),
    Tile.GetSourceRectangle(tileID),
    Color.White);


The only real changes here are replaceing TileWidth and TileHeight with TileStepX and TileStepY, and adding the rowOffset value to the X portion of the rectangle definition.

Before we run the code, lets make one last change to the Draw() method. The very first line of the method clears the screen to a light blue color. Lets change that to black:

GraphicsDevice.Clear(Color.Black);


Execute your code, and you will see your hex-based map, with a one pixel black border between each tile.



Note: You can remove the black border around each tile by lowering the TileStepX, TileStepY, and OddRowXOffset values to 50, 13, and 25 respectively, but for an RPG map, the border gives a nice effect.

Fixing the Scrolling


If you were to scroll your map to the edges, you would find that sideways you could not reach the edge of the map, while moving downwards you can crash your program. This is because our code that constrains the position of the camera is still using TileWidth and TileHeight and needs to be updated to use Steps as well. In the Update() method of the Game1 class, change TileWidth and TileHeight to TileStepX and TileStepY:

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.TileStepX);
    }

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

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

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

    base.Update(gameTime);
}


Clearing up the Edges


Our last few modifications are simply to make things look a little better. Right now, there is a black edge on the top and left of the map when we are in the default (0,0) camera position. We can correct this by setting a global base offset for our tiles in both the X and Y directions. Add these variables at the top of the Game1 class:

int baseOffsetX = -14;
int baseOffsetY = -14;


Update the SpriteBatch.Draw() call in the Draw() method to include these two values when determining the destination drawing rectangle:

spriteBatch.Draw(
    Tile.TileSetTexture,
    new Rectangle(
        (x * Tile.TileStepX) - offsetX + rowOffset + baseOffsetX, 
        (y * Tile.TileStepY) - offsetY + baseOffsetY,
        Tile.TileWidth, Tile.TileHeight),
    Tile.GetSourceRectangle(tileID),
    Color.White);


Finally, lets modify the rows and columns of map we display. At the top of the Game1 class, update squaresAcross and squaresDown:

int squaresAcross=17;
int squareDown=37


As you can see, the staggering of the rows means we need a LOT more rows to cover the display screen. You only get to scroll downward for a few seconds before you reach the bottom of the map. In contrast, we need less columns displayed because the columns are spaced wider than the tile size.






































 

 
 
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