Next Tutorial Post – Monster Stats
Previous Tutorial Post – Player Stats
Goal
During this tutorial we’ll create our first monster, the lowly Kobold. We’ll also randomly create our Kobold and his friends in rooms across our map. At this point the Kobold will not act or even be able to be attacked. That will come later.
Choosing a Kobold Color
The most important part of creating our monster is choosing a color for him. I’m going to use an existing color from our swatch DbBrightWood. Pick any color you want and add it to Colors.cs.
public static RLColor KoboldColor = Swatch.DbBrightWood;
Create a Monster Base Class
Next we need to create the Monster base class that all of our monsters going forward will inherit from. At this point it will be pretty lackluster and not have any functionality but we will be adding to it in the future. Our Monster class is a type of Actor so it needs to inherit from Actor. Create a new class Monster.cs in the Core folder and add code that looks like the following:
public class Monster : Actor { // Empty for now, we will add functionality later }
Creating a Kobold
Our first Monster will be the reptilian Kobold. Create a new folder called Monsters and create a new class in the Monsters folder called Kobold.cs. The Kobold class will inherit from the Monster class that we made up above.
public class Kobold : Monster { public static Kobold Create( int level ) { int health = Dice.Roll( "2D5" ); return new Kobold { Attack = Dice.Roll( "1D3" ) + level / 3, AttackChance = Dice.Roll( "25D3" ), Awareness = 10, Color = Colors.KoboldColor, Defense = Dice.Roll( "1D3" ) + level / 3, DefenseChance = Dice.Roll( "10D4" ), Gold = Dice.Roll( "5D5" ), Health = health, MaxHealth = health, Name = "Kobold", Speed = 14, Symbol = 'k' }; } }
For now the Kobold will only have a Create() method. We’ll pass the current level of the dungeon into the Create() method so that as the player progresses deeper into the dungeon the Kobolds will get stronger. Feel free to assign the stats to whatever you want. I used the Dice class to roll for some random values. See my previous post about the Dice class for more examples of how that class works.
One thing to note is that we didn’t create a Draw() method for the Kobold, yet he will still be able to be drawn to our map just fine. The reason for this is because of the inheritance chain we set up. Kobold : Monster : Actor. Because an Actor already has a Draw() method the Kobold will get it automatically through inheritance!
Adding Monsters to the DungeonMap
Now that we have our first monster we need a way to be able to add them to our DungeonMap. Open DungeonMap.cs and add a new private field that is a List<Monster>. Make sure that you initialize the list in the constructor.
private readonly List<Monster> _monsters; public DungeonMap() { // Initialize all the lists when we create a new DungeonMap _monsters = new List<Monster>(); }
Next we’ll need to add a few new methods to DungeonMap.cs to help us add monsters.
public void AddMonster( Monster monster ) { _monsters.Add( monster ); // After adding the monster to the map make sure to make the cell not walkable SetIsWalkable( monster.X, monster.Y, false ); } // Look for a random location in the room that is walkable. public Point GetRandomWalkableLocationInRoom( Rectangle room ) { if ( DoesRoomHaveWalkableSpace( room ) ) { for ( int i = 0; i < 100; i++ ) { int x = Game.Random.Next( 1, room.Width - 2 ) + room.X; int y = Game.Random.Next( 1, room.Height - 2 ) + room.Y; if ( IsWalkable( x, y ) ) { return new Point( x, y ); } } } // If we didn't find a walkable location in the room return null return null; } // Iterate through each Cell in the room and return true if any are walkable public bool DoesRoomHaveWalkableSpace( Rectangle room ) { for ( int x = 1; x <= room.Width - 2; x++ ) { for ( int y = 1; y <= room.Height - 2; y++ ) { if ( IsWalkable( x + room.X, y + room.Y ) ) { return true; } } } return false; }
The first method AddMonster() will add a Monster to list of monsters currently on the map and make sure that the Cell the Monster is located in will not be walkable.
The next two methods GetRandomWalkableLocationInRoom() and DoesRoomHaveWalkableSpace() will both be useful for us in determining a good location to place our Monsters.
The last change to DungeonMap.cs will be to update the Draw() method to make sure that all of the Monsters on the map are also drawn.
// Iterate through each monster on the map and draw it after drawing the Cells foreach ( Monster monster in _monsters ) { monster.Draw( mapConsole, this ); }
Generating and Placing Monsters
We now have the ability to add Monsters to the DungeonMap. The only thing left to do is to actually generate our Monsters. We’ll update MapGenerator.cs and add a method to PlaceMonsters(). The algorithm will basically go through each room and roll a 10-sided die. If the result of the roll is 1-6 then we’ll roll 4-sided die and add that many monsters to the room in any open Cells. We will make extensive use of the methods that we previously added to DungeonMap.cs to help us out.
Add the following method to MapGenerator.cs
private void PlaceMonsters() { foreach ( var room in _map.Rooms ) { // Each room has a 60% chance of having monsters if ( Dice.Roll( "1D10" ) < 7 ) { // Generate between 1 and 4 monsters var numberOfMonsters = Dice.Roll( "1D4" ); for ( int i = 0; i < numberOfMonsters; i++ ) { // Find a random walkable location in the room to place the monster Point randomRoomLocation = _map.GetRandomWalkableLocationInRoom( room ); // It's possible that the room doesn't have space to place a monster // In that case skip creating the monster if ( randomRoomLocation != null ) { // Temporarily hard code this monster to be created at level 1 var monster = Kobold.Create( 1 ); monster.X = randomRoomLocation.X; monster.Y = randomRoomLocation.Y; _map.AddMonster( monster ); } } } } }
Don’t forget to add a call to PlaceMonsters() in the CreateMap() method.
public DungeonMap CreateMap() { // ... Existing code PlacePlayer(); // After the existing PlacePlayer() call, add another call to PlaceMonsters() PlaceMonsters(); }
If you run the game now you should have some ‘k’ shaped kobolds on your map. They won’t do anything interesting just sit there and act like walls. We will give them some interesting behaviors later on.
As always the code for the tutorial series so far can be found on Bitbucket:
https://bitbucket.org/FaronBracy/roguesharpv3tutorial/commits/tag/10MonsterGeneration
Bored waiting for the next tutorial post? The completed project is already available on Bitbucket.
Pingback: RogueSharp V3 Tutorial – Player Stats | Creating a Roguelike Game in C#
Pingback: RogueSharp V3 Tutorial – Monster Stats | Creating a Roguelike Game in C#
Great tutorial so far. A typo in the text here:
should be:
Nice catch! Thank you for pointing it out. I updated the post.
Hi when I do the part
public Point GetRandomWalkableLocationInRoom(Rectangle room)
{
if (DoesRoomHaveWalkableSpace(room))
{
for (int i = 0; i < 100; i++)
{
int x = Game.Random.Next(1, room.Width – 2) + room.X;
int y = Game.Random.Next(1, room.Height – 2) + room.Y;
if (IsWalkable(x, y))
{
return new Point(x, y);
}
}
}
return null;
}
I get the error "Cannot convert null to 'Point' because it is a non-nullable value"
After doing some digging I found the suggestion that I should change
public Point GetRandomWalkableLocationInRoom(Rectangle room)
to
public Point? GetRandomWalkableLocationInRoom(Rectangle room)
While that solved the issue at the time, when I got to this part in MapGenerator.cs
private void PlaceMonsters()
{
foreach ( var room in _map.Rooms )
{
if ( Dice.Roll( "1D10") < 7 )
{
//make between 1-4 monsters
var numberOfMonsters = Dice.Roll("1D4");
for ( int i = 0; i < numberOfMonsters; i++)
{
Point randomRoomLocation = _map.GetRandomWalkableLocationInRoom(room);
//It's possible that the room doesn't have room for monster
//In that case skip creation
if ( randomRoomLocation != null)
{
var monster = Kobold.Create(1);
monster.X = randomRoomLocation.X;
monster.Y = randomRoomLocation.Y;
_map.AddMonster(monster);
}
}
}
}
}
I receive the error "Cannot implicitly convert type 'RogueSharp.Point?' to 'RogueSharp.Point'. An explicit conversion exists (are you missing a cast?)
I thought maybe changing the 'Point' in this section to 'Point?' would solve it, but that gave a different error about how 'Point?' does not have a definition for X or Y and no accessible extension accepting the argument 'Point?' could be found.
What should I look for in order to fix this issue? Should I not use 'Point?' at all? And if not, how do I get around this?
figured it out, I changed
Point randomRoomLocation = _map.GetRandomWalkableLocationInRoom(room);
to
Point randomRoomLocation = (Point)_map.GetRandomWalkableLocationInRoom(room);
I found that online and it works, but I’d be lying if I said I really understood why
A little late here, but: it works because you’re explicitly casting _map.GetRandomWalkableLocationInRoom(room) to Point so that it matches the type of the variable randomRoomLocation. C# lets you do this because sometimes you’re sure the cast will work even when the compiler isn’t.
Unfortunately, it’s a bad idea here. The suggestion to change the return type of GetRandomWalkableLocationInRoom() to Point? was correct, because what that does is give you a nullable Point. But then if you’re returning a nullable point, your variable should be Point? too.Otherwise, when GetRandomEtc() does return null, you’ll get a runtime error! So you should really change your line to:
Point? randomRoomLocation = _map.GetRandomWalkableLocationInRoom(room);
I changed
return null;
to
return default(Point);
I am not sure why Point is not nullable, but default appears to be a functionally identical value for this purpose.
Has any one had monsters come in with walk-able turned on?? i get a weird thing where most are not walk-able but some are, usually near hallways i haven’t really gone through my code yet but i thought id see if this has happened to anyone else.
In what order does stuff occur in CreateMap()? If you’re running CreateHorizontalTunnel() and CreateVerticalTunnel() after PlaceMonsters() then that will do it; they set walkable to true on every cell they visit.
I have PlacePlayer() and PlaceMonsters() as the last things before return _map.