[004] Godot3D - Procedurally generated Hex Maps

Hello there 🐐 I am currently working on a small project and needed to experiment with different map sizes and shapes. I took this opportunity to learn more about how maps can be procedurally generated in 3D.

In this blog post I will show you how to procedurally create maps in Godot using Grid Maps.

result

I also felt like I struggled a bit with creating and optimizing the code and I took this as a perfect opportunity to give back to the community :)

Get the latest version of the code here

Prerequisites

For this tutorial we will be using a 3D model of a hexagon that I've borrowed from Jmbiv's tutorial.

Alternatively, you can also scour the web for other 3D models and resize them to 1mm height and ~1.74mm width.

As long as you don't want to sell your game and don't want to build the 3D models yourself, I suggest also looking over MagHex Tiles for Gloomhaven which you can download and resize for your game.

Setting up your scene

In this part you can follow my steps from the gifs in order to setup you scene with everything you need and then we can go over the nitty-gritty of the code.

If for some God forbidden reason the website doesn't load the gifs for you, ping me with your browser type in the comments and follow the gifs posted on the repo.

Note - You will see in the gifs that the first key I press is F7, which I use for starting the recordings. You will not need to press F7 when tracing my steps.

Step 1 - The Main Node

We will need to create a main node to which we will attach the other components and which will be displayed when running the game.

step_1

Step 2 - The Main Camera

You see that nice blue horizon line in the background?

If we run the game as it is, we won't be able to see it without a camera.

step_2

Step 3 - The Mesh Library

In this approach of procedurally generating maps, we will be using GridMaps.

In order to take full advantage of them, we will transform our 3D object into a Mesh Library, such that we can later import it inside the GridMap.

step_3

Step 4 - The GridMap

We now create the main piece of our puzzle, the GridMap.

step_4

You will see me typing in some numbers for the cell_size. I will go more into details about those numbers when explaining the code. All you have to know now is this specific 3D object (now Hex Mesh) that we are using, we have to put in those values for it to perfectly fit between its brothers and sisters.

Step 5 - Adding The Script

Now you just copy-paste the script, and that's it... project done. Now you can add Godot game developer in your CV.

step_5

Or... if you really want you can also follow the next chapters in order to understand how the code works.

Understanding the code

The first draft For the first iteration of the code, all we want to be able to do is to create a rectangle given the number of rows and columns.

So let's define the variables that we need...

extends GridMap

# for now we hardcode the height of the 3D mesh
const TILE_HEIGHT := 1

# one mesh, one color
var num_colors := 1
# we expose the number of rows and columns
export var hex_rows := 7
export var hex_cols := 5

We will now use Godot's _ready() function to update the values of cell_size dynamically.

func _ready():
    # in 3D the z component corresponds to the height of the mesh
    cell_size.z = TILE_HEIGHT / 2.0

    # we use the cosine function to find half of the hexagon width and stepify to round the result
    cell_size.x = stepify(TILE_HEIGHT * cos(deg2rad(30)), 0.01)
    _generate_square_grid()

I'll try not to deviate too much and go into the math and why we use the cosine function here. But if you're curios (which is something admirable) you can look at this explanatory YouTube video.

Now let's actually generate the square map...

func _generate_square_grid():
    for y in range(hex_rows):
        for x in range(hex_cols):
            # we shift the cell twice horizontally and once more if the row is even
            x = x * 2 + y % 2

            # we set the cell of the grid at the position (x, 0, y)
            # with value 0 (our first mesh) 
            set_cell_item(x, 0, y, 0)

In the end, you should have a result similar to the one in the image below.

itteration_1

Centering map - Experimentation

So a first approach to centering may be to move all cells to the right by half the width of the rectangle.

So let's implement it and see if it works...

func _generate_square_grid():
    # the shift should be equal to half of the length of the rectangle
    # we don't divide by 2 because for every cell we need to multiply cell_size.x by 2
    var x_shift = hex_cols * cell_size.x

    for y in range(hex_rows):
        for x in range(hex_cols):
            # we now shift each element to the left
            x = x * 2 + y % 2 - x_shift
            set_cell_item(x, 0, y, 0)

itteration_2

Well that doesn't look quite right. Let's try printing out the x position of each cells in a row to see what's wrong.

# This code is for a rectangle with 5 columns
X_shift is: 4.35

X is: 0 X-X_shift is: -4.35
X is: 1 X-X_shift is: -2.35
X is: 2 X-X_shift is: -0.35
X is: 3 X-X_shift is: 1.65
X is: 4 X-X_shift is: 3.65

It seems like the pattern may be something like even numbers from -4 to 4 with a 0.35 shift. But something breaks when we subtract the shift from 3.

⚔️ What do you say about a small competition?

Try thinking of a way to solve the problem and after you come up with a solution, you can compare your solution with mine.

I'll add some empty space for you not to get any spoilers.

placeholder1

Centering map - My solution So ideally what we want is to replace our range from [0 to hex_cols] to something like [-hex_cols/2 to hex_cols/2].

Unfortunately at the time of me writing this blogpost Godot does not support negative ranges, so the only solution is to create our own range.

func _get_zero_centered_range(n: int):
    var array = []
    var shift = (n-1) / 2.0

    # if n is equal to 0 we just have a centered element
    if not shift:
        return [shift]
    # else we create the normalized range
        for x in range(n):
            array.append((x - shift)/shift)
    return array

We then just have to integrate the zero centered range in our previous code.

func _generate_square_grid():
    var x_shift = hex_cols * cell_size.x
    for y in range(hex_rows):
        # we create a variable to store our custom range
        var zc_range = _get_zero_centered_range(hex_cols)
        for x in zc_range:
            # we shift the elements with half the rectangle width
            x = x * x_shift + y % 2
            set_cell_item(x, 0, y, 0)
Now what the map will look like would be something like this.

itteration_3

Yup... that looks a bit better but it seems like something is still out of place.

Removing extra cells

It seems that on even rows the rectangle is a little bit longer. Nothing a few if statements can't solve.

func _generate_square_grid():
    var x_shift = hex_cols * cell_size.x
    for y in range(hex_rows):
        var zc_range = _get_zero_centered_range(hex_cols)
        # if the row is even, we remove the last cell from the range
        if y % 2:
            zc_range.pop_back()
        for x in zc_range:
            x = x * x_shift + y % 2
            set_cell_item(x, 0, y, y % num_colors)

itteration_4

That looks mutch better, but how did we get those new colors?

Must have been a bug, who knows...

Future investigations

Generating hexagonal maps should also be something to look into.

result2

The starter code should be enough for you to wrap your head around how to generate the hex map, but if you struggle with it you can ping me in the comments and I'll create a tutorial for it.

Get the latest version of the code here

Conclusions

In today's post we looked over:

how to create a scene and mesh library how to use grid map together with the mesh library how to procedurally populate the map with hexagon meshes If there are any questions leave them in the comments and I'll try to answer them as soon as I can.

Hope you enjoyed the blogpost :)

Peace 🐐

References

Jmbiv - 3D Hexcell Square Map - YouTube link

Gingerageous Games - 2D Squarecell Hex Map - YouTube link

Brian McLogan - Find missing length using cosine function - YouTube link

Comments

Popular posts from this blog

[003] OpenCV: How to obtain disparity maps

[001] OpenCV: How to capture video from a PS4 camera