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. **
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 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 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 4 - The GridMap
We now create the main piece of our puzzle, the GridMap.
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.
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.
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)
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 logic 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.
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.
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)
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.
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