It was a bit delayed due to Canadian Thanksgiving, but welcome to this week’s devblog! In this week’s article, we’ll explain how the technical aspects of the dungeon creation was done, as well as some other tricks we used to create the structure we have for the game.
When we first set out on this project, we wanted to deliver a fun, interesting experience that gave replayability value without losing level design. Procedural games often use a voxel system to generate the environment, but with that, it doesn’t give a sense of the environment being designed around gameplay. Another challenge we had was introducing progressive story elements into a game that had a procedural structure, so, we’ll be talking about how we developed around these limitations.
To tackle the first part, we decided on room based generation over completely random generation. The difference between the two is connecting a bunch of handcrafted areas together versus defining a floor tile, wall tile, prop tile, and putting them into a configuration based on rules. This puts a bit of a strain on level design, as it requires a lot of rooms to be created for it to feel different, but it’s a small price to pay for better gameplay.
After we chose to go with the room based generation, we made use of DunGen (https://www.assetstore.unity3d.com/en/#!/content/15682).
With DunGen, we decided to go with a dungeon separated into an almost endless (there’s actually a reachable ending) floor structure, which we can continuously add content to. Each floor is given a [Flow] that we define, leaving the majority of the generation to the algorithm but allowing us to input custom events and rooms along the way. By doing this, we can introduce the main storyline through crucial interactions and fights.
Lore is definitely something we want to expand on while the player explores the dungeon, so we created random spawners within the rooms that may spawn lore related things that the player can read. It’ll also take in account the player’s current progression decided by the game, and spawn lore relative to that.
We also extended the same functionality of progression based generation on items and enemies. In addition, we added some options to the spawning system to create enemies based on points we setup previously. It also adds enemies based on the density of enemies after the generation. This ensures that there’s a perfect number of monsters on a floor, and we can increase or decrease that if we ever need to.
One thing that we wanted to have were puzzles that made use of tools available as well as the environment. To “coerce” the player into solving these, we make use of triggers placed inside the rooms for scripted events, where a door may lock behind you, a wall of fire comes creeping up, and other random hazards. As well, we included parameters to each [Archetype] inside DunGen so that only a certain number of puzzle rooms would appear, but the selection would be random as well as the contents. For example, we could have a total of six different puzzle rooms on a floor, but only two would be selected of them all. This would at least give replayability, as well as provide a somewhat different experience for each player.
Scenes & Levels
Miraculously, there’s actually just one scene that’s loaded async over and over again for each dungeon floor! We make use of a database script that grabs references on startup, and we load the appropriate [Flow] when the player transitions onto another floor.
An issue came up with transitioning between floors: how do we preserve the information without a performance cost? To deal with this, we save the random seed when the player moves on from a floor, as well as saving some important aspects and removing them from the [Flow] so they don’t have to repeat the same encounter. Making use of this feature let us to try out a multiset puzzle which required the player to travel between floors, and it’s worked out pretty well.
Optimization on the generation was the next problem to handle. This was the part where the majority of our customization of the generation went. We tweaked the navmesh generation to be faster and run async runtime, so it didn’t slow the game down, but the dungeon generation was still somewhat of a bottleneck. A custom change was making use of a pooling system for the initial generation, then deleting any unused rooms after the generation was finished. It was a boost as whenever the generation failed, it wouldn’t destroy everything and recreate.
Bombs are working now on destructible environmental objects!
As well, we’ve implemented some universal behaviours, such as a pressure plate that sends events. Here’s a trap that uses it to pop out spikes.