03.random-positions-with-blue-noise

Random positions with blue noise

In this part, we’ll randomize the position of each rock in its grid cell.

This technique allows you to place objects randomly without overlapping and with an even, natural-looking distribution.

We call this kind of random distribution blue noise. We use the expression loosely in games: when talking about blue noise, we mean that we distribute objects randomly yet reasonably evenly.

There are many algorithms to produce blue noise. In this lesson, you’ll learn the simplest: shifting objects in grid cells.

To break down the rocks’ alignment, we want to offset their positions so that each sprite stays within its grid cell. This keeps objects from overlapping.

Offsetting each rock with blue noise

Each of our rocks has a different sprite and a slightly different size.

To calculate a random offset for each rock, we need to take its size into account so it doesn’t get out of its grid cell.

We extend our add_rocks_on_grid() function to offset each rock in the nested for loop.

Calculating the rock size

To calculate a sprite’s size on screen, we have to consider two factors:

We access a sprite’s texture with the texture member variables and call its get_size() function to get its size in pixels.

We multiply this value by the sprite node’s scale to calculate the size in pixels.

Please note how, below, we included the existing for loop lines to help you know where the new code goes. We often do this when adding code to existing blocks.

func add_rocks_on_grid(columns: int, rows: int) -> void:
    for column in range(columns):
        for row in range(rows):
            # ...
            var rock_size := rock.scale * rock.texture.get_size()

We often scale game objects, so it’s a good practice to account for the scale when working with sizes.

Calculating each rock’s random offset

With the rock size, we can calculate the offset of each rock in its grid cell.

We first calculate the maximum offset we can apply to each rock based on its size. We store that value in the available_space variable.

We then generate a random offset and add it to the rock’s position.

func add_rocks_on_grid(columns: int, rows: int) -> void:
    for column in range(columns):
        for row in range(rows):
            # ...
            # This is the maximum offset in pixels we can apply to a given rock
            # based on its size.
            var available_space := CELL_SIZE - rock_size
            # We want the offset to be random both on the X and Y axes so we
            # call randf() twice. The randf() function generates a random number
            # between 0.0 and 1.0.
            var random_offset := Vector2(randf(), randf()) * available_space
            rock.position = CELL_SIZE * cell + random_offset

If you run the scene with the code above, you should see each rock at a different position.

Every time you run the scene, the rocks should change look and position. Many games with randomized levels build upon this technique.

In the next and final part, we’ll learn to place rocks only on the grass in a level, thanks to Godot’s TileMap node.

Practice: Generating random positions

Open the practice Generating random positions.

Your goal is to get gems to scatter like below.

Practice: Applying blue noise

Open the practice Applying blue noise.

Your goal in this practice is to place rocks randomly using the technique you learned in this lesson.

The code so far

Here’s the complete code listing for RandomRocks.gd.

extends Node2D


const ROCKS := [
    preload("rocks/Rock1.tscn"),
    preload("rocks/Rock2.tscn"),
    preload("rocks/Rock3.tscn"),
]

const CELL_SIZE := Vector2(128, 128)


func _ready() -> void:
    randomize()
    add_rocks_on_grid(9, 5)


# Calculates the offset of each grid cell and a small random vector amount to
# instantiate a rock `Sprite` positioned at that location.
func add_rocks_on_grid(columns: int, rows: int) -> void:
    for column in range(columns):
        for row in range(rows):
            var cell := Vector2(column, row)
            var rock := get_random_rock()
            add_child(rock)

            var rock_size := rock.scale * rock.texture.get_size()
            # This is the maximum offset in pixels we can apply to a given rock
            # based on its size.
            var available_space := CELL_SIZE - rock_size
            # We want the offset to be random both on the X and Y axes so we
            # call randf() twice. The randf() function generates a random number
            # between 0.0 and 1.0.
            var random_offset := Vector2(randf(), randf()) * available_space
            rock.position = CELL_SIZE * cell + random_offset


func get_random_rock() -> Sprite:
    var rock_random_index := randi() % ROCKS.size()
    return ROCKS[rock_random_index].instance()