To vary the levels in this game, we want to generate the map randomly from a list of premade rooms. This is a common approach to procedural level generation.
Each of these rooms should have four bridges to connect to the surrounding rooms and a shared structure.
Here, we’ll create a scene and script to use as the parent of all the other rooms.
In this chapter, you will:
In the rooms/
folder, duplicate the
TestRoom.tscn
file.
Name the new scene BaseRoom.tscn
, and open the
scene.
First, rename the top node to BaseRoom, so we don’t mistake it with the TestRoom.
Then delete the Robot, the Dummy, the Spawner, and any other additional node you might have added to try things.
Add:
You should end up with a structure like the below:
Our rogue-like will be made of rooms of equal size, sitting in a grid next to each other.
Each room will contain four bridges, which will allow us to connect them.
Each room:
Each room will also have invisible walls, which we want to hide.
All rooms will spawn items and mobs, except:
Additionally, some rooms will get special treatment.
The rooms at the borders will have their bridges hidden and replaced with invisible walls.
You will find a script called BaseRoom.gd
. Drag it onto
the top node and open it.
Note: We call this BaseRoom and not
because there’s already a Godot built-in node called . We can’t use class names already taken by built-in nodes.Let’s explore this script together. It does two main things:
We’ll look at the spawning code first.
Look at the left of the script text editor. You will find the script outline.
The outliner lists all the functions used in the script, which allows jumping from one to the other more easily. It lists functions in the script’s order by default.
Let’s look at the four spawn functions first:
spawn_mobs()
: will spawn all mobs.spawn_pickups()
: will spawn all pickups.spawn_robot()
: will spawn the robot.spawn_teleporter()
: will spawn the teleporter.Reminder: There’s one teleporter spawner and one robot spawner, but there may be any number of pickup or mob spawners in a given room.
Here’s what a completed room may look like. Notice how there are four mob and pickup spawners.
The spawn_robot()
and spawn_teleporter()
functions are really simple. They just forward the call to the
SpawnerRobot and SpawnerTeleporter, respectively.
Let’s look at spawn_mobs()
:
func spawn_mobs() -> void:
for child in _mobs.get_children():
if child is Spawner:
spawn() child.
The condition if child is Spawner
is not strictly
necessary, but we might want to have hand-placed mobs under the
Mobs node. Without this check, doing that would result in an
error.
The function to spawn items works similarly.
func spawn_items() -> void:
for child in _items.get_children():
if child is Spawner:
spawn() child.
We have four bridges in every room. In most cases, those bridges will
be 2x2
tiles, but we want to give ourselves the freedom to
make smaller or larger bridges.
For that reason, we export four
values.A
represents a rectangle and is defined by a position and a size.For example, Rect2(Vector2(10, 20), Vector2(150, 70))
would create a rectangle at position Vector2(10, 20)
, with
a width of 150
and a height of 70
.
Note: These are not visual rectangles. You cannot see them in the editor or the game. They are purely mathematical representations of rectangles.
We exported four
variables, each representing a bridge’s cells on the tilemap.export var top_bridge := Rect2(Vector2(5, -2), BRIDGES_DEFAULT_SIZE)
export var right_bridge := Rect2(Vector2(11, 4), BRIDGES_DEFAULT_SIZE)
export var left_bridge := Rect2(Vector2(-2, 4), BRIDGES_DEFAULT_SIZE)
export var bottom_bridge := Rect2(Vector2(5, 11), BRIDGES_DEFAULT_SIZE)
You can edit each of them in the Inspector:
For example, the Top Bridge in the image above starts at
cell 5
on the x-axis, at -2
on the y-axis, and
its size is 2
by 2
cells.
Then we have a function,
_hide_bridge(bridge_region: Rect2)
, which removes bridge
tiles and adds instead collision tiles, which block the player.
The function isn’t complicated, but it’s a little tedious to write and won’t teach you anything new. We commented it for you to read if you’d like to understand how it works, but we will not explain it here.
We then use this function in the four remaining methods,
hide_top_bridge()
, hide_left_bridge()
,
hide_right_bridge()
, and
hide_bottom_bridge()
.
We want to be able to test each room individually by running it with F6.
Normally, we want the game to manage the room and decide what gets spawned. But when we run the scene directly with F6, we want the room to manage its own spawning.
For that reason, we have one additional bit of interesting code in
_ready()
.
var is_main_scene = get_tree().current_scene == self
The SceneTree
’s current_scene
is the
topmost loaded scene.
To verify this, run any scene, and while the game is still running, go back to Godot, and click the Remote tab in the Scene dock.
This will show you the current running nodes in the running game.
get_tree().root
will return the current
viewport, typically the node named root in the image
above.get_tree().current_scene
will return the current
loaded scene, in this example, BaseRoom.If we’re running the room scene directly, it is the current scene.
In this case, we want to spawn the robot, the teleporter, the mobs, the items, and hide all bridges.
We do not hide the invisible walls because we might want to verify they work correctly.
But if we’re running a different main scene that loads the room, we only hide the walls and do nothing else. The room will be managed externally.
That’s what our _ready()
function does.
func _ready() -> void:
# If we instantiate this room in another scene, we want the other scene to
# manage the robot and the teleporter.
#
# But if we run the room scene directly with F6, we want to spawn the player
# and teleporter to test the room.
var is_main_scene = get_tree().current_scene == self
if is_main_scene:
spawn_robot()
spawn_teleporter()
spawn_mobs()
spawn_items()
hide_top_bridge()
hide_right_bridge()
hide_left_bridge()
hide_bottom_bridge()
else:
# hide invisible walls
_limits.hide()
With the base room ready, you want to create at least two inherited rooms. You need to create an inherited scene from the BaseRoom scene for each room in your game.
You can name them whatever you like. In our project, we called them
RoomA
, RoomB
, RoomC
, etc.
You can make them as different from each other as you like. Just make sure you preserve the four bridges.
You can add as many SpawnerMob
and
SpawnerItem
as you like.
Here are two rooms we made, for example:
To draw a room like this, you want to draw the floor on the Floor tilemap, then the walls on the Walls tilemap.
Don’t forget to add the necessary limits on the Limits tilemap (the red crosses above)! They’re what prevent the player from walking out of the rooms.
Feel free to add items, sprites, and other props. You’ll find assets
in res://props/details/
. Have fun!
Test each room in isolation to ensure that it works as intended. Once you’re satisfied, let’s work on putting it all together!
We’ll do that in the next guide.