White noise world generator

In this lesson, we will create a class derived from WorldGenerator and use white noise to place asteroids around the world.

Setting up the scene

Create a new 2D Scene and rename the root node WhiteNoiseWorldGenerator.

We want to instantiate the Player and GridDrawer scenes, which we prepared for you. The first is a ship the player can control to explore the world. The second draws a grid over the world to help visualize the active sectors.

You can find both scenes in the SpaceInfiniteGeneration/Shared/ directory. Create one instance of each as a child of WhiteNoiseWorldGenerator.

In the course’s screenshots, you’ll notice the game’s background is a deep blue. To change the background color as we did, you can go to Project -> Project Settings… -> Rendering -> Environment and click the color swatch next to Default Clear Color.

Spawning asteroids around the player

With that, we can save the scene, attach a new script to WhiteNoiseWorldGenerator, and get started coding.

We start by setting up the GridDrawer node and generating our world in the _ready(). We also define two exported variables to give us control over the generation of asteroids.

## Infinite world generator that uses white noise to place asteroids in sectors.
class_name WhiteNoiseWorldGenerator
extends WorldGenerator

## The asteroid scene to instantiate inside sectors.
export var Asteroid: PackedScene
## The number of asteroids to place in each sector.
export var asteroid_density := 3

onready var _grid_drawer := $GridDrawer


# Upon starting the game, we generate sectors around the player and initialize
# the grid drawer, which needs to know the sector size and the number of sector
# we want along each axis.
func _ready() -> void:
    generate()
    _grid_drawer.setup(sector_size, sector_axis_count)

For now, we’ll generate the world only once upon starting the game. In the next lesson, we will look at how we can generate new rows and columns of sectors as the player moves through space.

To generate asteroids inside sectors, we must override _generate_sector(). There, we:

  1. Generate a unique seed for the sector and use it to reinitialize our RandomNumberGenerator.
  2. Instantiate asteroid_density asteroids and randomize their transform.
  3. Save a reference to each of them in the _sectors dictionary to later access and free them.

Here’s the code. Note that I used a _generate_random_position() function to help keep the corresponding calculations in one place and make the code more comfortable for you to read.

# We need to override the `_generate_sector()` method for this class to do anything.
# This method spawns asteroids and places them inside the sector's bounds
# with a random position, rotation, and scale.
func _generate_sector(x_id: int, y_id: int) -> void:
    # We calculate and set a unique seed for the current sector. This resets the
    # series of numbers generated by our `RandomNumberGenerator` back to the
    # start, which ensures the world generates the same every time we use the
    # same seed.
    _rng.seed = make_seed_for(x_id, y_id)

    # List of entities generated in this sector.
    var sector_data := []
    # Generates random Vector2 in a square and assign an asteroid to it, with a
    # random angle and scale. The asteroids can overlap.
    for _i in range(asteroid_density):
        var asteroid := Asteroid.instance()
        add_child(asteroid)

        # We generate a random position for each asteroid within the rectangle's bounds.
        asteroid.position = _generate_random_position(x_id, y_id)
        asteroid.rotation = _rng.randf_range(-PI, PI)
        asteroid.scale *= _rng.randf_range(0.2, 1.0)
        sector_data.append(asteroid)

    # We store references to all asteroids to free them later.
    _sectors[Vector2(x_id, y_id)] = sector_data


# Returns a random position within the sector's bounds, given the sector's coordinates.
func _generate_random_position(x_id: int, y_id: int) -> Vector2:
    # Calculate the sector boundaries based on the current x and y sector
    # coordinates.
    var sector_position = Vector2(x_id * sector_size, y_id * sector_size)
    var sector_top_left = Vector2(
        sector_position.x - _half_sector_size, sector_position.y - _half_sector_size
    )
    var sector_bottom_right = Vector2(
        sector_position.x + _half_sector_size, sector_position.y + _half_sector_size
    )

    # Here, we are not preventing the asteroids from overlapping. They may
    # also be relatively large and so close to the sector's boundaries they
    # overlap with a neighboring sector.
    # We'll address those issues when implementing the blue noise world
    # generator.
    return Vector2(
        _rng.randf_range(sector_top_left.x, sector_bottom_right.x),
        _rng.randf_range(sector_top_left.y, sector_bottom_right.y)
    )

We have one step left before running the scene. We exported an Asteroid variable and we must assign a valid file to it. In the Scene dock, select WhiteNoiseWorldGenerator and in the Inspector, assign the file Asteroid.tscn to its Asteroid property. You can find the file in the SpaceInfiniteGeneration/Shared/ directory.

With that, you can run the scene to see the world’s grid and three asteroids in each sector by default.

You can tweak the WhiteNoiseWorldGenerator’s properties in the Inspector to generate more or fewer asteroids per sector and see the output that white noise produces.

If you move around, you’ll see once you get to the edges of the generated sectors, the world is empty.

We’ll address that in the next lesson by generating new sectors as the player moves.