Isometric Coordinates

Traditional top-down or side-view games normally work in a traditional grid. Graphics are placed in these grid locations. Each graphic is a rectangle, and sometimes they are referred to as “tiles.”

Converting between grid locations and the screen’s pixel coordinates are reasonably straight-forward.

Another type of 2D game uses “Isometric Tiles.” Here, we can fake a 3D view with 2D graphics. We do that by tilting the grid 45 degrees. Each tile then becomes a diamond.

Unfortunately the math to go from pixels to grid locations is no longer straight forward.

Equations For Tiles To Pixels

Equations to go from tile coordinates to screen coordinates.

Variable definitions

Given:

  • tilewidth = width of each tile in pixels
  • tileheight = height of each tile in pixels
  • tilex = x-coordinate of the tile, in tiles
  • tiley = y-coordinate of the tile, in tiles
  • width = width of the map, in tiles
  • height = hieght of the map, in tiles

Result:

  • screenx = x-coordinate of the screen in pixels
  • screeny = y-coordinate of the screen in pixels

Equations

\[screenx = \frac{tilewidth \cdot tilex}{2} + \frac{height \cdot tilewidth}{2} - \frac{tiley \cdot tilewidth}{2}\]
\[screeny = \frac{(height - tiley - 1) \cdot tileheight}{2} + \frac{width \cdot tileheight}{2} - \frac{tilex \cdot tileheight}{2}\]

Equations For Pixels To Tiles

Equations to go from screen pixel coordinates to tile coordinates.

This needs to work for any coordinate inside the diamond, not just the center.

Variable definitions

Given:

  • screenx = x-coordinate of the screen in pixels
  • screeny = y-coordinate of the screen in pixels
  • tilewidth = width of each tile in pixels
  • tileheight = height of each tile in pixels
  • width = width of the map, in tiles
  • height = hieght of the map, in tiles

Result:

  • tilex = x-coordinate of the tile, in tiles
  • tiley = y-coordinate of the tile, in tiles

Equations

<Insert magic math stuff here.>

Examples

2x2 Grid

_images/2x2.png
Parameters
MAP_WIDTH = 2
MAP_HEIGHT = 2
TILE_WIDTH = 128
TILE_HEIGHT = 128
Tile coordinates to screen coordinates (center of tile)
0, 0 => 128, 192
0, 1 =>  64, 128
1, 0 => 192, 128
1, 1 => 128,  64

3x3 Grid

_images/3x3.png
Parameters
MAP_WIDTH = 3
MAP_HEIGHT = 3
TILE_WIDTH = 128
TILE_HEIGHT = 128
Tile coordinates to screen coordinates (center of tile)
0, 0 => 192, 320
0, 1 => 128, 256
0, 2 =>  64, 192
1, 0 => 256, 256
1, 1 => 192, 192
1, 2 => 128, 128
2, 0 => 320, 192
2, 1 => 256, 128
2, 2 => 192,  64

4x4 Grid

_images/4x4.png
Parameters
MAP_WIDTH = 4
MAP_HEIGHT = 4
TILE_WIDTH = 128
TILE_HEIGHT = 128
Tile coordinates to screen coordinates (center of tile)
0, 0 => 256, 448
0, 1 => 192, 384
0, 2 => 128, 320
0, 3 =>  64, 256
1, 0 => 320, 384
1, 1 => 256, 320
1, 2 => 192, 256
1, 3 => 128, 192
2, 0 => 384, 320
2, 1 => 320, 256
2, 2 => 256, 192
2, 3 => 192, 128
3, 0 => 448, 256
3, 1 => 384, 192
3, 2 => 320, 128
3, 3 => 256,  64

4x1 Grid

_images/4x1.png
Parameters
MAP_WIDTH = 4
MAP_HEIGHT = 1
TILE_WIDTH = 128
TILE_HEIGHT = 128
Tile coordinates to screen coordinates (center of tile)
0, 0 =>  64, 256
1, 0 => 128, 192
2, 0 => 192, 128
3, 0 => 256,  64

1x4 Grid

_images/1x4.png
Parameters
MAP_WIDTH = 1
MAP_HEIGHT = 4
TILE_WIDTH = 128
TILE_HEIGHT = 128
Tile coordinates to screen coordinates (center of tile)
0, 0 => 256, 256
0, 1 => 192, 192
0, 2 => 128, 128
0, 3 =>  64,  64

3x3 Squished Grid

The height and width don’t have to equal each other. In fact, they often don’t. Here’s an example where they are different.

_images/3x3_squished.png
Parameters
MAP_WIDTH = 3
MAP_HEIGHT = 3
TILE_WIDTH = 128
TILE_HEIGHT = 64
Tile coordinates to screen coordinates (center of tile)
0, 0 => 192, 160
0, 1 => 128, 128
0, 2 =>  64,  96
1, 0 => 256, 128
1, 1 => 192,  96
1, 2 => 128,  64
2, 0 => 320,  96
2, 1 => 256,  64
2, 2 => 192,  32

Code Example

isometric_example.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""
Example code showing Isometric Grid coordinates
"""

import arcade
import os

SCREEN_WIDTH = 700
SCREEN_HEIGHT = 700

MAP_WIDTH = 5
MAP_HEIGHT = 4
TILE_WIDTH = 128
TILE_HEIGHT = 128


def get_screen_coordinates(tile_x, tile_y, width, height, tilewidth, tileheight):
    screen_x = tilewidth * tile_x // 2 + height * tilewidth // 2 - tile_y * tilewidth // 2
    screen_y = (height - tile_y - 1) * tileheight // 2 + width * tileheight // 2 - tile_x * tileheight // 2
    return screen_x, screen_y


class MyGame(arcade.Window):
    """ Main application class. """

    def __init__(self, width, height):
        super().__init__(width, height)

        self.axis_shape_list = None
        self.isometric_grid_shape_list = None

    def setup(self):
        """ Set up the game and initialize the variables. """

        # Set the background color
        arcade.set_background_color((50, 50, 50))

        self.axis_shape_list = arcade.ShapeElementList()

        # Axis
        start_x = 0
        start_y = 0
        end_x = 0
        end_y = SCREEN_HEIGHT
        line = arcade.create_line(start_x, start_y, end_x, end_y, arcade.color.WHITE, 2)
        self.axis_shape_list.append(line)

        # Axis
        start_x = 0
        start_y = 0
        end_x = SCREEN_WIDTH
        end_y = 0
        line = arcade.create_line(start_x, start_y, end_x, end_y, arcade.color.WHITE, 2)
        self.axis_shape_list.append(line)

        # x Tic Marks
        for x in range(0, SCREEN_WIDTH, 64):
            start_y = -10
            end_y = 0
            line = arcade.create_line(x, start_y, x, end_y, arcade.color.WHITE, 2)
            self.axis_shape_list.append(line)

        # y Tic Marks
        for y in range(0, SCREEN_HEIGHT, 64):
            start_x = -10
            end_x = 0

            line = arcade.create_line(start_x, y, end_x, y, arcade.color.WHITE, 2)
            self.axis_shape_list.append(line)

        tilewidth = TILE_WIDTH
        tileheight = TILE_HEIGHT
        width = MAP_WIDTH
        height = MAP_HEIGHT

        # Gridlines 1
        for tile_row in range(-1, height):
            tile_x = 0
            start_x, start_y = get_screen_coordinates(tile_x, tile_row, width, height, tilewidth, tileheight)
            tile_x = width - 1
            end_x, end_y = get_screen_coordinates(tile_x, tile_row, width, height, tilewidth, tileheight)

            start_x -= tilewidth // 2
            end_y -= tileheight // 2

            line = arcade.create_line(start_x, start_y, end_x, end_y, arcade.color.WHITE)
            self.axis_shape_list.append(line)

        # Gridlines 2
        for tile_column in range(-1, width):
            tile_y = 0
            start_x, start_y = get_screen_coordinates(tile_column, tile_y, width, height, tilewidth, tileheight)
            tile_y = height - 1
            end_x, end_y = get_screen_coordinates(tile_column, tile_y, width, height, tilewidth, tileheight)

            start_x += tilewidth // 2
            end_y -= tileheight // 2

            line = arcade.create_line(start_x, start_y, end_x, end_y, arcade.color.WHITE)
            self.axis_shape_list.append(line)

        for tile_x in range(width):
            for tile_y in range(height):
                screen_x, screen_y = get_screen_coordinates(tile_x, tile_y, width, height, tilewidth, tileheight)
                point_width = 3
                point_height = 3
                point = arcade.create_rectangle_filled(screen_x, screen_y, point_width, point_height, arcade.color.LIGHT_CORNFLOWER_BLUE, 3)
                self.axis_shape_list.append(point)
                print(f"{tile_x}, {tile_y} => {screen_x:3}, {screen_y:3}")


    def on_draw(self):
        """
        Render the screen.
        """

        # This command has to happen before we start drawing
        arcade.start_render()


        self.axis_shape_list.draw()

        # x Labels
        for x in range(0, SCREEN_WIDTH, 64):
            text_y = -25
            arcade.draw_text(f"{x}", x, text_y, arcade.color.WHITE, 12, width=200, align="center",
                         anchor_x="center")

        # y Labels
        for y in range(0, SCREEN_HEIGHT, 64):
            text_x = -50
            arcade.draw_text(f"{y}", text_x, y - 4, arcade.color.WHITE, 12, width=70, align="right",
                         anchor_x="center")


        tilewidth = TILE_WIDTH
        tileheight = TILE_HEIGHT
        width = MAP_WIDTH
        height = MAP_HEIGHT

        for tile_x in range(width):
            for tile_y in range(height):
                screen_x, screen_y = get_screen_coordinates(tile_x, tile_y,
                                                            width, height,
                                                            tilewidth, tileheight)
                arcade.draw_text(f"{tile_x}, {tile_y}",
                                 screen_x, screen_y + 6,
                                 arcade.color.WHITE, 12,
                                 width=200, align="center", anchor_x="center")

    def update(self, delta_time):
        view_left = -50
        view_bottom = -50
        arcade.set_viewport(view_left,
                            SCREEN_WIDTH + view_left,
                            view_bottom,
                            SCREEN_HEIGHT + view_bottom)

    def on_mouse_press(self, x: float, y: float):
        screen_x = x + self.view_left
        screen_y = y + self.view_bottom

        grid_x = screen_x // TILE_WIDTH
        grid_y = screen_y // TILE_HEIGHT
        point_x = (screen_x % TILE_WIDTH) - (TILE_WIDTH / 2)
        point_y = (screen_y % TILE_HEIGHT) - (TILE_HEIGHT / 2)


        # print(f"({screen_x}, {screen_y}) -> ({map_x:.2}, {map_y:.2})")


def main():
    """ Main method """
    window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()