Thursday, December 24, 2009

Cell to Cell Distance in a 2D Hexagonal Tile Map

I needed to grab the distance between any two cells in a hex grid, and I found it was more difficult than I originally thought it would be.. There were lots of blogs and theory sites which had distance formulas, but none of them worked for every cell in the grid, and most of them didn't work at all! Wtf! Most of them didn't even work on the grids they claimed they worked on.. I ended up creating my own distance function, feel free to use it if you find it handy!

FireTeams uses an isometric hex-tile grid for its game. Once your tiles have been made, these grids are fairly simple to create and implement. The only real trouble I had with this style of grid was grabbing the distance (measured by the number of tiles a unit would have to traverse) between any two tiles on the grid. Here's the function as a whole, I'll proceed to explain each part throughout the rest of the post:

Update: An anonymous poster found a bug in my implementation below, and graciously posted a fixed-up version! See the comments for his working algorithm. Thanks, Anonymous!!


var dx = Math.abs(start.x - end.x);
var dy = Math.abs(start.y - end.y);
var distance = 0;

if (dx < dy/2 )
      distance = dy;
else
      distance = dy + dx - Math.floor(dy/2.0);

if (start.y % 2 == 0)
      if (dy % 2 == 1 && start.x > end.x)
            distance--;
else
      if (dy % 2 == 1 && start.x < end.x)
            distance--;

return distance;


The distance formula wasn't as simple as it is in a standard square-tile grid. For one, the hex-tile grid allows units to move in exactly 6 directions instead of the 4 or 8 (if diagonal movement is allowed) directions of movement. Two, because of the nature of hex tiles, every other row or column (I guess this depends on how your tiles are oriented), is offset. The grid implementation I used is as follows:


When the tiles are layed out in this format, it's easy to see that the Y coordinates increase at approximately twice the rate as the X coordinates when traversing the tiles south. This gives us a really easy special case for the distance:

if (dx < dy / 2) distance = dy;

In other words, any time the horizontal distance is less than half the vertical distance, the shortest path between the two cells will always be the vertical distance. For example, starting at cell <0,0> on the grid above, the cells <0,7> through <3,7> can all be reached in exactly 7 steps.

In fact, the minimum distance between any two cells can be visualized pretty simply: travel diagonally towards the cell until your y-coordinates are the same, then travel horizontally until you've reached the cell. This leads us to the more general case for the distance between our hex tiles:

distance = dy + dx - Math.floor(dy / 2.0);

This term requires a slight bit of explaining. Since the shortest path between two cells require traveling vertically and horizontally, it makes sense to add the dy and dx terms together. This over-estimates the actual distance. This can be seen easily by looking at the distance to go from cell <0,0> to cell <4,7>. Adding the dx and dy terms (which are 4 and 7 respectively) give us a distance of 11, while the actual distance is only 8. The distance of 11 would be true if our unit were to travel directly south, then directly east. Since we are trying to get to cell <4,7> in the shortest distance, we would actually travel along the diagonal all the way down to cell <3,7>, and then move one more east to cell <4,7>. To compensate for this shorter distance, we can use our observation from above: when traveling south in a diagonal, the x-coordinate increases at half the rate the y-coordinate does. You can see on the grid, when traveling in the diagonal from <0,0> to <3,7>, the distance dx increases by 1 tile everytime dy increases by 2. The further we travel along a diagonal, the bigger this dx term gets. This increase in dx is not wanted in our calculation, since these extra tiles do not need to be traversed in order to get to our destination, so we want to subtract them. The term:

Math.floor(dy / 2.0)

gives us that extra distance generated for dx as dy increases, so we can simply subtract this from the addition of dy and dx to get our distance! Yay!

...Almost done! Most tiles will now give correct distances, but not all of them. There is still one more special case we have to tackle, and this is a direct cause of the offsetting done on a row x row basis. Because the X-coordinate is offset every other row, about 1/4 of our calculated tiles get calculated by 1 too many tiles. Cells sitting on the offset rows are overestimated by 1. In particular, depending on which row our start tile is in, either the tiles in the odd rows on the east of the start tile are overestimated, or the tiles on the west side of the tile are overestimated. This is an easy special case:

if (start.y % 2 == 0)
      if (dy % 2 == 1 && start.x > end.x)
            distance--;
else
      if (dy % 2 == 1 && start.x < end.x)
            distance--;

In other words, for the first half of the statement, if our start tile is in a non-offset row, and our dy term is even, and the destination tile is west of the start tile, then our distance will be over-estimated, and we should decrease by a value of 1. If our start tile is on an offset row, then the over-estimated tiles are to the east of the start tile.


So there you have it! A simple function for calculating the distance between any two cells in a hex-tile grid. I hope others find this useful as well! Cheers, and I hope everyone has a good Christmas Eve!

10 comments:

  1. I have tried this algorithm on a hex grid I'm creating, it uses the same "pointy" hex grid orientation as you. While this algorithm works in most cases, it fails on some. For example, the distance between cells (4,4) and (3,1) is 2. Did you ever see this issue and work out a solution?

    ReplyDelete
  2. @Anonymous

    Hmm, I never did update this apparently! Yes, after a while i finally realized this solution had many flaws.. I eventually gave up on a mathematical approach, and used a depth-first search algorithm. This approach had three advantages: it gave me the distance between the tiles, it gave me the shortest path between those two tiles, and I was easily able to add obstacles in the map and the algorithm automatically accounted for it.

    Of course, my game is turn-based, so this solution works well for me but if you're game is real-time and you're constantly determining the distances between two hexes that are far apart, you'll probably hit some performance issues. Of course, you could always use another, more efficient search algorithm, such as A* search.

    I hope this helps!

    Brixican-

    ReplyDelete
  3. Thanks. Here is a C++ implementation that I believe resolves the issues:

    ----

    int HexGrid::distance(const HexCell& cell1, const HexCell& cell2)
    {
    int distance = 0;

    int cell2x = cell2.x;
    int dy = std::abs(cell1.y - cell2.y);
    //
    // If dy is odd, then reflect the x coordinate about the starting cell, do a similar
    // reflection if dy is even.
    if (cell1.y % 2 == 0 && cell2.x < cell1.x && dy % 2 == 1)
    {
    cell2x = cell1.x + (cell1.x - cell2.x) - 1;
    }
    else if (cell1.y % 2 == 1 && cell2.x > cell1.x && dy % 2 == 1)
    {
    cell2x = cell1.x - (cell2.x - cell1.x) + 1;
    }

    int dx = std::abs(cell1.x - cell2x);

    if (dx < dy/2)
    {
    distance = dy;
    }
    else
    {
    distance = dy + dx - static_cast(std::floor(dy/2.0f));
    }

    if (cell1.y % 2 == 0)
    {
    if (dy % 2 == 1 && cell1.x > cell2x)
    {
    distance--;
    }
    }
    else
    {
    if (dy % 2 == 1 && cell1.x < cell2x)
    {
    distance--;
    }
    }

    return distance;
    }

    ReplyDelete
  4. I posted that too quickly, you can actually remove the following code from the end:

    ---
    if (cell1.y % 2 == 0)
    {
    if (dy % 2 == 1 && cell1.x > cell2x)
    {
    distance--;
    }
    }
    else
    {
    if (dy % 2 == 1 && cell1.x < cell2x)
    {
    distance--;
    }

    ReplyDelete
  5. @Anonymous

    This is excellent, thank you very much for sharing your fixed version! I'll replace the code in my post with your improved code once I get the chance. Thanks again!

    Brixican-

    ReplyDelete
  6. Hi!
    Unfortunately this algorithm is not 100% ok. Starting with distances of 3 it sometimes returns 2 instead of 3 (2 out of 18 with distance of 3 return 2)
    Nevertheless it is the best I've found sofar...

    ReplyDelete
  7. Corrected the algorithm. Now it works better. Tested it with even & odd tiles to start, and up to distance of 4. I will write you if I find any problems for larger distances...

    2 changes: 1) return distance in first if (no need to correct it if dx is much larger than dy) 2) I have to use ciel instead of floor (but could be due to the fact that my hexagons are turned. 0/0 is still in the upper left corner, but the hexagons are standing on an edge and not on a peak)

    Here my algorithm (I don't try to change yours, since it could be that 2) is not necessary):
    public int distanceTo(Position other) {
    int rowDistance = Math.abs(this.row - other.row);
    int columnDistance = Math.abs(this.column - other.column);
    int distance;

    if (rowDistance < ((columnDistance + 1) / 2)) return columnDistance;
    distance = (columnDistance + rowDistance) - (columnDistance / 2);

    if ((this.column % 2) == 0) {
    if (((columnDistance % 2) == 1) && (this.row < other.row)) {
    distance-- ;
    }
    } else if (((columnDistance % 2) == 1) && (this.row > other.row)) {
    distance-- ;
    }

    return distance;
    }

    ReplyDelete
    Replies
    1. and if i want this code for a grid 90° turned?

      Delete
    2. and if i need this code for a grid 90° turned? please help me ):

      Delete
  8. I am interested in working together. Take a look at what I have done and I'll do the same
    Hexagonal floor Tile

    ReplyDelete