Tutorial Part 6: Create a Map and Map Generator

Now that we’ve got some foundational classes like TileBase and Actor ready to roll, they need a place to live and talk to each other in. So far we’ve used a simple _tiles[] array in the GameLoop to manage our tile data, but I’m of the ilk that prefers to keep the main game loop free and clean of game logic/data.

Before we get to work, let’s establish a few requirements for a Map class:

Let’s build the new Map class. We’re going to keep this class very light and simple, because in the future I’d like to hook into the GoRogue library to handle heavy-duty map generation, field-of-view, and lighting.

Here’s a basic Map class that stores our tile data, and lets us create a new map:

    using System;
    using Microsoft.Xna.Framework;

    // Stores, manipulates and queries Tile data
    public class Map
    {
        TileBase[] _tiles; // contain all tile objects
        private int _width;
        private int _height;

        public TileBase[] Tiles { get { return _tiles; } set { _tiles = value; }}
        public int Width { get { return _width; } set { _width = value; }}
        public int Height { get { return _height; } set { _height = value; }}

        //Build a new map with a specified width and height
        public Map(int width, int height)
        {
            _width = width;
            _height = height;
            Tiles = new TileBase[width * height];
        }
    }

Of note is the appearance of the _tiles[] array that we previously had stored in the GameLoop. It makes a lot more sense to store it in here, where we can create multiple instances of it, run queries on it, and transform it however we’d like.

Notice that we haven’t populated the map with any tile data in the constructor. We’ll save map generation for another step. Right now we just want a blank canvas to work with.

Let’s add some helper methods to the Map class that will let us query its tile data. Cut-and-paste the IsTileWalkable method from the GameLoop into the Map class.

        // IsTileWalkable checks
        // to see if the actor has tried
        // to walk off the map or into a non-walkable tile
        // Returns true if the tile location is walkable
        // false if tile location is not walkable or is off-map
        public bool IsTileWalkable(Point location)
        {
            // first make sure that actor isn't trying to move
            // off the limits of the map
            if (location.X < 0 || location.Y < 0 || location.X >= Width || location.Y >= Height)
                return false;
            // then return whether the tile is walkable
            return !_tiles[location.Y * Width + location.X].IsBlockingMove;
        }

You’ll probably notice a complaint from Visual Studio right off the bat:

‘TileBase.IsBlockingMove’ is inaccessible due to its protection level.

Ha! So it turns out I was a little over-eager to protect TileBase’s data in an earlier tutorial. I did not anticipate that I would create a new Map class that would need to peek into TileBase’s data. Thankfully, this is easily fixed. Modify the two following lines of the TileBase class accordingly:

        // Movement and Line of Sight Flags
        public bool IsBlockingMove;
        public bool IsBlockingLOS;

Now the Map class can peek into each tile’s IsBlockingMove and IsBlockingLOS properties.

I think that’s a good start for our Map class. Let’s move on to some basic generation and put our new class to work.

Generating Maps

Be forewarned – we’re going to toss a lot of our generation code in the near future as I transition the tutorials into using the GoRogue library. For the time being though, I think it’s worthwhile to explore some basic room generation techniques to get our minds wrapped around the basics of making Roguelike games.

One of my favourite room and passageway methods is called Tunnelling. This creates a dungeon that is primarily made of walls that we carve out rooms from, and then create connecting passages (tunnels) between those rooms. It’s quick, and most importantly, it’s easy to understand.

It’s time to create a new class: MapGenerator

MapGenerator Class

Unlike our other classes which are used to create objects (Wall objects, Floor objects, Map objects) – this class won’t create any MapGenerator instances. It won’t store any permanent data of its own. It exists purely to fill a Map object with map data. That means that MapGenerator’s constructor will remain empty.

Let’s deal with the MapGenerator class definition and its GenerateMap method first:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;

namespace SadConsoleRLTutorial
{
    // based on tunnelling room generation algorithm
    // from RogueSharp tutorial
    // https://roguesharp.wordpress.com/2016/03/26/roguesharp-v3-tutorial-simple-room-generation/
    public class MapGenerator
    {
        // empty constructor
        public MapGenerator()
        {
        }

        Map _map; // Temporarily store the map currently worked on

        public Map GenerateMap(int mapWidth, int mapHeight, int maxRooms, int minRoomSize, int maxRoomSize)
        {
            // create an empty map of size (mapWidth x mapHeight)
            _map = new Map(mapWidth, mapHeight);

            // Create a random number generator
            Random randNum = new Random();

            // store a list of the rooms created so far
            List<Rectangle> Rooms = new List<Rectangle>();

            // create up to (maxRooms) rooms on the map
            // and make sure the rooms do not overlap with each other
            for (int i = 0; i < maxRooms; i++)
            {
                // set the room's (width, height) as a random size between (minRoomSize, maxRoomSize)
                int newRoomWidth = randNum.Next(minRoomSize, maxRoomSize);
                int newRoomHeight = randNum.Next(minRoomSize, maxRoomSize);

                // sets the room's X/Y Position at a random point between the edges of the map
                int newRoomX = randNum.Next(0, mapWidth - newRoomWidth - 1);
                int newRoomY = randNum.Next(0, mapHeight - newRoomHeight - 1);

                // create a Rectangle representing the room's perimeter
                Rectangle newRoom = new Rectangle(newRoomX, newRoomY, newRoomWidth, newRoomHeight);

                // Does the new room intersect with other rooms already generated?
                bool newRoomIntersects = Rooms.Any(room => newRoom.Intersects(room));

                if (!newRoomIntersects)
                {
                    Rooms.Add(newRoom);
                }
            }

            // This is a dungeon, so begin by flooding the map with walls.
            FloodWalls();

            // carve out rooms for every room in the Rooms list
            foreach (Rectangle room in Rooms)
            {
                CreateRoom(room);
            }

            // spit out the final map
            return _map;
        }
}

Notice that we’re storing a copy of the game map in the class object. I’m doing this because it’s easier than passing around the current map as a parameter between every method in the class. I’m sure there is plenty of room for argument on which style is performant, but I care more about code habitability.

Take a look at the GenerateMap method. It is defined as a method that returns a Map when it completes execution. It takes the desired width and height of the map, max number of rooms to generate, and min/max room sizes as parameters.

The method begins by instantiating a new empty map, and stores that skeleton of information in a temporary variable called _map. We’ll be filling _map with walls and floors using our dungeon creation algorithm, and then returning its data when the method exits.

Then we create a new Random number generator. This wonderful tool comes from the System.Random namespace, and will become something that you’re intimately familiar with by the end of these tutorials. I’ll explain how they work in a bit.

Ah now, a new data structure! List<Rectangle> should be read aloud (or in your head if you’re doing this tutorial during work hours like a naughty sarariman) as “List of Rectangles”. A List is very similar to an Array in that it allows you to store an inventory of ONE type of data. Unlike an Array however, a List is dynamic – meaning that you can shrink and expand the number of items in the list as much as you want. We’re going to use a List of Rectangles, because we don’t know how many Rectangles we’re going to generate for building the dungeon.

Now for the meat of the dungeon generation algorithm. The for loop iterates i up to the maximum number of rooms we defined earlier as a parameter. newRoomWidth and newRoomHeight use the Random Number Generator randNum we created earlier. Let’s break down randNum‘s parameters:

randNum.Next(minRoomSize, maxRoomSize);

The .Next method tells the Random Number Generator to pull the next number it has available in sequence. Think of it like picking the next ball in the rotating drum at grandma’s bingo hall. The method’s (integer, integer) parameters specify the range of numbers to pick from – the first number is the minimum, and the second number is the maximum. In this case, we’re telling it that we want a random number between our minimum and maximum room sizes. Note that do this step twice: once to establish the width of the room, then its height.

Next, we randomly choose a position on the map to place the room. We’re using the map’s X and Y boundaries as our range for the generator. We then build a new Rectangle that represents this new room: it has an X/Y position for the top-left corner, using its width and height to establish the bottom-right corner.

Here’s where the fun begins:

bool newRoomIntersects = Rooms.Any(room => newRoom.Intersects(room));

Lambda (Lamba Lambda) expressions and Linq queries! Nuts and gum, together at last!

This code looks significantly different than any other code we’ve written so far, so let’s break down its structure into something understandable:

We’re creating a new boolean (true/false) variable called newRoomIntersects. It uses the list of rectangles (Rooms) method “Any”, and takes a long expression as its parameter. The “Any” method comes from System.Linq, and allows us to search a List (or any other Collection data structure) for a predicate – all that slick code sent as a parameter.

We could use a loop to dig through the entire list of rectangles to find what we want, but .Any makes life much easier and it’s one line instead of ten. It’s debatable how habitable it makes our code however, so let’s break down the predicate:

Rooms.Any(room => newRoom.Intersects(room))

A predicate is a function to test for. In this case, the predicate is room => newRoom.Intersects(room)

The Lambda operator is the => sign, which separates input variable from the lambda body. In our example, the input variable is room, and the body is newRoom.Intersects(room).

So what is room then? room represents every rectangle in our big list of rectangles. The newRoom.Intersects method is a wonderful built-in method of Rectangles that checks if any of the borders of the object overlap with another rectangle – in this case room.

Let’s put this all together in actual human language: Search through our list of Rectangles for Any rectangle that Intersects with our new rectangle. If we find a single intersection, respond with ‘true’.

Lambda expressions can take a while to sink in, so let’s move on to using the boolean we got from it. In the next block of code, if there are no intersections between our new rectangle and our list of existing rectangles, Add the new room to our List of Rooms. (By definition: if there is an intersection, drop the new rectangle like a bad third year computing science course and return to the beginning of the loop.)

When the for loop completes, we’ll have a large list of rectangles stored in Rooms. We’ll use that list to construct actual map data now.

We begin by flood-filling the entire map with walls. That’s something we’ve already done before, and it’s necessary because we’re building a dungeon that we carve rooms out of.

Then we use a wonderful new keyword: foreach. This is exactly like a for loop, except that it makes it much easier to address each item in a list because we don’t use an indexer. Instead, it relies upon enumeration. The foreach keyword takes two parameters separated by the “in” keyword. It humanreads: “For each instance of a specified variable type in a big list of the same kind of variables…” What it’s really doing is going through the list one item at a time, and storing that item in a temporary variable. In our case, it’s going through a big list of Rectangles Rooms, storing each entry in room every step of the way. The great thing is that every time it hits a new item in the list, it allows us to do something in the body using the current item. We’re going to build a CreateRoom() method to build each room in the list of Rooms.

And finally – when the method completes – it will output our final _map to whatever object we want it to.

Whew.

We still have a lot to go through in order to generate this map. We need to build the CreateRoom method, which will itself require a bunch of helper methods. Ah well, this is the life we chose.

        // Builds a room composed of walls and floors using the supplied Rectangle
        // which determines its size and position on the map
        // Walls are placed at the perimeter of the room
        // Floors are placed in the interior area of the room
        private void CreateRoom(Rectangle room)
        {
            // Place floors in interior area
            for (int x = room.Left + 1; x < room.Right - 1; x++)
            {
                for (int y = room.Top + 1; y < room.Bottom - 1; y++)
                {
                    CreateFloor(new Point(x,y));
                }
            }

            // Place walls at perimeter
            List<Point> perimeter = GetBorderCellLocations(room);
            foreach (Point location in perimeter)
            {
                CreateWall(location);
            }
        }

There are a few loops in this method. The first (nested) loop builds out the room by creating its interior area full of floors. This standard nested loop that iterates through x and y is one of the most common programming patterns you’ll see making 2D tile-based games. Notice that we’ve referenced a new helper method: CreateFloor. We’ll get to that soon.

The second part of the method creates a new list of Points (X/Y coordinates) using a new helper method called GetBorderCellLocations. It then iterates through the list using foreach, creating walls for each point in the perimeter.

Let’s create those helper methods now:

        // Creates a Floor tile at the specified X/Y location
        private void CreateFloor(Point location)
        {
            _map.Tiles[location.ToIndex(_map.Width)] = new TileFloor();
        }

        // Creates a Wall tile at the specified X/Y location
        private void CreateWall(Point location)
        {
            _map.Tiles[location.ToIndex(_map.Width)] = new TileWall();
        }

        // Fills the map with walls
        private void FloodWalls()
        {
            for (int i = 0; i < _map.Tiles.Length; i++)
            {
                _map.Tiles[i] = new TileWall();
            }
        }

The important stuff happening in this method involves the _map.Tiles array. We’re addressing each location (point) by using the .ToIndex(_map.Width) helper method, which translates X/Y coordinates into an array index. We could have just as easily wrote the code like this:

_map.Tiles[(location.Y * _map.Width + location.X)]

I prefer letting point.ToIndex() do all the work. Let’s deal with our next helper methods, all of which were stolen from the RogueSharp:

        // Returns a list of points expressing the perimeter of a rectangle
        private List<Point> GetBorderCellLocations(Rectangle room)
        {
            //establish room boundaries
            int xMin = room.Left;
            int xMax = room.Right;
            int yMin = room.Top;
            int yMax = room.Bottom;

            // build a list of room border cells using a series of
            // straight lines
            List<Point> borderCells = GetTileLocationsAlongLine(xMin, yMin, xMax, yMin).ToList();
            borderCells.AddRange(GetTileLocationsAlongLine(xMin, yMin, xMin, yMax));
            borderCells.AddRange(GetTileLocationsAlongLine(xMin, yMax, xMax, yMax));
            borderCells.AddRange(GetTileLocationsAlongLine(xMax, yMin, xMax, yMax));

            return borderCells;
        }

The method returns a list of Points (X/Y coordinates) based on an input Rectangle. We’re basically breaking down the perimeter of a rectangle into its constituent coordinates, one at a time. There are a few things worth pointing out in this method. Take a look at the room.Left, room.Right, room.Top, room.Bottom properties – those are read-only properties that give us the position of the edges of the rectangle.

Then we create a list of Points called borderCells representing the perimeter of the rectangle. At each stage of building the list, we’re taking one straight line and converting that into a series of points. Do that four times, representing each side of the rectangle. The .AddRange method adds the new list of points to the end of the borderCells list. GetTileLocationsAlongLine is a method that we’ll build next, allowing us to create those lists of points.

        // returns a collection of Points which represent
        // locations along a line
        public IEnumerable<Point> GetTileLocationsAlongLine(int xOrigin, int yOrigin, int xDestination, int yDestination)
        {
            // prevent line from overflowing
            // boundaries of the map
            xOrigin = ClampX(xOrigin);
            yOrigin = ClampY(yOrigin);
            xDestination = ClampX(xDestination);
            yDestination = ClampY(yDestination);

            int dx = Math.Abs(xDestination - xOrigin);
            int dy = Math.Abs(yDestination - yOrigin);

            int sx = xOrigin < xDestination ? 1 : -1;
            int sy = yOrigin < yDestination ? 1 : -1;
            int err = dx - dy;

            while (true)
            {

                yield return new Point(xOrigin, yOrigin);
                if (xOrigin == xDestination && yOrigin == yDestination)
                {
                    break;
                }
                int e2 = 2 * err;
                if (e2 > -dy)
                {
                    err = err - dy;
                    xOrigin = xOrigin + sx;
                }
                if (e2 < dx)
                {
                    err = err + dx;
                    yOrigin = yOrigin + sy;
                }
            }
        }

This is a chunky method, and it’s not extremely habitable. Because I intend to toss much of this code at a later date, I’m not going to rewrite it. It uses a lot of mathematical logic to build an enumerable series of points given a straight line. I won’t break down the loop into excruciating detail, except to point out a few things:

This code is meant to compress a ton of code into a very small space, but it’s hardly readable. I’m keeping it here because (a) it serves the need of getting our MapGenerator up and running, (b) because it reminds us that using expressive variable naming is important, (c) proper commenting is critical, and (d) that sometimes unwrapping compressed code into longer tracts of code is far more habitable.

Okay, two more helper functions and we can breathe again:

        // sets X coordinate between right and left edges of map
        // to prevent any out-of-bounds errors
        private int ClampX(int x)
        {
            if (x < 0)
                x = 0;
            else if (x > _map.Width - 1)
                x = _map.Width - 1;
            return x;
            // OR using ternary conditional operators: return (x < 0) ? 0 : (x > _map.Width - 1) ? _map.Width - 1 : x;
        }

        // sets Y coordinate between top and bottom edges of map
        // to prevent any out-of-bounds errors
        private int ClampY(int y)
        {
            if (y < 0)
                y = 0;
            else if (y > _map.Height - 1)
                y = _map.Height - 1;
            return y;
            // OR using ternary conditional operators: return (y < 0) ? 0 : (y > _map.Height - 1) ? _map.Height - 1 : y;
        }

I like Clamp. You’ll see it in lots of places while programming in different languages, and it surprises me that it does not exist in the System.Math namespace. Clamp sets a number within a range, preventing it from going “out-of-bounds”. That’s just awesome, because games are all about maps with boundaries.

The methods’ contents are fairly simple: if the input parameter is less than the map’s width or height, set it to zero. If the input parameter is larger than the map’s width of height, set it to the width or height.

Now, take a look at the commented line at the end of each method. Ternary conditional operators let you compress a ton of code into a single line. They’re also nearly impossible to read easily, especially when they have else-if conditions in them. That being said, you’re going to run into them everywhere where you find engineers who love their processors more than humans.

return (x < 0) ? 0 : (x > _map.Width - 1) ? _map.Width - 1 : x;

A ternary conditional operator is set up like this:

condition ? first expression : second expression;

The condition evaluates to true or false. If the condition is true, set the first expression as the result. If the condition turns out to be false, set the second condition as the result.

We are returning the outcome of everything after the return keyword. The statement reads aloud like this: If x is less than zero, return zero. If x is greater than or equal to zero, then check if x is greater than the map width minus one. If the x is greater than the map width minus one, return the map width minus one. If x is less than the map width minus one, just return x.

Ternary conditional operators are generally praised by lovers of Bashō and other self-starving ascetics. Likewise, they are generally despised by readers of Tolkien. There is no right answer here, except that you must choose, and choose wisely!

Whew. That’s our MapGenerator class. We’ve got some code cleanup to do in other classes before we start using it.

Head over to the GameLoop, and we can remove some code that is now extraneous:

private static TileBase[] _tiles;
private const int _roomWidth = 10;
private const int _roomHeight = 20;

CreateWalls();
CreateFloors();

//startingConsole.Fill(...)
startingConsole.Print(...)

private static void CreateFloors()
{ ... }

private static void CreateWalls()
{ ... }

public bool IsTileWalkable(Point location)
{ ... }

Then we’re going to modify the GameLoop’s definition to add a static instance of the game Map, because we’re only going to have one map for now. We’ll then set some static sizes for the map’s Width and Height, etc. In the Init method we’ll initialize the Map, initialize a new MapGenerator, and generate a new Map using the generator. Finally, we’ll modify the startingConsole to use our Map tiles instead of the old tile array. Your final GameLoop should look like this:

class GameLoop
    {

        public const int Width = 80;
        public const int Height = 25;
        private static Player player;

        public static Map GameMap;
        private static int _mapWidth = 100;
        private static int _mapHeight = 100;
        private static int _maxRooms = 500;
        private static int _minRoomSize = 4;
        private static int _maxRoomSize = 15;

        static void Main(string[] args)
        {
            // Setup the engine and create the main window.
            SadConsole.Game.Create("IBM.font", Width, Height);

            // Hook the start event so we can add consoles to the system.
            SadConsole.Game.OnInitialize = Init;

            // Hook the update event that happens each frame so we can trap keys and respond.
            SadConsole.Game.OnUpdate = Update;
                        
            // Start the game.
            SadConsole.Game.Instance.Run();

            //
            // Code here will not run until the game window closes.
            //
            
            SadConsole.Game.Instance.Dispose();
        }
        
        private static void Update(GameTime time)
        {
            // Called each logic update.
            CheckKeyboard();

        }

        // Scans the SadConsole's Global KeyboardState and triggers behaviour
        // based on the button pressed.
        private static void CheckKeyboard()
        {
            // As an example, we'll use the F5 key to make the game full screen
            if (SadConsole.Global.KeyboardState.IsKeyReleased(Microsoft.Xna.Framework.Input.Keys.F5))
            {
                SadConsole.Settings.ToggleFullScreen();
            }

            // Keyboard movement for Player character: Up arrow
            // Decrement player's Y coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Up))
            {
                player.MoveBy(new Point(0, -1));
            }

            // Keyboard movement for Player character: Down arrow
            // Increment player's Y coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Down))
            {
                player.MoveBy(new Point(0, 1));
            }

            // Keyboard movement for Player character: Left arrow
            // Decrement player's X coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Left))
            {
                player.MoveBy(new Point(-1, 0));
            }

            // Keyboard movement for Player character: Right arrow
            // Increment player's X coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Right))
            {
                player.MoveBy(new Point(1, 0));
            }
        }

        private static void Init()
        {
            // Initialize an empty map
            GameMap = new Map(_mapWidth, _mapHeight);

            // Instantiate a new map generator and
            // populate the map with rooms and tunnels
            MapGenerator mapGen = new MapGenerator();
            GameMap = mapGen.GenerateMap(_mapWidth, _mapHeight, _maxRooms, _minRoomSize, _maxRoomSize);

            // Create a console using gameMap's tiles
            Console startingConsole = new Console(GameMap.Width, GameMap.Height, Global.FontDefault, new Rectangle(0, 0, Width, Height), GameMap.Tiles);

            // Set our new console as the thing to render and process
            SadConsole.Global.CurrentScreen = startingConsole;

            // create an instance of player
            CreatePlayer();

            // add the player Entity to our only console
            // so it will display on screen
            startingConsole.Children.Add(player);

        }

        // Create a player using the Player class
        // and set its starting position
        private static void CreatePlayer()
        {
            player = new Player(Color.Yellow, Color.Transparent);
            player.Position = new Point(5, 5);
        }
    }

Now that we’ve moved around some of our methods, we need to make a few changes in other files. In the Actor class, change:

if (SadConsoleRLTutorial.GameLoop.IsTileWalkable(Position + positionChange))

to:

if (GameLoop.GameMap.IsTileWalkable(Position + positionChange))

Run the project. You’ll notice that it will take longer to run, as it is now using a MapGenerator to create the rooms. Notice that the rooms are all separate from one another, without connecting tunnels. That is the subject of our next tutorial!

Download the final source code for this tutorial here.

Published on November 4, 2018