Creating Roguelike Map Rooms
Now that we have some supporting classes and constants, it’s time to start generating the map for our roguelike game. Roguelike maps are made up of a network of rooms and the algorithm for creating the individual rooms is important. In this chapter, we’ll look at the use of loops and arrays in maintaining the collection of rooms.
This chapter is loaded with new concepts for beginners. You’ll see examples of:
- How to auto-generate new methods
- Using the Random class to generate pseudo-random numbers
- Arrays, FOR loops and simple type conversions in C#
- Decisions based on chance
This post is part of a series on creating a roguelike game in C#. For the latest chapters and more information, please visit the Official Project Page on ComeauSoftware.com. The current code for this project is also available on Github.
It’s time to start mapping …
Finally, we’re going to create some rooms on our game map! We’ll start with the MapLevel class constructor that we added previously. Currently, it’s not doing anything when the class is called so now you can add some code.
public MapLevel()
{
// Constructor - generate a new map for this level.
MapGeneration();
while(!VerifyMap())
MapGeneration();
}
This code references two methods that do not exist yet but we’re going to be creating them over the next few chapters. MapGeneration() will start the actual map creation process and VerifyMap() will test the map to ensure that it meets the requirements. The code also uses a while loop in order to re-create the map if it fails the verification. To generate the first method, do the following:
- Click on the MapGeneration name and use CTRL+. (period) to bring up the refactoring menu. You can also right-click on the name and select Quick Actions and Refactorings from the context menu.
- Select the Generate method ‘MapGeneration’ option.
- Your new method will be inserted directly below the constructor. You can remove the single line inside that throws the NotImplementedException.
Refactoring is the process of changing or improving code without actually changing its function. This might mean splitting one method into two to avoid repeating code or eliminating unnecessary steps. This action that you’re about to do is not technically a refactoring but is still helpful.
The throw line that I said to remove is an example of error handling in C# which we’re not doing yet. This line would pop up an error if the code called that method before any other functionality was added. Error handling is an important function in any programming language as unavoidable errors and unexpected conditions can and do happen. Before we get there, however, I like to fix the avoidable ones.
A note on terminology – I refer to both methods and functions in this course. I use the word ‘method’ for a function that does not return a value such as the MapGeneration() method. I use ‘function’ for one that does return a value as you’ll see later. Some sources use the word ‘function’ for both.
The Algorithm
In the chapter on algorithms, I laid out the high-level steps for creating the rooms and hallways. In this method, we’ll focus on the rooms themselves and that process has its own algorithm.
As we enter this code, keep in mind that it didn’t just spring into existence as fast as I could type it. This method and the hallway generation you’ll see later were the results of a lot of trial and error. The hallway generation algorithm I’ll show you later was not the first approach I tried before finding something that worked. I say this because it’s true of a lot of code that professional programmers write. Workable solutions rarely walk up and introduce themselves; you often have to work them out a piece at a time.
For the rooms themselves, the basic algorithm goes like this:
- Iterate through the map regions from left to right and top to bottom.
- Randomly decide if a room will be created in the region.
- Determine a random size within the limits defined by the constants.
- Center the room within that region and determine the top-left (northwest) corner.
- Determine the coordinates for the other walls from the anchor and the size values.
- Create MapSpace objects with the appropriate characters and coordinates.
- For each wall, randomly determine if it needs an exit.
- Randomly place an exit on the wall if it does and start a hallway.
- Make sure all the room corner spaces have been set.
Now, let’s turn that into code.
The Code
Remember that the current code for this project is available on Github.
In your new MapGeneration() method, enter the following code.
var rand = new Random();
int roomWidth = 0, roomHeight = 0, roomAnchorX = 0, roomAnchorY = 0;
// Clear map by creating new array of map spaces.
levelMap = new MapSpace[80, 25];
The first line creates a new variant object from the Random class which is used for generating random numbers. It uses a var type because the class can return random values of different types including integers and doubles.
As some people will make a point of telling you, there are no truly random numbers – this class actually uses a seed value from your system clock and its own algorithm to generate a different collection of pseduo-random numbers each time. You can also supply your own integer value in the call shown above if you like. Still, the numbers are a lot more randomized than many people are capable of on their own.
The next line initializes a few integers we’ll need, particularly the X and Y coordinates of the room’s northwest anchor. C# is strict about declaring all variables that will be used in code and, by convention, I like to do it at the top of a subroutine to keep things organized.
Finally, we re-instantiate the levelMap array in order to clear it. The array was declared and exists at class level, i.e. outside of any function, so it can retain its values independently of any of the other code. Re-instantiating the array is a quick way of removing all the contents.
Arrays
At this point, you should think back to the Excel grid that you saw in a previous lesson.
Arrays in C# can have up to 32 dimensions although it’s rare that you would use more than three. For a two-dimensional array like the levelMap, it’s easy to visualize it as a grid although it’s really just a collection of elements with coordinates assigned. A three-dimensional array could be shown as a cube while anything more would probably best be thought of as simply a list of coordinates unless you really like imagining n-dimensional spaces.
This is the point in the code where the math might get a little annoying for some and why it’s good to keep the grid as a reference. Array elements are, of course, numbered for reference and the numbering is zero-based meaning that if you have an 80 x 25 array, then the very first and last elements will be referenced as follows:
levelMap[0,0]
levelMap[79,24]
This usually means spending a lot of time subtracting 1 from variables in your code in order to ensure that you’re referencing the right element. In this case, we have a convenient reason not to do that – our map space needs to be divisible by three in both directions to get nine equal map regions so one “row” and two “columns” go unused in the array. This means that we can usually just start with element [1,1] and go from there.
The FOR loop
Earlier, you saw the WHILE loop which repeats an operation while a condition exists. In the next code, you’ll meet a loop that you’ll see in most languages you encounter in one form or another, the FOR loop.
This loop contains a counter variable which automatically limits the number of iterations. Its C# syntax is very flexible; you declare the counter variable, set a limiting condition and then increment it with each loop.
for(declaration; condition; increment){
}
In the case of the two-dimensional array, we need a nested FOR loop – one inside the other. Go ahead and enter the following code right after the line where you redeclared the levelMap array.
for (int y = 1; y < 18; y += REGION_HT)
{
for (int x = 1; x < 54; x += REGION_WD)
{
(code to be inserted here)
}
}
Again, we need to travel the grid left to right and top to bottom so the outside loop represents the “vertical” dimension and then the inside loop represents the “horizontal”. For each iteration of the outside loop, the inside loop repeats as many times as its definition allows. In effect, for each cycle of the outside “vertical” loop, it goes down one “row” and the inside loop iterates horizontally again.
The constants we defined earlier come back into use here. This loop needs to move over one region at a time so it can work with each region separately. Therefore, the counters are incremented by the height and width of the region with each iteration and will reference the following elements.
- [1,1]
- [1,27]
- [1,53]
- [9,1]
- [9,27]
- [9,53]
- [17,1]
- [17,27]
- [17,53]
If you look back at the screenshot of the Excel grid, you can verify that these coordinates represent the northwest corners of each region. The limiting conditions in each loop are set to stop the loop once it exceeds the numbers available in the third region for each direction.
There are also a couple new math operators here in including += :
x += REGION_WD
is equivalent to:
x = x + REGION_WD
Many of the other math operators in C# are the same as you probably learned in high school but there are many references online where you can review the full list and you should take the time to do so.
Measuring the room
Now, we plot the actual room within the array. Insert some new code within the FOR loops shown above.
for (int y = 1; y < 18; y += REGION_HT)
{
for (int x = 1; x < 54; x += REGION_WD)
{
if (rand.Next(101) <= ROOM_CREATE_PCT)
{
// Room size
roomHeight = rand.Next(MIN_ROOM_HT, MAX_ROOM_HT + 1);
roomWidth = rand.Next(MIN_ROOM_WT, MAX_ROOM_WT + 1);
// Center room in region
roomAnchorY = (int)((REGION_HT - roomHeight) / 2) + y;
roomAnchorX = (int)((REGION_WD - roomWidth) / 2) + x;
// Create room - let's section this out
// in its own procedure
RoomGeneration(roomAnchorX, roomAnchorY,
roomWidth, roomHeight);
}
}
}
When possible, I encourage you to type this code rather than copying it as it will give you experience with using the intellisense and other features in Visual Studio.
- The first new line (in bold) above shows the use of the rand variable we declared earlier. The Random function in C# is overloaded, meaning it has more than one set of options. This option shows the maximum exclusive value for the random integer being generated so it will give you a random integer between 1 and 100. If this value is less than or equal to the ROOM_CREATE_PCT constant, it will create a room. Otherwise, the room will be skipped. That’s how we get something to happen by chance.
- The room size uses the other overload of the Random function that accepts a minimum value and the maximum exclusive with the appropriate constants passed in. The results are stored in the variables we declared at the top of the method. This will give us nicely randomized rooms.
- Now we center the rooms by defining the X and Y value for the room’s northwest anchor point and we get into a bit more math. For the vertical coordinate, we subtract the room’s height from the region’s height, divide it by two, take the integer from the result and add the starting coordinate of the region to it. Then do the same for the horizontal.
For the example above, the X coordinate works out like this:
roomAnchorX = (int)((REGION_WD - roomWidth) / 2) + x;
roomAnchorX = (int)((26 - 18) / 2) + 27
roomAnchorX = 31
This is about the point during my own coding where I realized I needed the Excel grid.
As you might have already guess, the (int) syntax converts any number after it to an integer by simply dropping the portion after the decimal point. It does not round, it just drops the decimal – that’s an important distinction to be aware of when writing code like this. Order of operations is also an important concept and is not guaranteed to be the same from language to language so it’s good to review it. I use a lot of parentheses above to be safe.
Plotting the room
We now have all the numbers we need to plot the room within the array. The indentation levels in the code are getting a little deep at this point and that’s the main reason I decided to throw the rest of the code into another method and call it from here. This also makes the code more readable and compartmentalizes it for easier debugging. In Part 2, we’ll look at how that method actually puts the code into the array.
Sign up for our newsletter to receive updates about new projects, including the upcoming book "Self-Guided SQL"!
We respect your privacy and will never share your information with third-parties. See our privacy policy for more information.
0