13.designing-the-base-room

Designing the base room

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:

  1. Create a BaseRoom scene with common functionality for all rooms.
  2. Study the provided room script.
  3. Design a few rooms.

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:

  1. Will contain one Robot spawner.
  2. Will contain one Teleporter spawner.
  3. May contain a few pickup spawners.
  4. May contain a few mob spawners.

Each room will also have invisible walls, which we want to hide.

All rooms will spawn items and mobs, except:

  1. The first room (top-left) will spawn the robot and items; no mobs (we don’t want the player to be immediately attacked).
  2. The last room (bottom-right) will spawn the teleporter and mobs.

Additionally, some rooms will get special treatment.

The rooms at the borders will have their bridges hidden and replaced with invisible walls.

Going through the base room’s code

You will find a script called BaseRoom.gd. Drag it onto the top node and open it.

Note: We call this BaseRoom and not Room because there’s already a Godot built-in node called Room. We can’t use class names already taken by built-in nodes.

Let’s explore this script together. It does two main things:

  1. Trigger the various spawners.
  2. Hide bridges.

Spawning the room’s contents

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:

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:
            child.spawn()

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:
            child.spawn()

Hiding bridges

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 Rect2 values.

A Rect2 represents a rectangle and is defined by a Vector2 position and a Vector2 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 Rect2 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().

Enabling testing each room in isolation

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.

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

Creating a few rooms

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.