Posted August 14, 2022 by Bozar
This article is also avaiable on GitHub.
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.
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:
Grounds and buildings are more common than traps and actors in a prefab.
Prefabs can be classified by size or “purity”.
Next, I will present three types of dungeons in One More Level and explain the usages of prefabs.
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.
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.
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.
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:
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.
Each type has 5 sub types: 0 to 4 corners are occupied by a tree trunk or a tree branch.
Figure 3: 5 types of Baron prefabs.
The Factory is consisted of workshops of various sizes. They are island prefabs behind the scene.
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.
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).
Figure 5: Big workshop prefabs.
To put a prefab into use requires four steps.
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.
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.