How to Create Dungeon Prefabs for a Roguelike Game


This article is also avaiable on GitHub.

Introduction

A Roguelike game usually takes place in a procedurally generated dungeon, more specifically, passable grounds and impassable buildings are created by code following certain rules. Each dungeon has similar elements but their amount and combination are more or less random. However, sometimes I find it more convenient to create a dungeon based on hand-crafted prefabs. I will explain in detail about why and how to use dungeon prefabs based on my own project: One More Level, a turn-based, single player Roguelike game made with Godot engine.

This article has three parts. Part 1 explains what a prefab is in One More Level. Part 2 shows three examples. The last part is about writing code to use prefabs during dungeon generation.

The Content and Classification of Prefabs

In One More Level, a prefab is simply a text file. Every character in the file can be one of four objects in the game:

  • Ground: PC and NPCs can stand on and pass through a ground grid.
  • Building: A building blocks movement. Sometimes PC or NPCs can interact with a building.
  • Trap: A trap does not block movement and is interactable.
  • Actor: Any object that either has an AI or is controlled by player is an actor.

Grounds and buildings are more common than traps and actors in a prefab.

Prefabs can be classified by size or “purity”.

  • Size
    • Full-size: A single prefab that covers the whole dungeon.
    • Jigsaw: A group of prefabs that are adjacent to each other and cover the whole dungeon.
    • Island: A group of prefabs that do not touch each other. The dungeon has grids that are not covered by any prefabs.
  • “Purity”:
    • Pure: A dungeon that only contains prefabs.
    • Mixed: A dungeon that is composed of prefabs and code generated content.

Next, I will present three types of dungeons in One More Level and explain the usages of prefabs.

Example 1: Ninja

In Ninja, PC needs to bump and kill all Ninjas to beat the game. The combat happens in a long and narrow elevator shaft. Enemies fall down from the top of the screen. Since I have a clear image of the scene in which every wall and ground has a fixed position, I build the dungeon from a pure, full-size prefab.

Ninja

Figure 1: Ninja.

Part of the fun in Roguelike games is exploring random content. In this dungeon, the randomness comes from actors and traps, rather than buildings or grounds. Ninjas appear at random positions. PC can decide when to hit a ninja. A dead ninja leaves a soul fragment behind, which is a trap that helps PC to win. On the other hand, some ninjas can remove existing soul fragments. A more detailed explanation of game mechanics is beyond the scope of this article. I’d like to conclude this part by pointing out that a Roguelike game can use a static map and still provides enough challenge.

Example 2: Baron

This time, the dungeon is equally divided into 7x5 blocks. Every block is a jigsaw prefab. When generating a new dungeon, the game picks 9 blocks out of 25 candidates, flips them horizontally or vertically or leaves them unchanged, which means an asymmetric prefab is favored over a symmetric one, and then puts them in random places.

Baron

Figure 2: Baron.

PC acts as the baron in the trees and he needs to find out bandits in the woods. I build the dungeon with prefabs because I find it difficult to implement my idea by code. I want to create a map by these rules.

The map has two layers: ground layer and canopy layer. The ground layer is composed of passable grounds and impassable tree trunks. All ground grids are connected. The canopy layer is composed of tree trunks and tree branches. All canopy grids are passable and connected.

A tree branch must be adjacent to a tree trunk. A tree trunk can have any number of neighbors. PC can wait on a tree trunk but not on a tree branch.

Bandits walk on the ground. They cannot climb a tree. PC and birds stay on the canopy. They cannot descend to the ground. Birds are always visible to PC. A bandit is visible if he is close to PC and is not covered by a tree branch.

In order to guarantee grids are connected, there are two additional rules for a prefab:

  • More than half of the grids on an edge are either grounds or tree branches.
  • More than half of the grids on an edge are either tree trunks or tree branches.

In order to make the game more challenging, there should be more tree branches where bandits can hide under and PC cannot wait above. On the other hand, adding too many tree trunks lower the difficulty for PC and thus should be avoided.

A jigsaw prefab is smaller than the whole dungeon, then how to design a collection of prefabs that meet the requirements above and are as diverse as possible? My solution is to define a few types first, then fill in each type with a few prefabs. There are 5 types of Baron prefabs based on the connectivity of their edge grids.

  • Type A: All edge grids are connected.
  • Type B: There is at least one isolated grid on each edge.
  • Type C: All horizontal edge grids are connected.
  • Type D: All vertical edge grids are connected.
  • Type E: There are exactly 1 or 3 edge(s) with connected grids.

Each type has 5 sub types: 0 to 4 corners are occupied by a tree trunk or a tree branch.

5 types of Baron prefabs

Figure 3: 5 types of Baron prefabs.

Example 3: Factory

The Factory is consisted of workshops of various sizes. They are island prefabs behind the scene.

  • 20 big workshops: 7x7 grids
  • 2 small workshops: 3x3 grids
  • 1 starting workshop: 3x3 grids
  • 1 door frame: 3x1 grids

Every dungeon has a starting workshop, at least 2 big workshops, a few small workshops and door frames. They are selected randomly, rotated or flipped or left unchanged, and put into a place that is at least 1 grid away from another building.

Factory

Figure 4: Factory.

I use island prefabs because I want to create a dungeon that is ordered in a small scale but unordered in a big scale. Therefore, all workshops are hand crafted but they can choose their neighbors freely.

The one-grid-path between two buildings guarantees that a door cannot be blocked by another building. But what if a building is adjacent to one or two dungeon edges? Adding three doors to three different sides of a big workshop can solve this problem.

A big workshop can be grouped by the size of its square (0x0, 2x2, 2x3, 3x3, 2x4, 4x4). For a given type of workshop, it can be further grouped by the shape of its rooms (one-grid-wide corridors or rectangles).

Big workshop prefabs

Figure 5: Big workshop prefabs.

Write Code to Parse, Modify & Build from Prefabs

To put a prefab into use requires four steps.

  • Design a prefab.
  • Write code to read the prefab file.
  • Modify the prefab if necessary.
  • Generate a dungeon based on the prefab data.

I use REXPaint to create a prefab and then export a text file. All the prefabs for One More Level are stored in two folders: *.xp file, *.txt file.

The next step is to open and read the prefab text file, and then output the file content. These procedures mainly depend on the programming language. As for my project, I use a custom function read_as_line(path_to_file: String) -> Game_FileParser. The file content is stored in a dictionary (FileParser.output_line), in which the keys are line numbers and the values are strings. Refer to two scripts for more information: FileIOHelper.gd, FileParser.gd.

A prefab is modified in two stages, refer to DungeonPrefab.gd for more information. We can get a character in a prefab by FileParser.output_line[line_number][string_index], which is equivalent to FileParser.output_line[y][x], see Figure 6. Since I am more accustomed to search a character first by x then y, I create a new dictionary in which new_dict[x][y] = FileParser.output_line[y][x].

 string_index
--------------> x
|
| line_number
|
v y

Figure 6: Characters in a prefab.

As mentioned above, a prefab can be flipped or rotated if necessary. Therefore I need three functions: _horizontal_flip() -> Dictionary, _vertical_flip() -> Dictionary and _rotate_right() -> Dictionary.

Flipping is quite straightforward. The core part of _horizontal_flip() is as follows.

for y in range(0, max_y):
    for x in range(0, max_x):
        flip_x = max_x - x - 1
        if x > flip_x:
            break
        save_char = dungeon[x][y]
        dungeon[x][y] = dungeon[flip_x][y]
        dungeon[flip_x][y] = save_char

In order to rotate a prefab clockwise by 90 degrees, first rotate it around the origin of coordinates, then move it to the right horizontally.

Rotate a prefab

Figure 7: Rotate a prefab.

The core part of _rotate_right() is as follows.

for x in range(0, max_x):
    for y in range(0, max_y):
        new_x = max_y - y - 1
        new_y = x
        new_dungeon[new_x][new_y] = dungeon[x][y]

The last step is tightly coupled with a specific game. Readers have to rely on themselves from now on. This article ends here.

Get One More Level

Leave a comment

Log in with itch.io to leave a comment.