Tutorial Part 13: Door Generation

Since I’m doing this entire roguelike tutorial bass-ackwards, why don’t we build some doors? I’ve been avoiding them for weeks months, knowing that I don’t have a great way of doing them using our existing map generator. I was hoping that I’d come up with a brilliant door placement strategy, but failing that, I’m falling back to my default strategy of stealing from improving upon RogueSharp.

First, some bugfixing. @Chris3606 noticed a nasty habit of mine – instantiating Random number generators in constructors, using them, then disposing of them. There’s a good example of this in the Monster class:

    public class Monster : Actor
    {
        public Monster(Color foreground, Color background) : base(foreground, background, 'M')
        {
            Random rndNum = new Random();

Seems reasonable, doesn’t it? Well, every time a new Monster object is constructed, it also creates a new Random number generator. The problem is that the seed value passed to the Random generator isn’t guaranteed to be different than the last time the generator was created. In the end, you’ll end up with non-random Monster generation which is pretty annoying in a Roguelike. Thankfully, the fix is simple:

    public class Monster : Actor
    {
        private Random rndNum = new Random();

        public Monster(Color foreground, Color background) : base(foreground, background, 'M')
        {

The Random number generator must be declared in the class declaration itself. Now, each time rndNum.Next() is called, the generator will be passing a new pseudorandom number.

This simple mistake is peppered all over the project – take some time to move any Random number generator object declarations from the body of methods, into the class header. I found a couple of them in the World class’s CreateMonsters and CreateLoot methods. There is another in MapGenerator’s GenerateMap method. Faugh!

Okay, and now some very light duty refactoring because I did something very silly in a previous tutorial, when we were adding the EntityViewSyncComponent to each Entity in the World class. If you caught this yourself and fixed it earlier, good on you! Here’s a good example of what I was doing:

newMonster.Components.Add(new EntityViewSyncComponent());

And then again here:

Player.Components.Add(new EntityViewSyncComponent());

And AGAIN here:

newLoot.Components.Add(new EntityViewSyncComponent());

It sure would make a lot more sense to add the EntityViewSyncComponent to Entity’s constructor, instead of repeating code ad infinitum, wouldn’t it? Delete the aforementioned occurrences, and then modify the Entity class like so:

using SadConsole.Components;

namespace SadConsoleRLTutorial.Entities
{
    ...
    {
        ...
        protected Entity(Color foreground, Color background, int glyph, int width = 1, int height = 1) : base(width, height)
        {
           ...
            // Create a new unique identifier for this entity
            ID = Map.IDGenerator.UseID();
            // Ensure that the entity position/offset is tracked by scrollingconsoles
            Components.Add(new EntityViewSyncComponent());

        }

Okay, time for some housecleaning. Create a Tiles sub-folder in your project if you haven’t already – these will store all of our Tile-related classes, including TileBase, TileFloor, TileWall – and yes – our next class, TileDoor.

Move those classes into the folder. This also means updating their namespaces to:

namespace SadConsoleRLTutorial.Tiles

Try to compile your project. Now you’ll see it complain of missing references. Update these missing references by adding the following directive at the top of the files:

using SadConsoleRLTutorial.Tiles;

TileDoor Class

Now it’s time to get down and dirty. Our doors are going to have a few properties:

Now let’s construct the new TileDoor class:

using System;
using Microsoft.Xna.Framework;
namespace SadConsoleRLTutorial.Tiles
{
    public class TileDoor : TileBase
    {

        public bool Locked; // Locked door = 1, Unlocked = 0
        public bool IsOpen; // Open door = 1, closed = 0

        //Default constructor
        //A TileDoor can be set locked/unlocked/open/closed using the constructor.
        public TileDoor(bool locked, bool open) : base(Color.Gray, Color.Transparent, '+')
        {
            //+ is the closed glyph
            //closed by default
            Glyph = '+';

            //Update door fields
            Locked = locked;
            IsOpen = open;

            //change the symbol to open if the door is open
            if (!Locked && IsOpen)
                Open();
            else if (Locked || !IsOpen)
                Close();
        }

        //closes a door
        public void Close()
        {
            IsOpen = false;
            Glyph = '+';
            IsBlockingLOS = true;
            IsBlockingMove = true;
        }

        //opens a door
        public void Open()
        {
            IsOpen = true;
            IsBlockingLOS = false;
            IsBlockingMove = false;
            Glyph = '-';
        }
    }
}

I’ve intentionally kept this class extremely simple for the time being. Later we’re going to add doorlocks and keys, but for now we just want to set the locks and open/closed states using some simple booleans.

Map Door Generation

Now we’re going to generate some doors using the room generation method we created several tutorials ago. If you’ve created your own room generation algorithm, you’re on your own here. If you’re working from the vanilla tutorials, this is a breakdown of the strategy:

Load up the MapGenerator class. This is where we’re going to create doors by modifying the GenerateMap method. Let’s add a new method first:

        //Tries to create a TileDoor object in a specified Rectangle
        //perimeter. Reads through the entire list of tiles comprising
        //the perimeter, and determines if each position is a viable
        //candidate for a door.
        //When it finds a potential position, creates a closed and
        //unlocked door.
        private void CreateDoor(Rectangle room)
        {
            List<Point> borderCells = GetBorderCellLocations(room);

            //go through every border cell and look for potential door candidates
            foreach (Point location in borderCells)
            {
                int locationIndex = location.ToIndex(_map.Width);
                if (IsPotentialDoor(location))
                {
                    // Create a new door that is closed and unlocked.
                    TileDoor newDoor = new TileDoor(false, false);
                    _map.Tiles[locationIndex] = newDoor;

                }
            }
        }

You might remember the GetBorderCellLocations method from an earlier tutorial. If you feed it a Rectangle, it returns a List of Points that make up the rectangle’s perimeter. Handy for this kind of job!

The foreach loop goes through each location in the border of the room, testing if the position is a viable candidate for a door using IsPotentialDoor. If it finds a viable candidate, it creates a closed and unlocked door there.

Well, obviously we need that IsPotentialDoor method now, so let’s take a crack at it:

        // Determines if a Point on the map is a good
        // candidate for a door.
        // Returns true if it's a good spot for a door
        // Returns false if there is a Tile that IsBlockingMove=true
        // at that location
        private bool IsPotentialDoor(Point location)
        {
            //if the target location is not walkable
            //then it's a wall and not a good place for a door
            int locationIndex = location.ToIndex(_map.Width);
            if (_map.Tiles[locationIndex] != null && _map.Tiles[locationIndex] is TileWall)
            {
                return false;
            }

            //store references to all neighbouring cells
            Point right = new Point(location.X + 1, location.Y);
            Point left = new Point(location.X - 1, location.Y);
            Point top = new Point(location.X, location.Y - 1);
            Point bottom = new Point(location.X, location.Y + 1);

            // check to see if there is a door already in the target
            // location, or above/below/right/left of the target location
            // If it detects a door there, return false.
            if (_map.GetTileAt<TileDoor>(location.X, location.Y) != null ||
                _map.GetTileAt<TileDoor>(right.X, right.Y) != null ||
                _map.GetTileAt<TileDoor>(left.X, left.Y) != null ||
                _map.GetTileAt<TileDoor>(top.X, top.Y) != null ||
                _map.GetTileAt<TileDoor>(bottom.X, bottom.Y) != null
               )
            {
                return false;
            }

            //if all the prior checks are okay, make sure that the door is placed along a horizontal wall
            if (!_map.Tiles[right.ToIndex(_map.Width)].IsBlockingMove && !_map.Tiles[left.ToIndex(_map.Width)].IsBlockingMove && _map.Tiles[top.ToIndex(_map.Width)].IsBlockingMove && _map.Tiles[bottom.ToIndex(_map.Width)].IsBlockingMove)
            {
                return true;
            }
            //or make sure that the door is placed along a vertical wall
            if (_map.Tiles[right.ToIndex(_map.Width)].IsBlockingMove && _map.Tiles[left.ToIndex(_map.Width)].IsBlockingMove && !_map.Tiles[top.ToIndex(_map.Width)].IsBlockingMove && !_map.Tiles[bottom.ToIndex(_map.Width)].IsBlockingMove)
            {
                return true;
            }
            return false;
        }

See, this is why it’s better to buy beer from the liquor store instead of brewing your own at home. Yeah, you get that nice feeling of accomplishment and warmth with homebrewed stuff, but an hour later you’re bloated and gassy and your burps taste like cat food. Brewing my own Door placement logic tastes pretty much the same. This method is bloated and heavy, and is computationally expensive. But it works.

This is a pretty heavy method, so let’s step through it. It basically does a bunch of checks to determine whether the target Point is worthy of receiving a brand new door. To pass that test the location must:

See that GetTileAt method? It’s sexy, and we’ll be creating it soon. Its purpose is to check for a specific kind of tile at a specified position, and returns true if it finds one there. This is one of those general purpose methods that will become more useful the deeper we went into this little project of ours. For now though, it’s going to tell us if there is a Door located at a position.

Finally, the method does some boolean and/or logic that helps to decide whether the door is being placed along a wall that is horizontal or vertical. This is a sanity check to make sure that the door isn’t being placed, say, in the middle of two intersecting rooms or tunnels. There always have to be walls above/below, or right/left, of the Door to ensure that this is an appropriate place for a door.

Now it’s time to create that GetTileAt method. Open your Map class and add this method:

        //really snazzy way of checking whether a certain type of
        //tile is at a specified location in the map's Tiles
        //and if it exists, return that Tile
        //accepts an x/y coordinate
        public T GetTileAt<T>(int x, int y) where T : TileBase
        {
            int locationIndex = Helpers.GetIndexFromPoint(x, y, Width);
            // make sure the index is within the boundaries of the map!
            if (locationIndex <= Width * Height && locationIndex >= 0)
            {
                if (Tiles[locationIndex] is T)
                    return (T)Tiles[locationIndex];
                else return null;
            }
            else return null;
        }

Remember that little talk we had about generics in Tutorial 11? The idea is to create a GetTileAt method that accepts any type of Tile that inherits TileBase using the T directive. The where T: TileBase keywords guarantee to the compiler that it can only accept objects of type TileBase. The definition reads aloud something like this: Return a TileBase of Some Type given that we know the X and Y coordinates. If the location has no Tile of that type, then return null.

Oh – and don’t forget to include a using SadConsoleRLTutorial.Tiles directive in there too.

Okay, now back to the MapGenerator class. Now we can actually generate the doors. Modify the GenerateMap method like this:

        public Map GenerateMap(int mapWidth, int mapHeight, int maxRooms, int minRoomSize, int maxRoomSize)
        {
...
            // carve out tunnels between all rooms
            // based on the Positions of their centers
            for (int r = 1; r < Rooms.Count; r++)
            {
...

            // Create doors now that the tunnels have been carved out
            foreach (Rectangle room in Rooms)
            {
                CreateDoor(room);
            }

            // spit out the final map
            ...

Make sure that the CreateDoor code is after the Tunnels have been carved out, but before the final map has been returned.

Run your project. Congratulations! With any luck you have doors littered all over your map and no way of getting past them. Don’t worry – we’ll cover that in the next tutorial. For now, enjoy your imprisonment 😀

Download the completed source files for the tutorial here.

Published on April 3, 2019