Rogue C# – Wandering the Map
In the last post, I showed you how to work with key events in C# and got the program responding to the four directional keys, even if it was only to say they were pressed. Now, it’s time to add the player to our roguelike map to wander around and collect some of that gold we tossed around a little while back. In the process, we need to stop the player from walking through walls or falling out of the hallways.
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.
Laying the foundation …
The Game class starts to get busier now as it will coordinate activity between the main form and the other classes in the project such as the Player class. Part of object oriented programming is deciding which class can best handle each new function and I had a few decisions to make here.
The Game class has access to the map array with all it’s information and is able to make changes to the individual elements but this can lead to unpredictable code that’s harder to support later. As I’ve mentioned before, it’s best to put code into a single function and call it rather than repeat it in many places. In general, I’m trying to maintain the MapLevel class as an interface to the map and the Game class as a manager that can call the map’s functions as needed rather than micro-managing it.
The first step was to make the MapSpace class a bit more capable. That’s the class that actually holds the character information for each space in the array. I realized that the two character properties, one showing the actual item present and one for a display character, were not enough. In this roguelike, I need a way to document:
- The actual map character (wall, doorway, room interior)
- Any item such as a scroll or gold that might be sitting on the map and then …
- The player or monster that could be sitting on top of both.
If a player moves on top of a scroll and can’t pick it up because their inventory is full, the scroll needs to still be there when they move away. If they pick it up later, I need the program to know that it should restore the character that will indicate that it was inside a room or in a hallway. Those characters are the only way the program has to navigate and tell what a space on the map represents.
public char MapCharacter { get; set; } // Actual character on map
public char? ItemCharacter { get; set; } // Item sitting on map
public char? DisplayCharacter { get; set; } // Player or monster
public bool SearchRequired { get; set; }
public bool Visible { get; set; } // Is space supposed to be visible.
public int X { get; set; }
public int Y { get; set; }
As the MapSpace class properties changed, this meant a bit of debugging as I had to sort out which property was being used for different things but Visual Studio’s search features make that pretty easy.
The Visible property will provide an explicit way to hide spaces and implement the fog of war rather than just assigning a blank space which could mean multiple things. I was hesitant to make the class more complicated but sometimes you just have to bow to the inevitable.
The Player class gets a new constant which you’ll see again later in other classes for monsters and items and a Location property to store a reference to the MapSpace class which was also moved outside of the MapLevel class so it can be declared independently.
public const char CHARACTER = '☺';
public MapSpace? Location { get; set; }
Then the MapLevel class gets a new function for placing items randomly.
public MapSpace PlaceMapCharacter(char MapChar, bool Living)
{
// Find a random space within one of the rooms that
// hasn't been occupied and return the array reference.
Random random = new Random();
int xPos = 1, yPos = 1;
bool freeSpace = false;
while (!freeSpace)
{
xPos = rand.Next(1, MAP_WD);
yPos = rand.Next(1, MAP_HT);
freeSpace = (levelMap[xPos, yPos].MapCharacter == ROOM_INT)
&& levelMap[xPos, yPos].DisplayCharacter == null
&& levelMap[xPos, yPos].ItemCharacter == null;
}
// If the character is for the player or a monster, add
// it to the Display character. Otherwise, use the item character.
if (Living)
levelMap[xPos, yPos].DisplayCharacter = MapChar;
else
levelMap[xPos, yPos].ItemCharacter = MapChar;
return levelMap[xPos, yPos];
}
The function looks for a space that the map character indicates is inside a room and where the other two character properties are null so there’s nothing else there. If the item to be placed is a player or monster, it puts the character in the DisplayCharacter property. Otherwise it goes in the ItemCharacter property. Then the function returns the MapSpace reference for that space.
After that, a single new line in the Game class constructor adds the player to the map and retrieves the MapSpace object for the Player.Location property.
this.CurrentPlayer.Location = CurrentMap.PlaceMapCharacter(Player.CHARACTER, true);
There’s our player in the room to the southwest. He looks ready to fight some monsters but first we have to get him moving.
Setting some boundaries …
It’s one thing to get the player moving around but every so often he might bump into things and we need to decide what to do then. There are five things a player might encounter on the map.
- Monsters (A-Z) – Running into one of these actually starts a fight and the program will have to manage that.
- Walls / Empty Spaces – There’s nothing in the game that lets the player or monsters walk through walls or whatever exists outside the rooms and hallways so we have to observe these boundaries.
- Room interiors, hallways and doorways – The player can walk all over these.
- Stairways and traps – These are map features the player can walk over and the program will need to respond if the player is stuck in a trap or uses a stairway.
- Everything else is an inventory item like scrolls and potions. The player will usually pick these up automatically unless their inventory is full so the program needs to recognize them and make the necessary decisions.
In the last chapter, the DungeonMap form was passing the four arrow keys to the Game class which just printed a notification they’d been pressed. Now it’s time to do something with them.
public void MoveCharacter(Player player, MapLevel.Direction direct)
{
// Move character if possible.
// List of characters a living character can move onto.
List<char> charsAllowed =
new List<char>(){MapLevel.ROOM_INT, MapLevel.STAIRWAY,
MapLevel.ROOM_DOOR, MapLevel.HALLWAY};
// Set surrounding characters
Dictionary<MapLevel.Direction, MapSpace> surrounding =
CurrentMap.SearchAdjacent(player.Location.X, player.Location.Y);
// If the map character in the chosen direction is habitable
// and if there's no monster there,
move the character there.
if (charsAllowed.Contains(surrounding[direct].MapCharacter) &&
surrounding[direct].DisplayCharacter == null)
player.Location =
CurrentMap.MoveDisplayItem(player.Location, surrounding[direct]);
}
This method is only in its starting stages but you’ll notice many of the constants from the MapLevel class here. They were previously marked private but I changed some of them to public to be accessed in the Game class as you see here. Public constants in a class are essentially static; they belong to the class itself and not an instance of the class. So they can be referenced as you see above.
The method creates a list of the character constants that a player is permitted to move onto. It then uses the SearchAdjacent function and Direction enumeration from the MapLevel class, which have also been rescoped as public, to look at the next character in four directions. Unlike constants, a public function or method must be accessed through a declared object.
Finally, if the MapCharacter in the direction the player is trying to move is included in the list of allowed characters and if the DisplayCharacter is null, meaning that there’s no monster there. The code calls another new, very simple, method, MoveDisplayItem() which simply moves the player’s display character to the new location and deletes it from the current one.
What finally makes this all work are some changes to the MapText() function which assembles the map for the form itself.
if(levelMap[x, y].Visible)
{
if (levelMap[x, y].DisplayCharacter != null)
sbReturn.Append(levelMap[x, y].DisplayCharacter);
else if (levelMap[x, y].ItemCharacter != null)
sbReturn.Append(levelMap[x, y].ItemCharacter);
else
sbReturn.Append(levelMap[x, y].MapCharacter);
}
- The MapSpace.DisplayCharacter is the first choice to be shown on the map. Remember this space shows the player and any monsters which are always on top of anything else.
- The ItemCharacter, showing gold, potions and other collectibles is the next priority.
- If there’s nothing in the other two, the MapCharacter will show whatever map feature is at that spot.
Of course, the real fun is finally getting to move a character around the map.
In the upcoming chapters, we’ll look at how to pick up that gold and add it to the player’s purse and then add other collectibles to the map and manage the player’s inventory.
What about the requirements map?
If you look back at the chapter on defining requirements, you’ll see that we’ve satisfied most of the first batch of requirements so far. The only things missing are the count of the map levels, the Amulet of Yendor and the fog of war feature that hides most of the map until the player discovers it. I’ll look at those in the next couple of chapters. After that comes the really fun part as we add collectibles and monsters and traps for the player to fall into!
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