In this last part of this series, you’ll learn how to generate rocks only on desired parts of a level. We’ll achieve this by using a
node in a scene that we prepared for you.When we assembled a game from premade parts in the course’s introduction, you got a quick look at tilemaps. A tilemap is a grid-based drawing tool for game levels.
With it, you can draw tiling textures next to one another to design levels efficiently. We call the set of tiling textures we use to draw levels a tileset. It looks like this.
Because tilemaps rely on a grid, we can use them in code to help place random objects.
In this lesson, we’ll use a new
node to draw where we want to generate rocks in a premade game level. You’ll learn how to use the node to draw complete levels in future lessons. But for now, we’ll keep it simple and focus on the code.Please open the scene Island.tscn
in the
RandomRocks/
directory to get started.
Create a new
node as a child of Island and name it Mask.We’ll use this tilemap to select areas on which we want to generate rocks.
Right-click on the slot next to Tile Set in the
Inspector and select Quick Load. Load the resource
RandomRocks/mask_tileset.tres
we prepared for you.
Also, please set the Cell -> Size to
Vector2(128, 128)
, as shown below. This is the size of each
cell in the tileset in pixels.
A new column should appear to the left of the Inspector with a single square named Blue Noise Cell. This is the tiles palette. It allows you to select a tile to draw on your
.With the Blue Noise Cell selected, left-click and drag on the viewport to draw tiles. Try drawing over the grass patches. You’ll notice the shade leaks out of the grass.
We want the shaded area to perfectly cover the grass as it represents where our rocks will appear.
We need to offset our
node to cover the grass perfectly.In the Inspector, set the node’s Transform ->
Position to (64, 64)
: half of the cell’s size.
This will offset the whole
and make the Blue Noise Cell tile perfectly fit the grassy patches.If your drawn cells are completely outside of the grass now, you can right-click and drag to erase cells.
Please cover the grass areas with the mask tile as shown below.
With the tiles drawn, we can now add a script to place rocks procedurally.
To make our script work with the Mask node, we need to
change the add_rocks_on_grid()
function we wrote in the
previous lessons.
Let’s duplicate our RandomRocks.gd
script to preserve
the original scene.
In the FileSystem dock, right-click the
RandomRocks.gd
file and select Duplicate. Name the
new script Island.gd
.
Then, click and drag the Island.gd
script onto the
Island node in the Scene dock.
Click the Script icon to head to the script editor. Delete
the add_rocks_on_grid()
function and the call to it in the
_ready()
function.
You can also remove the CELL_SIZE
constant as we’ll get
the cell size from the Mask node.
Your starting code should look like this.
extends Node2D
const ROCKS := [
preload("rocks/Rock1.tscn"),
preload("rocks/Rock2.tscn"),
preload("rocks/Rock3.tscn"),
]
func _ready() -> void:
randomize()
func get_random_rock() -> Sprite:
var rock_random_index := randi() % ROCKS.size()
return ROCKS[rock_random_index].instance()
So far, we have used two nested for
loops to generate
grid positions.
We don’t have to do this when using a
node. The node handles the grid for us and gives us all used cell coordinates in its internal grid.To get all used cell coordinates in a get_used_cells()
function.
# This is the `TileMap` we use to pick offsets for object placement. We only use
# it to define regions for object placement. We remove it at run-time since it
# isn't part of the game world.
onready var mask: TileMap = $Mask
func add_rocks_on_grid() -> void:
# TileMap.get_used_cells() returns an array of Vector2 cell coordinates
# where we drew a tile.
for cell in mask.get_used_cells():
var rock := get_random_rock()
add_child(rock)
We then calculate a random offset for each rock like before. The only
difference is that we use the Mask node’s
cell_size
variable.
func add_rocks_on_grid() -> void:
# TileMap.get_used_cells() returns an array of Vector2 cell coordinates
# where we drew a tile.
for cell in mask.get_used_cells():
# ...
var rock_size := rock.scale * rock.texture.get_size()
# Because the Mask node has a cell_size property, we use it instead of
# the previously hard-coded CELL_SIZE constant.
var available_space := mask.cell_size - rock_size
var random_offset := Vector2(randf(), randf()) * available_space
We can now update the rock’s position. We first account for the Mask node’s position, as we moved it in the editor.
Then, we use the TileMap.map_to_world()
function to
convert the cell’s coordinates from the node’s grid to a position on the map in
pixels.
Finally, we add our random_offset
.
func add_rocks_on_grid() -> void:
# TileMap.get_used_cells() returns an array of Vector2 cell coordinates
# where we drew a tile.
for cell in mask.get_used_cells():
# ...
= mask.position + mask.map_to_world(cell) + random_offset rock.position
Then, we call our add_rocks_on_grid()
function inside
_ready()
and hide the Mask node.
func _ready() -> void:
#...
add_rocks_on_grid()
# We hide the mask tiles, otherwise, we will get an unwanted shade on the
# game level.
= false mask.visible
If you run the scene, you should see non-overlapping rock sprites placed only on the grassy patches.
With that, you now know the basics of procedural content generation. You can use the same techniques to generate all sorts of randomized objects in your games.
You will often need to paint masks or define areas like collisions by hand in games, as we did in this lesson.
Technically, you can always calculate masks or collision boxes using code. However, it’s often more complicated than drawing things by hand, especially at this learning stage.
In this lesson, we manually painted areas with grass on a dedicated
node to keep the code simple.And this concludes the series.
We’ll bring our top-down character and rocks generation together in an obstacle course in the next chapter. We’ll code new game mechanics and prototype a little game.
Here’s the complete code listing for Island.gd
.
extends Node2D
const ROCKS := [
preload("rocks/Rock1.tscn"),
preload("rocks/Rock2.tscn"),
preload("rocks/Rock3.tscn"),
]
# This is the `TileMap` we use to pick offsets for object placement. We only use
# it to define regions for object placement. We remove it at run-time since it
# isn't part of the game world.
onready var mask: TileMap = $Mask
func _ready() -> void:
randomize()
add_rocks_on_grid()
# We hide the mask tiles, otherwise, we will get an unwanted shade on the
# game level.
= false
mask.visible
func get_random_rock() -> Sprite:
var rock_random_index := randi() % ROCKS.size()
return ROCKS[rock_random_index].instance()
# Generates rocks on drawn cells in the Mask tilemap and randomly offsets them
# using blue noise.
func add_rocks_on_grid() -> void:
# TileMap.get_used_cells() returns an array of Vector2 cell coordinates
# where we drew a tile.
for cell in mask.get_used_cells():
var rock := get_random_rock()
add_child(rock)
var rock_size := rock.scale * rock.texture.get_size()
# Because the Mask node has a cell_size property, we use it instead of
# the previously hard-coded CELL_SIZE constant.
var available_space := mask.cell_size - rock_size
var random_offset := Vector2(randf(), randf()) * available_space
= mask.position + mask.map_to_world(cell) + random_offset rock.position