 # Map coordinates

2020-01-31

Hello and welcome to another blog post about the development of Nebuchadnezzar. This time we will look at the core of almost every isometric game: the map. Specifically, we’ll go over two different map coordinate systems and show you the one we use.

Typically, isometric games take place on a tile map. There are two basic coordinate systems for these maps: Staggered or Diamond. Games usually use one or the other. We use Staggered in Nebuchadnezzar. Although it looks complicated at first sight, we have several good reasons for choosing this system.

Staggered vs Diamon overview.

Let’s take a look.

From the beginning of Nebuchadnezzar’s development, we knew we wanted the maps to have a rectangular shape because it’s much more immersive. If all tiles are in range (0 - [map width], 0 - [map height]) then the Staggered map system retains a rectangular shape where as a Diamond map would have a rhomboid shape, and therefore, wouldn’t fit the whole screen.

If all tiles are in this range, then we can store them all in the matrix data structure. This provides fast access to arbitrary tiles. This is very advantageous—mainly during rendering, when in each frame we need to iterate over many tiles.

Staggered vs Diamon in (0 - [map width], 0 - [map height]) range.

It’s connected with another big advantage. Because rendering is one of the most compute consuming parts of a game, we only want to render what’s necessary. This typically means: only the tiles visible on a player’s screen. In the Staggered system’s rectangular cutout of the screen, all the tiles will be in range ([x1] - [x2], [y1] - [y2]). Thanks to this, we can exclusively iterate over this sub-matrix of tiles. Notice the diagram of the Diamond system—there isn’t a simple way to describe its range.

Staggered vs Diamon in screen cutout.

Of course, this system has disadvantages too. Mainly, it’s ineffective at interpreting coordinates. This is a problem, for example, when moving objects. If we want to move in the four main directions, the Diamond system makes computation of neighboring tiles very simple. Just add or subtract 1 in the [x] or [y] axis. This gives us all four directions. In the Staggered system, it’s more complicated, and also the formula differs for odd and even rows.

In general, most of the operations in the Staggered system are more complicated. Another example being the distances between two tiles (with limitation to the four main directions) or even the rotation of the whole system, which in itself can be a separate blog post. In the end, we have transformation functions between the Staggered and Diamond systems. And for more complicated operations, we may use the more convenient one.

Staggered vs Diamon in direction computation.

With ongoing developments, there’s a growing number of coordinate operations we need. Although none of the operations are complex, it can be difficult to invent and test them. Under the text, you will find snippets of our source code with some of coordinate operations. You can use the code in your own project or to test your spatial orientation.

Next time, we’ll look at another type of coordinate system as well as other operations. For example, an important element is the transformation between screen space and game space. We hope you learned something new about isometric games in this post. Feel free to ask any questions about this topic or any other.

See you soon!

#### Get neighbor tile in the given direction.

``````/**
* @brief Get neighbor tile in the given direction.
*
* @param pos Cooridinates of origin tile.
* @param direction Neighbor direction in range <0, 7> for NE, E, SE, S, SW, W, NW, N.
*/
N_Point Tile_Scene::neighbor(const N_Point pos, const int direction) {
const int x = pos.x;
const int y = pos.y;

switch (direction) {
case 0:
return {x + std::abs(y % 2), y - 1};
case 2:
return {x + std::abs(y % 2), y + 1};
case 4:
return {x - std::abs((y + 1) % 2), y + 1};
case 6:
return {x - std::abs((y + 1) % 2), y - 1};
case 1:
return {x + 1, y};
case 3:
return {x, y + 2};
case 5:
return {x - 1, y};
case 7:
return {x, y - 2};
}

n_assert_msg("Invalid parameters.");
return N_Point();
}``````

#### Get direction between two neighboring tiles.

``````/**
* @brief Get direction between two neighboring tiles.
*
* @param from Cooridinates of origin tile.
* @param to Cooridinates of destination tile.
*
* @return Direction in range <0, 7> for NE, E, SE, S, SW, W, NW, N.
*/
int Tile_Scene::direction(const N_Point from, const N_Point to)
{
const int x = to.x - from.x;
const int y = to.y - from.y;

if ((x == std::abs(from.y % 2)) && (y == -1)) {
return 0;
}
else if ((x == std::abs(from.y % 2)) && (y == 1)) {
return 2;
}
else if ((x == -std::abs((from.y + 1) % 2)) && (y == 1)) {
return 4;
}
else if ((x == -std::abs((from.y + 1) % 2)) && (y == -1)) {
return 6;
}
else if ((x == 1) && (y == 0)) {
return 1;
}
else if ((x == 0) && (y == 2)) {
return 3;
}
else if ((x == -1) && (y == 0)) {
return 5;
}
else if ((x == 0) && (y == -2)) {
return 7;
}

n_assert_msg("Invalid parameters.");
return 0;
}``````

#### Transformation between staggered and diamond system.

``````/**
* @brief Get diamond coordinates for given staggered coordinates.
*
* @param pos Staggered coordinates.
*/
N_Point Tile_Scene::to_diamond(const N_Point pos)
{
return {((pos.y + 1) / 2) + pos.x + (((pos.y + 1) % 2) * (pos.y < 0)), (pos.y / 2) - pos.x + ((pos.y % 2) * (pos.y < 0))};
}

/**
* @brief Get staggered coordinates for given diamond coordinates.
*
* @param pos Diamond coordinates.
*/
N_Point Tile_Scene::from_diamond(const N_Point pos)
{
if (pos.x % 2) {
return {(pos.x / 2) - ((pos.y - (pos.y < 0)) / 2) + ((pos.x % 2) * (pos.x < 0)), pos.x + pos.y};
}
else {
return {(pos.x / 2) - ((pos.y + (pos.y > 0)) / 2) + ((pos.x % 2) * (pos.x < 0)), pos.x + pos.y};
}
}
``````
Discuss this blog post on our forum or reddit.