Drawing the procedural level

Now that we covered the _update_*() functions, we can look at the functions that place the actual rooms.

func _generate_level() -> void:
    _reset()
    _update_start_position()
    while _state.offset.y < grid_size.y:
    _update_room_type()
    _update_next_position()
    _update_down_counter()

    _place_walls()
    _place_path_rooms()
    _place_side_rooms()

In this part we’ll cover the _place_walls(), _place_path_rooms() and _place_side_rooms() functions.

Placing the outer walls

We place the outer walls of the level with _place_walls().

func _place_walls(type: int = 0) -> void:
    var cell_grid_size := _grid_to_map(grid_size)

    for x in [-1, cell_grid_size.x]:
        for y in range(-1, cell_grid_size.y + 1):
            level.set_cell(x, y, type)

    for x in range(cell_grid_size.x + 1):
        for y in [-1, cell_grid_size.y]:
            level.set_cell(x, y, type)

This function takes a type parameter which has the value 0 by default. This corresponds to our wall tile. We get the grid size of the TileMap via _grid_to_map(grid_size). This function is covered at the end of this part.

We then iterate over the two x positions: -1 and cell_grid_size.x. For each of these we go through all the cells from -1 to cell_grid_size.y and set the cell with TileMap.set_cell(). This creates the left and right boundaries of our level.

We do the same for all the horizontal cells between 0 and cell_grid_size.x at the two Y boundaries: -1 and cell_grid_size.y. This creates the top and bottom boundaries of our level.

Placing the path rooms

Next we have the _place_path_rooms() function.

func _place_path_rooms() -> void:
    for path in _state.path:
        yield(timer, "timeout")
        _copy_room(path.offset, path.type)
    emit_signal("path_completed")

This is a straightforward loop over _state.path. For each element we use _copy_room() to place the appropriate cells from the Rooms scene.

To delay the room placement, we wait on the Timer’s timeout signal using yield().

At the end of the process we call emit_signal("path_completed") so that the next function _place_side_rooms() will start. We’ll cover _copy_room() after we look at _place_side_rooms.

Placing the side rooms

func _place_side_rooms() -> void:
    yield(self, "path_completed")
    var rooms_max_index: int = _rooms.Type.size() - 1
    for key in _state.empty_cells:
        var type := _rng.randi_range(0, rooms_max_index)
        _copy_room(key, type)

When the path_completed signal is emitted, we again use _copy_room() to place cells for each position from _state.empty_cells. Remember that _state.empty_cells is a dictionary which stores the positions as keys. We pick a room type at random since it doesn’t change the valid path. These rooms can safely be Type.SIDE as well.

Let’s now take a look at the _copy_room() utility function.

func _copy_room(offset: Vector2, type: int) -> void:
    var map_offset := _grid_to_map(offset)
    var data: Array = _rooms.get_room_data(type)
    for d in data:
        level.set_cellv(map_offset + d.offset, d.cell)

Given an offset position in grid coordinates, we first convert it to TileMap coordinates with _grid_to_map(). Then we get the room data and loop over it, setting the appropriate cell with set_cellv().

Converting grid coordinates

The last functions that we need to cover are the _grid_*() functions.

func _grid_to_map(vector: Vector2) -> Vector2:
    return _rooms.room_size * vector

func _grid_to_world(vector: Vector2) -> Vector2:
    return _rooms.cell_size * _rooms.room_size * vector

_grid_to_map() maps a vector on the level grid to a position on the level TileMap.

_grid_to_world() maps a vector from the level grid to its position in pixels. This is its position in the world.

Recall that _grid_to_world() was used in the _setup_camera() function to calculate the camera.position property.

References

Here is the full RandomWalker.gd for reference.

extends Node2D


signal path_completed

const STEP := [Vector2.LEFT, Vector2.LEFT, Vector2.RIGHT, Vector2.RIGHT, Vector2.DOWN]

export (PackedScene) var Rooms := preload("Rooms.tscn")
export var grid_size := Vector2(8, 6)

var _rooms: Node2D = null
var _rng := RandomNumberGenerator.new()
var _state := {}
var _horizontal_chance := 0.0

onready var camera: Camera2D = $Camera2D
onready var timer: Timer = $Timer
onready var level: TileMap = $Level


func _ready() -> void:
    _rng.randomize()

    _rooms = Rooms.instance()
    _horizontal_chance = 1.0 - STEP.count(Vector2.DOWN) / float(STEP.size())

    _setup_camera()
    _generate_level()


func _setup_camera() -> void:
    var world_size := _grid_to_world(grid_size)
    camera.position =  world_size / 2

    var ratio := world_size / OS.window_size
    var zoom_max := max(ratio.x, ratio.y) + 1
    camera.zoom = Vector2(zoom_max, zoom_max)


func _generate_level() -> void:
    _reset()
    _update_start_position()
    while _state.offset.y < grid_size.y:
        _update_room_type()
        _update_next_position()
        _update_down_counter()

    _place_walls()
    _place_path_rooms()
    _place_side_rooms()


func _reset() -> void:
    _state = {
        "random_index": -1,
        "offset": Vector2.ZERO,
        "delta": Vector2.ZERO,
        "down_counter": 0,
        "path": [],
        "empty_cells": {}
    }
    for x in range(grid_size.x):
        for y in range(grid_size.y):
            _state.empty_cells[Vector2(x, y)] = 0


func _update_start_position() -> void:
    var x := _rng.randi_range(0, grid_size.x - 1)
    _state.offset = Vector2(x, 0)


func _update_room_type() -> void:
    if not _state.path.empty():
        var last: Dictionary = _state.path.back()

        if last.type in _rooms.BOTTOM_CLOSED and _state.delta.is_equal_approx(Vector2.DOWN):
            var index := _rng.randi_range(0, _rooms.BOTTOM_OPENED.size() - 1)
            var type: int = (
                _rooms.BOTTOM_OPENED[index]
                if _state.down_counter < 2
                else _rooms.Type.LRTB
            )
            _state.path[-1].type = type

    var type: int = (
        _rooms.Type.LRT
        if _state.delta.is_equal_approx(Vector2.DOWN)
        else _rng.randi_range(1, _rooms.Type.size() - 1)
    )

    _state.empty_cells.erase(_state.offset)
    _state.path.push_back({"offset": _state.offset, "type": type})


func _update_next_position() -> void:
    _state.random_index = (
        _rng.randi_range(0, STEP.size() - 1)
        if _state.random_index < 0
        else _state.random_index
    )
    _state.delta = STEP[_state.random_index]

    var horizontal_chance := _rng.randf()
    if _state.delta.is_equal_approx(Vector2.LEFT):
        _state.random_index = 0 if _state.offset.x > 1 and horizontal_chance < _horizontal_chance else 4
    elif _state.delta.is_equal_approx(Vector2.RIGHT):
        _state.random_index = 2 if _state.offset.x < grid_size.x - 1 and horizontal_chance < _horizontal_chance else 4
    else:
        if _state.offset.x > 0 and _state.offset.x < grid_size.x - 1:
            _state.random_index = _rng.randi_range(0, 4)
        elif _state.offset.x == 0:
            _state.random_index = 2 if horizontal_chance < _horizontal_chance else 4
        elif _state.offset.x == grid_size.x - 1:
            _state.random_index = 0 if horizontal_chance < _horizontal_chance else 4

    _state.delta = STEP[_state.random_index]
    _state.offset += _state.delta


func _update_down_counter() -> void:
    _state.down_counter = (
        _state.down_counter + 1
        if _state.delta.is_equal_approx(Vector2.DOWN)
        else 0
    )


func _place_walls(type: int = 0) -> void:
    var cell_grid_size := _grid_to_map(grid_size)

    for x in [-1, cell_grid_size.x]:
        for y in range(-1, cell_grid_size.y + 1):
            level.set_cell(x, y, type)

    for x in range(cell_grid_size.x + 1):
        for y in [-1, cell_grid_size.y]:
            level.set_cell(x, y, type)


func _place_path_rooms() -> void:
    for path in _state.path:
        yield(timer, "timeout")
        _copy_room(path.offset, path.type)
    emit_signal("path_completed")


func _place_side_rooms() -> void:
    yield(self, "path_completed")
    var rooms_max_index: int = _rooms.Type.size() - 1
    for key in _state.empty_cells:
        var type := _rng.randi_range(0, rooms_max_index)
        _copy_room(key, type)


func _copy_room(offset: Vector2, type: int) -> void:
    var map_offset := _grid_to_map(offset)
    var data: Array = _rooms.get_room_data(type)
    for d in data:
        level.set_cellv(map_offset + d.offset, d.cell)


func _grid_to_map(vector: Vector2) -> Vector2:
    return _rooms.room_size * vector


func _grid_to_world(vector: Vector2) -> Vector2:
    return _rooms.cell_size * _rooms.room_size * vector