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.
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.
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
.
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()
.
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.
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