02.regular-grid

Placing objects on a regular grid

In this lesson, we’ll generate randomized rocks on a grid using two nested for loops.

By the end, you’ll have a grid with one rock appearing in each cell.

In our code, we’ll write two functions: one to pick random rocks to instantiate, and another to place those rocks on the grid.

Creating the scene and script

In the FileSystem dock, right-click on the RandomRocks/ folder and click New Scene… to create an empty scene. You can name it RandomRocks.

In the Scene dock, click the 2D Scene button to create a Node2D and rename the node to RandomRocks.

Right-click the RandomRocks node and select Attach Script to create a new script for it.

Getting a random rock sprite

In the script editor, we start with a function to get a random rock.

First, we need a list of rocks to instantiate. We prepared three scenes for you, each with a different rock. You can find them in the RandomRocks/rocks folder.

Each scene contains only a Sprite node at the moment, but we’ll add collisions to them in the next chapter.

For now, we define an array where we preload the three rock variations.

# We use the `ROCKS` array to pick a random scene to vary the visuals that we
# place on the grid.
const ROCKS := [
    preload("rocks/Rock1.tscn"),
    preload("rocks/Rock2.tscn"),
    preload("rocks/Rock3.tscn"),
]

We then define a function to create a random rock from our ROCKS array.

# Creates and returns a new random rock instance.
func get_random_rock() -> Sprite:
    # We first calculate a random index in the ROCKS array.
    var rock_random_index := randi() % ROCKS.size()
    # Then, we get the preloaded scene in the array and create a new instance of
    # it.
    return ROCKS[rock_random_index].instance()

To pick a random preloaded rock scene, we need a random index. The randi() function generates a random integer, but it is often a huge number.

To calculate a valid index in our ROCKS array, we use the % modulo operator and the array’s size: randi() % ROCKS.size().

Placing rocks on a grid

Now we can generate random rocks. We need to place them in each cell of a grid.

We want to be able to change the grid size, so we write a function that lets us choose how many columns and rows we want.

We generate cell coordinates using two nested for loops. One loop generates the cell’s column, and the other the cell’s row.

# Creates and places a random rock on each cell of the grid, based on the number
# of columns and rows.
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)

That’s how you loop over 2D coordinates in GDScript. To create 3D coordinates, you would nest three for loops instead.

We then use the get_random_rock() function to create a new rock instance and add it as a child.

# Creates and places a random rock on each cell of the grid, based on the number
# of columns and rows.
func add_rocks_on_grid(columns: int, rows: int) -> void:
    for column in range(columns):
        for row in range(rows):
            # ...
            var rock := get_random_rock()
            add_child(rock)

Finally, we need to place the rock. The cell variable represents grid coordinates. To covert these to screen coordinates in pixels, we multiply them by the CELL_SIZE.

# This constant is the size of each grid cell in pixels.
const CELL_SIZE := Vector2(128, 128)

# Creates and places a random rock on each cell of the grid, based on the number
# of columns and rows.
func add_rocks_on_grid(columns: int, rows: int) -> void:
    for column in range(columns):
        for row in range(rows):
            # ...
            rock.position = CELL_SIZE * cell

Your complete add_rocks_on_grid() function should look like this.

# Creates and places a random rock on each cell of the grid, based on the number
# of columns and rows.
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)
            rock.position = CELL_SIZE * cell

We call our new function inside of _ready() to generate rocks.

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

You can now run the scene to see a grid filled with random rocks.

Randomizing the random numbers

If you run the scene multiple times, you’ll always get the same rocks in the same cells.

That’s because functions to generate random numbers like randi() use clever calculations to output numbers that aren’t exactly random: they are pseudo-random.

Pseudo-random numbers are generated in a deterministic way.

They are a sequence of seemingly random numbers produced from an input number called the seed value. If you give the computer the same seed number, it will produce the same number sequence every time.

This deterministic behavior is helpful, as you can debug your code and compare the output of your procedural generation code as you change it.

It also allows players to share worlds in procedural games like Minecraft.

In Minecraft, when starting a new game, you can enter a seed value. Every player that uses the same given seed will land in the same environment.

In GDScript, we can randomize the seed value by calling the randomize() function once in the game. This function uses the computer’s clock to produce a unique number.

We can call the randomize() function in the _ready() function, before calling add_rocks_on_grid().

func _ready() -> void:
    # Start by refreshing the seed value used to generate random numbers so we
    # get different random values for each run.
    randomize()
    add_rocks_on_grid(9, 5)

If you play the scene multiple times, you should now see the rock sprites change each time.

In the next lesson, we’ll offset each rock randomly with a blue noise distribution.

Practice: Selecting random items

Open the practice Selecting random items.

In this practice, you’re starting with an inventory filled with swords.

You need to edit the get_random_item() function to randomize the generated items.

Practice: Placing gems on a grid

Open the practice Placing gems on a grid.

In this project, we want to generate a grid of randomly-placed gems.

The code

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

extends Node2D

# This constant is the size of each grid cell in pixels.
const CELL_SIZE := Vector2(128, 128)

# We use the `ROCKS` array to pick a random scene to vary the visuals that we
# place on the grid.
const ROCKS := [
    preload("rocks/Rock1.tscn"),
    preload("rocks/Rock2.tscn"),
    preload("rocks/Rock3.tscn"),
]

func _ready() -> void:
    # Start by refreshing the seed value used to generate random numbers so we
    # get different random values for each run.
    randomize()
    add_rocks_on_grid(9, 5)


# Creates and places a random rock on each cell of the grid, based on the number
# of columns and rows.
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)
            rock.position = CELL_SIZE * cell


# Creates and returns a new random rock instance.
func get_random_rock() -> Sprite:
    # We first calculate a random index in the ROCKS array.
    var rock_random_index := randi() % ROCKS.size()
    # Then, we get the preloaded scene in the array and create a new instance of
    # it.
    return ROCKS[rock_random_index].instance()