04.blue-noise-mask

Generating rocks only over grass areas

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 TileMap 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 TileMap node to draw where we want to generate rocks in a premade game level. You’ll learn how to use the TileMap 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.

Creating a TileMap node

Create a new TileMap 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 TileMap.

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 TileMap 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 TileMap 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.

Generating rocks over the grass

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()

Looping over the tilemap’s cell

So far, we have used two nested for loops to generate grid positions.

We don’t have to do this when using a TileMap 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 TileMap, we call its 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 TileMap 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():
        # ...
        rock.position = mask.position + mask.map_to_world(cell) + random_offset

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.
    mask.visible = false

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.

Would I have to paint masks manually like this in my 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 TileMap 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.

The code

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.
    mask.visible = false


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
        rock.position = mask.position + mask.map_to_world(cell) + random_offset