2D Godot Tactical RPG: Hexagonal and Isometric Battle Maps

By: Ryan Cooper | September 22, 2019

One of the most common questions I am asked when working with people who want to make a game is “Where do I begin?” I find that the best answer is actually very simple, “whatever gets you from idea to development the fastest.” Ask yourself “Is there a feature I am the most excited about? Is there a there a mechanic that is the focal point of my game? Or a mechanic that I would spend the most time interacting with?”

For me, I have already decided that I am making a tactical RPG, and the engine that I want to use is Godot, which leaves me with the choice of mechanic that the user is most engaged with, which would be Combat.

So with that mechanic, the best place to start is to get something drawn to the screen. There are two things that can start being drawn to the screen, the map and the units, and since the units depend on the map to move or interact, we will be starting with the map. Inside of Godot we will be drawing the map with TileMap Node

From a blank project, we will setup our node tree to look like this

*Note* The BattleManager Node in the screenshot will be taken care of in a tutorial in the future

Setting up the BattleMap

Here are some assets to use for a tileset that I have created so that we can get our project started. You will want to add these files to the project folder.

Once these assets have been imported we will need to set some values. I have already done the legwork finding the cell size and texture offset to use for these assets. They can be found here:

Sprite Sheet ImageCell SizeSprite SizeOffset
Topdown_Square_Autotile.png 32×3232×32(0, 0)
Topdown_Hex_Pointytop.png 56×4664×56(0,-8)
Topdown_Hex_Flattop.png 46×5656×64(-8,0)
Isometric_Square_Autotile.png 64×3264×32(0, 0)
Isometric_Hex_Pointytop.png 98×4686×42(4, 4)
Isometric_Hex_Flattop.png 98×4686×42(4, 4)


Select the type of perspective (shown as Mode to the right) to use. For a Top Down perspective select “Square” Mode, the other Mode is the Isometric perspective. The cell size is the next value we will set. If you are using the placeholder art, provided above, you can enter the value for Cell Size. If you are using your own art: square grids can use just the size of their sprite, hexagonal grids will require you to fit the cell size properly. The last setting here we will want to check is Half Offset; If you are using a hexagonal grid we will want to change Half Offset; to be either “Offset X” (Pointy Top), or “Offset Y” (Flat Top). If you are using a square grid, place this setting as “Disabled”.

After Setting up the mode, cell size, and half offset we will be making the Tileset. Doing this requires a Resource but you can make a TileSet Resource by simply clicking on empty under mode and clicking new TileSet. Click that Tileset and Godot’s screen layout should look similar to the image below.




Creating a TileSet Resource

To load a sprite into Tile Set Editor, simply click the + sign in the bottom left corner (as shown in the red box below) and choose the sprite sheet based on your mode and tile settings. With a sprite sheet loaded and selected it’s time to start making tiles for the TileSet. First, click “+ New Single Tile” (as shown in the orange box) and you will be able to select a region of the sprite, you will be able to later configure snap settings (as shown in the blue box below) and use the snap tool(as shown in the green box). Once a tile is created, you will be able to access “Selected Tile”, the Tex Offset (highlighted in yellow) shows where to add the last remaining information from the table above.

Setting up Autotile

If you are using Square grids, you will be able to setup autotile. Auto tile in Godot uses a bitmask to determine what tile to use, for example, the 2×2 Bitmask I am showing below (in isometric) splits the tile into quarters. While drawing the tile on the screen Godot looks in the 4 corners of the tile for the matching tile index, if it is found Godot turns the bit on. After all the bits are found, Godot goes through the list of tiles to match the bitmask with its appropriate tile, and draws the tile to the screen. This is easier to see inside of a top down view, so below I have made an example of what it looks like with isometric tiles.

Hexagonal Grids

Hexagonal tiles have different coordinate and directional systems, since it has 6 different directions rather than 4. RedBlobGames has one of the more comprehensive collections of information on hexagonal grids which can be found here. If you do visit RedBlobGames keep in mind that Godot stores hexes in the Odd-R(Pointy Top) or Odd-Q(Flat Top) coordinate systems.

Using the RedBlobGames collection of knowledge, I made a few helper functions to help us with directional movement, and distance calculations, in the future. If you are not yet sure on what grid you will be using, these functions also work with square grids.

To add more functionality to a node we need to add a script. To add a script we simply right click a node (the Tilemap node named BattleMap in this instance), and click “add script”. The Script should use the language GDScript, it should Inherit TileMap, you can use any of the templates, Built in should be off, and the path is wherever you choose. Then copy the below code and paste it into the script you created.

# BattleMap.gd
extends TileMap
class_name BattleMap

const DIRECTIONS_ODD_R = [
    [Vector2(1,  0), Vector2(0, -1), Vector2(-1, -1), 
     Vector2(-1,  0), Vector2(-1, 1), Vector2(0, 1)],
    [Vector2(1,  0), Vector2(1, -1), Vector2(0, -1), 
     Vector2(-1,  0), Vector2(0, 1), Vector2(1, 1)]]
const DIRECTIONS_ODD_Q = [
    [Vector2(1,  0), Vector2(1, -1), Vector2(0, -1), 
     Vector2(1,  -1), Vector2(-1, 0), Vector2(0, 1)],
    [Vector2(1,  1), Vector2(1, 0), Vector2(0, -1), 
     Vector2(-1,  0), Vector2(-1, 1), Vector2(0, 1)]]
const DIRECTIONS_SQUARE = [
	Vector2(1,0), Vector2(1,-1), Vector2(0,-1), Vector2(-1,-1),
	Vector2(-1,0), Vector2(-1,1), Vector2(0,1), Vector2(1,1)]


func directions(point : Vector2) -> Array:
	""" note that i am adding an empty array to make sure 
		nothing can unintentionally change the array """
	if cell_half_offset == HALF_OFFSET_X:
		return [] + DIRECTIONS_ODD_R[int(point.y) & 1]
	elif cell_half_offset == HALF_OFFSET_Y:
		return [] + DIRECTIONS_ODD_Q[int(point.x) & 1]
	else:
		return [] + DIRECTIONS_SQUARE


func euclidean(a : Vector2, b : Vector2) -> float:	
	if cell_half_offset == HALF_OFFSET_X:
		a += Vector2(int(a.y) & 1, 0) * 0.5
		b += Vector2(int(b.y) & 1, 0) * 0.5
	elif cell_half_offset == HALF_OFFSET_Y:
		a += Vector2(0, int(a.x) & 1) * 0.5
		b += Vector2(0, int(b.x) & 1) * 0.5
	
	return (a-b).length()


func manhattan(a : Vector2, b : Vector2) -> float:
	if cell_half_offset == HALF_OFFSET_DISABLED:
		return abs(a.x - b.x) + abs(a.y - b.y)
	elif cell_half_offset == HALF_OFFSET_Y:
		return max(
			abs(a.y - b.y + floor(b.x/2) - floor(a.x/2)),
			max(abs(b.y - a.y + floor(a.x/2) - floor(b.x/2) + b.x - a.x),
			abs(a.x - b.x)))
	else:
		return max(
			abs(a.x - b.x + floor(b.y/2) - floor(a.y/2)),
			max(abs(b.x - a.x + floor(a.y/2) - floor(b.y/2) + b.y - a.y),
			abs(a.y - b.y)))

Place some tiles down and see what it looks like and adjust values if needed. When you are done, run the game, you may have something like this