Roguelike C# in Unity (3/13): Generating a dungeon

Welcome to the 3rd part of the tutorial! The original Python tutorial can be found here. Previous posts for C# in Unity:

This is starting to get complex, in this tutorial we’ll keep working in our GridGenerator class, but move away entirely from the FloorSetup() and WallSetup() methods set previously to something entirely different: Creating a procedurally generated dungeon.

What we need?

  • Differentiate between Walkable and non-walkable tiles, in the future we can be more granular with this as walkable or non-walkable does not mean walls and floors necessarily, as a lava tile is walkable, but you’d rather prefer not to walk on it and does not block vision, however a mist tile is walkable, but blocks vision. For the moment we’ll go only with floors and walls for simplicity.
  • From the original tutorial we can see that the dungeon dungeon is generated by from a complete walled-off room, then we “digging” out sections to create the floor, therefore all tiles are blocked by default. In our case we’ll do it differently for 2 reasons: 1) We’re using TileMaps and these act as layer of tiles, so we won’t be digging anything into a TileMap but superposing different TileMaps with different setups to fit together. 2) We’ll be using a different algorithm, while the Python tutorial spawn rooms until there’s no space left and then connect rooms, we’ll use the Drunk Walker algorithm to create the playable scenario first, and wall the rest later.
  • Instead of relying in an unique CreateMap() method, we’ll split each part of the process in different methods, so we have a more granular control over what’s happening, these will:
  • 1) Create the initial setup, and create the floors: ProcGenFloorWalkersSetup()
  • 2) Create the walls where there’s no floors: ProcGenWallSetup()
  • 3) Fix the gaps the Walkers may have created (like crossing through non-playable area): ProcGenWallFixtures()
  • 4) Place our entities randomly across the map, only on valid coordinates (floor tiles): PlaceEntities() and PlaceItems()
  • 5) As a bonus, we’ll implement map seeding, so despite having procedural map generation and random placed entities, we can replicate this setup exactly as long as we use the same seed, this is great for debugging, improving the algorithm, fixing bugs, etc, …

Creating floors :

This is a bit long but bear with me, ProcGenFloorWalkersSetup() will get help from some other methods, and a new class:

First we’ll create a simple class for our Walkers, so each one can store and update independently both a position and a direction:

We’ll generate a bunch of initial Walkers in the center of the map, and then let them go crazy by looping through WalkerBehaviour() a certain number of times, I chose 10 initial Walkers and 1000 iterations but feel free to experiment with both the number of walkers and iterations for different map effects:

WalkerBehaviour() which will decide move each Walker on a different random direction, and open a new floor while doing it, and avoiding to pass the map boundaries. Each time this functions run, each walker of the list moves, replicates, or die, building a path of floor tiles in each new movement.

Check how we’re affecting 2 different TileMaps at the same time, we set tiles as floors, but at the same time we set to null the walls on that same position. This is because the wall TileMap is on top of the floors TileMap, if we don’t unset these wall tiles then the floors beneath it wouldn’t be visible. This is a little details that we must take into account in future iterations, as what may look like “not working” may be just “working but not visible”:

RandomDirection() moves the Walker in one of four directions, and turns the Walker around if happens to cross through the map limits.

This is a slow motion of what happens within a couple of frames:

This was long, but now we have a new procedurally generated map map each time we run the game:

This is a map without the boundaries behaviour on RandomDirection() , is not necessarily bad and could be solved in different ways. I’d rather locked this for simplicity.
And this is an example of a map being generated from (0,0) instead of the middle of the map, neither a bad solution, just needs different rework. Again I avoided this for simplicity and generated the map from the middle of the playable zone.

Creating walls:

ProcGenWallSetup() will be much much more simpler than the floors part, we’ll just check which tiles in the floor TileMap are null, and draw a wall in the wall TileMap for these positions:

Fixing the gaps:

Via ProcGenWallFixtures() I’ll fix the gaps that the walkers left into our map, that’s fill with walls these openings that let our player move outside of the playable area. Why is this set to Because by default will be undiscovered tiles, if we paint them normally these will be visible even if the player has not visited yet that part of the map. Then in the future once the player reaches that zone and the tiles enter in its Field Of Vision (FOV) these will be re-painted as normal.

I set those to green instead of black so we can easily see which tiles were fixed by ProcGenWallFixtures() . This is just the walls, the floors is an iteration to the algorithm that we’ll deal with in the future.

Placing our entities properly:

These should appear in random and valid floor tiles (not occupied by other items, enemies, the player, or walls) instead of in specific hardcoded places, this will be easy as we have been adding the vectors of the floor tiles to a listOfFloorTiles List, so we only have to pick random vectors from that list.

We can pass the Player through this function as well:

Or if you prefer, just spawn it in the middle of the map:

Map seed

This can be modified to be more advanced in the future, for example by setting a hash code instead, but for the moment a random number 1-100 is good enough.

Note that this is not a looped GIF but both runs of the same map are equal thanks to the pseudo-random generation via a pre-defined seed

In a nutshell, with this we’re using pseudo-random generation instead of random generation, as the “jumps” between each random value will be the same each time we run the random functions, the results will still be random, but will be replicable as long as we use the same seed, this results in the same map, same enemy position, same event handling, etc, …

Run the project now and you should be placed in a procedurally generated dungeon! By feeding the GridGenerator a specific seed you’ll generate the same level over and over again, a good way to start debugging other features could be to run a few random maps, and once you find one that you like, use that seed going forward.

Join 228 other subscribers


Leave a Reply