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.
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.
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.
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
= CELL_SIZE * cell + random_offset rock.position
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
node.Open the practice Generating random positions.
Your goal is to get gems to scatter like below.
Open the practice Applying blue noise.
Your goal in this practice is to place rocks randomly using the technique you learned in this lesson.
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
= CELL_SIZE * cell + random_offset
rock.position
func get_random_rock() -> Sprite:
var rock_random_index := randi() % ROCKS.size()
return ROCKS[rock_random_index].instance()