Tutorial 6 – Roguelike Combat using RogueSharp and MonoGame

Edit: This tutorial was written using RogueSharp 1.1 so if you want to follow it exactly make sure you are using that version. After RogueSharp version 2.0 released there were some changes to the way that Dice work. See the updates here to get the required code changes to make this work with version 2.0 of RogueSharp.

This is part 6 in a set of tutorials for using RogueSharp and MonoGame to create a basic Roguelike game. It is meant to serve as an introduction to the RogueSharp library.

If you missed part 5 in the tutorial you can find it here: https://roguesharp.wordpress.com/2014/07/13/tutorial-5-creating-a-2d-camera-with-pan-and-zoom-in-monogame/

Goal

In this tutorial we will add the ability for the Player to fight monsters and take damage. Up until this point we created a monster that would chase the player around the map there wasn’t really anything they could do except follow the player.

Enemy Awareness

When we introduced pathfinding we also created an aggressive enemy that had an all knowing awareness of where the player was at all times. We’re going to start by changing that. Enemies will only be aware of the player once they enter field-of-view.

Open up AggressiveEnemy.cs and update the constructor to also take in a parameter of type IMap. This will give the enemy access to field-of-view. This is a not great as it is coupling our Enemy class to the Map class, but it gets the job done.

// Add two private member variables
private readonly IMap _map;
private bool _isAwareOfPlayer;

// Update constructor to also take in an IMap
 public AggressiveEnemy( IMap map, PathToPlayer path )
{
  _map = map;
  ...
}

Next find the Update() method in AggressiveEnemy.cs and add the following code to create behavior for enemy awareness of the player.

public void Update()
{
  if ( !_isAwareOfPlayer )
  {
    // When the enemy is not aware of the player
    // check the map to see if they are in field-of-view
    if ( _map.IsInFov( X, Y ) )
    {
      _isAwareOfPlayer = true;
    }
  }
  // Once the enemy is aware of the player
  // they will never lose track of the player
  // and will pursue relentlessly
  if ( _isAwareOfPlayer )
  {
    _path.CreateFrom( X, Y );
    X = _path.FirstCell.X;
    Y = _path.FirstCell.Y;
  }
}

Don’t forget to also update the LoadContent() method in Game1.cs to also include the map as a parameter when we create a new AggressiveEnemy.

// Add _map parameter when we new up an enemy
_aggressiveEnemy = new AggressiveEnemy( _map, pathFromAggressiveEnemy )

The code so far

Multiple Enemies

Only having one enemy in our game is not very interesting. Lets look at Game1.cs and the ability to have a bunch of enemies.

// Switch the member variable to a list of enemies instead of a single enemy
private List<AggressiveEnemy> _aggressiveEnemies = new List<AggressiveEnemy>();

// Create a new method that will add a bunch of enemies to the game
private void AddAggressiveEnemies( int numberOfEnemies )
{
  for ( int i = 0; i < numberOfEnemies; i++ )
  {
    // Find a new empty cell for each enemy
    Cell enemyCell = GetRandomEmptyCell();
    var pathFromAggressiveEnemy =
      new PathToPlayer( _player, _map, Content.Load<Texture2D>( "White" ) );
    pathFromAggressiveEnemy.CreateFrom( enemyCell.X, enemyCell.Y );
    var enemy = new AggressiveEnemy( _map, pathFromAggressiveEnemy ) {
      X = enemyCell.X,
      Y = enemyCell.Y,
      Sprite = Content.Load<Texture2D>( "Hound" )
    };
    // Add each enemy to list of enemies
    _aggressiveEnemies.Add( enemy );
  }
}

// Replace the code in LoadContent that created an enemy
// with a call to our new AddAggressiveEnemies method
protected override void LoadContent()
{
...
  AddAggressiveEnemies( 10 );
...
}

// Replace the code in the Update method with a foreach
// to make sure each enemy's Update method is called
protected override void Update( GameTime gameTime )
{
...
  foreach ( var enemy in _aggressiveEnemies )
  {
    enemy.Update();
  }
...
}

// Replace code in the Draw method with a foreach
// to make sure each enemy's Draw method is called
protected override void Draw( GameTime gameTime )
{
...
  foreach ( var enemy in _aggressiveEnemies )
  {
    _aggressiveEnemy.Draw( spriteBatch );
    if ( Global.GameState == GameStates.Debugging || _map.IsInFov( enemy.X, enemy.Y ) )
    {
      enemy.Draw( spriteBatch );
    }
  }
...
}

If we run the game now we should see multiple hounds chasing our poor player around.

Multiple Enemies

Multiple Enemies

The code so far

Extract Base Class

We are going to need to add combat stats to both the player and the enemies. Before we do that, lets do a little refactoring and extract out a base class from both Player.cs and AggressiveEnemey.cs. I’m going to name this class Figure since I play a lot of tabletop games and I like to think of all of our monsters and the player as miniature figures on a map.

// The new figure class has common properties shared
// by both enemy and player classes
public class Figure
{
  public int X { get; set; }
  public int Y { get; set; }
  public Texture2D Sprite { get; set; }
}

// Have our enemy class inherit from Figure and remove
// X, Y and Sprite properties
public class AggressiveEnemy : Figure
...

// Do the same thing with the Player class
public class Player : Figure
...

The code so far

Add Combat Stats to Figures

There are tons of different ways combat could be handled. I’m going to use an approach familiar to table top D20 roleplaying games. In this system the attacker will roll a 20-sided die and add their attack bonus. If this result meets or exceeds the opponents armor class, the it is a hit. Otherwise it is a miss. Lets go ahead and add these properties to Figure.cs.

// Roll a 20-sided die and add this value when making an attack
public int AttackBonus { get; set; }
// An attack must meet or exceed this value to hit
public int ArmorClass { get; set; }
// Roll these dice to determine how much damage was dealt after a hit
public Dice Damage { get; set; }
// How many points of damage the figure can withstand before dieing
public int Health { get; set; }
// The name of the figure, used for attack messages
public string Name { get; set; }

Now we need to revisit Game1.cs and make sure that when we create the player and enemies that we also provide values for these properties

// In the LoadContent method update this code
_player = new Player
{
  X = startingCell.X,
  Y = startingCell.Y,
  Sprite = Content.Load<Texture2D>( "Player" ),
  // With a 15 armor class if the enemy has no attack bonus
  // the player will be hit 25% of the time
  ArmorClass = 15,
  AttackBonus = 1,
  // The player will roll 2D4 for damage or 2 x 4 sided Die
  // We can use the Dice class in RogueSharp for this
  Damage = new Dice( new List<IDie> {
    new Die( Global.Random, 4 ),
    new Die( Global.Random, 4 ) } ),
  // The player can take 50 points of damage before dying
  Health = 50,
  Name = "Mr. Rogue"
};

// In the AddAggressiveEnemies method update this code
var enemy = new AggressiveEnemy( _map, pathFromAggressiveEnemy )
{
  X = enemyCell.X,
  Y = enemyCell.Y,
  Sprite = Content.Load<Texture2D>( "Hound" ),
  // Hounds will get hit 50% of the time with no attack bonus
  ArmorClass = 10,
  AttackBonus = 0,
  // Hounds roll one 3 sided Die for damage
  Damage = new Dice( new List<IDie> {
    new Die( Global.Random, 3 ) } ),
  Health = 10,
  Name = "Hunting Hound"
};

The Dice and Die classes are part of the RogueSharp library. They can be a convenient abstraction on top of random number generators. I did realize while doing this tutorial that they are currently a bit cumbersome to create. I plan on updating RogueSharp soon to use System.Random by default if no random number generator is specified. I also plan on allowing you to create a new batch of dice by providing a string parameter to the constructor in dice notation from tabletop gaming. “2D20” or “3d6” for example.

The code so far

Introducing a Combat Manager

It would be useful to have a class that is responsible for handling combat. It will have a method for figures to call to make an attack and it will also have some utility functions for looking up which figures are on different map cells. Make a new class called CombatManager.

public class CombatManager
{
  private readonly Player _player;
  private readonly List<AggressiveEnemy> _aggressiveEnemies;

  // When we construct the CombatManager class we want to pass in references
  // to the player and the list of enemies.
  public CombatManager( Player player, List<AggressiveEnemy> aggressiveEnemies )
  {
    _player = player;
    _aggressiveEnemies = aggressiveEnemies;
  }

  // Use this method to resolve attacks between Figures
  public void Attack( Figure attacker, Figure defender )
  {
    // First create a twenty-sided die
    var attackDie = new Die( Global.Random, 20 );
    // Roll the die, add the attack bonus, and compare to the defender's armor class
    if ( attackDie.Roll() + attacker.AttackBonus >= defender.ArmorClass )
    {
      // Roll damage dice and sum them up
      int damage = attacker.Damage.Roll().Sum();
      // Lower the defender's health by the amount of damage
      defender.Health -= damage;
      // Write a combat message to the debug log.
      // Later we'll add this to the game UI
      Debug.WriteLine( "{0} hit {1} for {2} and he has {3} health remaining.",
        attacker.Name, defender.Name, damage, defender.Health );
      if ( defender.Health <= 0 )
      {
        if ( defender is AggressiveEnemy )
        {
          var enemy = defender as AggressiveEnemy;
          // When an enemies health dropped below 0 they died
          // Remove that enemy from the game
          _aggressiveEnemies.Remove( enemy );
        }
        // Later we'll want to display this kill message in the UI
        Debug.WriteLine( "{0} killed {1}", attacker.Name, defender.Name );
      }
    }
    else
    {
      // Show the miss message in the Debug log for now
      Debug.WriteLine( "{0} missed {1}", attacker.Name, defender.Name );
    }
 }

 // Helper method which returns the figure at a certain map cell
 public Figure FigureAt( int x, int y )
 {
   if ( IsPlayerAt( x, y ) )
   {
     return _player;
   }
   return EnemyAt( x, y );
 }

 // Helper method for checking if the player is at a map cell
 public bool IsPlayerAt( int x, int y )
 {
   return ( _player.X == x && _player.Y == y );
 }

 // Helper method for getting an enemy at a map cell
 public AggressiveEnemy EnemyAt( int x, int y )
 {
   foreach ( var enemy in _aggressiveEnemies )
   {
     if ( enemy.X == x && enemy.Y == y )
     {
       return enemy;
     }
   }
   return null;
 }

 // Helper method for checking if an enemy is at a map cell
 public bool IsEnemyAt( int x, int y )
 {
   return EnemyAt( x, y ) != null;
 }
}

In the Attack(…) method you’ll notice that we are using Debug.WriteLine() to report the results of combat. I know this isn’t very exciting, but we’ll work on improving that in the next tutorial. Don’t forget to instantiate a new CombatManager class in Game1.cs.

// First add a public member to Global.cs for the CombatManager
public class Global
{
  ...
  public static CombatManager CombatManager;
  ...
}

// In Game1.cs change the LoadContent method to instantiate a new CombatManager
protected override void LoadContent()
{
  ...
  Global.CombatManager = new CombatManager( _player, _aggressiveEnemies );
  ...
}

The code so far

The Player and Enemies can Attack

The last bit that we want to accomplish for this tutorial is to change our Player and AggressiveEnemy classes so that they use our CombatManager and make attacks when appropriate.

// Find the Update method in AggressiveEnemy.cs
public void Update()
{
  ...
  if ( _isAwareOfPlayer )
  {
    _path.CreateFrom( X, Y );
    // Use the CombatManager to check if the player located
    // at the cell we are moving into
    if ( Global.CombatManager.IsPlayerAt( _path.FirstCell.X, _path.FirstCell.Y ) )
    {
      // Make an attack against the player
      Global.CombatManager.Attack( this,
        Global.CombatManager.FigureAt( _path.FirstCell.X, _path.FirstCell.Y ) );
    }
    else
    {
      // Since the player wasn't in the cell, just move into as normal
      X = _path.FirstCell.X;
      Y = _path.FirstCell.Y;
    }
  }
}

// Find the HandleInput method in Player.cs
public bool HandleInput( InputState inputState, IMap map )
{
  if ( inputState.IsLeft( PlayerIndex.One ) )
  {
    int tempX = X - 1;
    if ( map.IsWalkable( tempX, Y ) )
    {
      // Check to see if there is an enemy at the location
      // that the player is attempting to move into
      var enemy = Global.CombatManager.EnemyAt( tempX, Y );
      if ( enemy == null )
      {
        // When there is not an enemy, move as normal
        X = tempX;
      }
      else
      {
        // When there is an enemy in the cell, make an
        // attack against them by using the CombatManager
        Global.CombatManager.Attack( this, enemy );
      }
      return true;
    }
    ...
    // The previous code only handles moving to the left.
    // Update the code blocks to handle each of the other
    // movement directions. Right, Up, and Down
  }
}

The code so far

If you run the game now, the player should be able to attack enemies simply by bumping into them, or trying to enter the same map square as an enemy. If you are not seeing the results of combat, you’ll need to open the Output window and choose Show output from: Debug.

Combat Debug

Combat Debug Output

Complete Tutorial 6 Code

Extra Fun

Thanks again for reading the tutorials. My release schedule has slowed down a bit for the summer. If you have suggestions for things you would like to see, please let me know in the comments. Until next time, here are some things to try for extra fun.

  • Make a health bar UI for the player
  • Show damage in the UI when the player or an enemy is hit
  • Add different types of enemies
  • Refactor some of the classes (should we really have a Global static class?)
  • Fix some of the bugs (yes there are bugs!)
    • Enemies should not be able move onto the same square as another enemy
    • The player doesn’t actually die when he reaches 0 or less health
    • Enemies should find paths around other figures to surround the player
  • Watch Jim Shepard code his Roguelike game Dungeonmans on Twitch on Wednesday and Friday afternoons (1:00 PM EST). His game is on Steam and it is created using XNA and C#
Advertisements

16 thoughts on “Tutorial 6 – Roguelike Combat using RogueSharp and MonoGame

  1. Joshua Madoc (@JoshuaMadoc)

    This tutorial series is getting pretty exciting to watch. Any chance of covering more advanced tutorials in the future, like ranged attacking, spellcasting with targeting based on highlighted tile areas, playing sprite animations for each action, multiple attacks per turn, or maybe having it so that movement is bound to WASD/left-handed equivalent and that manual aiming can be done by holding a button and using the directions to face where you aim?

    Well, I mean… I apologize if that sounds like a lot at once, but I thought I’d ask.

    Reply
    1. Faron Bracy Post author

      Hi Joshua,
      Thank you for reading this blog. I think that ranged and spell attacks could each be a post. Good ideas. Sprite animations could be a whole tutorial series by itself and is probably not something I will cover right away as there are already a lot of great posts online about how to do that. Remember that most old XNA tutorials and code will work just fine in MonoGame so when you are looking make sure you include “XNA” in your searches too. Here is one really good resource – http://www.xnaresources.com/default.asp?page=TUTORIALS – and the expanded sprite engine tutorial there gives some good ideas about animating sprites.

      –Faron

      Reply
    1. Faron Bracy Post author

      Hi Jonathan,
      Thanks for posting. Now that winter is coming up I plan on getting back into working on these tutorials. I’ll add an inventory system to the list of requests for tutorials.

      –Faron

      Reply
      1. Faron Bracy Post author

        Are you looking specifically for more MonoGame tutorials or would a complete roguelike tutorial using RogueSharp and any framework be ok? I’ve been working on making a complete RougeLike game with the pre-release v3 version of RogueSharp and RLNet. You can follow along with the progress here -> https://bitbucket.org/FaronBracy/roguesharprlnetsamples/branch/CompleteTutorial

        Unfortunately it does not use MonoGame so might not be what you are looking for.

  2. lingwraith

    Hi, thank you for the tutorial, but I’m having some weird bugs. When I zoom the camera, the last rendered frame stays there, like it wasn’t deleted.It’s the same when I enter debug mode and exit it, the map still stays explored, yet if I move the fog gets updated, where it should be updated, but where it doesn’t check for fog it stays explored. I’m pretty newbie, so it would be great if you could reply :/

    GIF https://i.gyazo.com/5cce8576282aaca0d1128422cc6036c5.gif
    GIF of zooming https://i.gyazo.com/e7845b28f3be4d29d9ee500c46f98661.gif
    Game1.cs: http://pastebin.com/jhWksuPJ

    There was a point when I was literally going and copy pasting all your code from source control, because I could’t find a solution. So most of the code should be the same as yours, but tell me if you require anything else 🙂 Thank you!

    Reply
    1. Faron Bracy Post author

      Hello,
      Thank you for visiting. Your sprites look a lot better than the goofy ones I tried to draw 🙂

      It looks like your problem can be fixed with a single line. At the start of your draw method you need to call Clear on the GraphicsDevice.
      protected override void Draw( GameTime gameTime )
      {
      GraphicsDevice.Clear( Color.Black );
      // Rest of your code here
      }

      I hope this works for you.
      Have fun making your game!
      –Faron

      Reply
  3. R

    Hello. I really like your tutorials. But I have a question: What is the strategy of creating doors in Monogame ? I saw your door creating strategy in RLNet tutorials, but there are creation of separate rooms and I can’t figure it out my problem. Thank you very much !

    Reply
    1. Faron Bracy Post author

      The map generator used in the MonoGame tutorial is really simple and doesn’t keep track of doors, rooms, or corridors. You can pull the MapGenerator class from the RLNet tutorials and use it in MonoGame.

      https://bitbucket.org/FaronBracy/roguesharprlnetsamples/src/d3339486cb89c3a960a0d36606c652de9486df27/RogueSharpRLNetSamples/Systems/MapGenerator.cs?at=CompleteTutorial&fileviewer=file-view-default

      The only difference will be in how you render it. Instead of going through the map cell by cell and using ASCII symbols for each, you’ll need to draw or acquire some sprites and then render your sprite as shown in the MonoGame tutorial. You’ll need additional sprites for open doors, closed doors, and stairs.

      If you don’t like that approach, there is another good looking map generator that someone showed me this week https://github.com/odedw/karcero

      I have not tried yet myself but it looks nice.

      Reply
      1. R

        Thank you. I also interested, where you have used other algorithms besides Dijkstra algorithm and edge-weighted directed graph for pathfinding ?

    2. Faron Bracy Post author

      I’m sorry but I’m not exactly sure what you mean. If you mean other pathfinding algorithms in general or just the uses of algorithms in this folder https://bitbucket.org/FaronBracy/roguesharp/src/dbbf43c38a84cce5a4586749e2f63a667a7e00a4/RogueSharp/Algorithms/?at=master

      As far as other pathfinding algorithms go, one of the most common is A*. It can be implemented very similar to Dijkstra (or rather Dijkstra can be viewed as a special case of A* where the heuristic is 0). A* is faster for finding a single path between a source and destination, but I have not included it in RogueSharp yet.

      Speaking of the other Algorithms included with RogueSharp, UnionFind is useful for a lot of things. I’ve used it during dungeon generation to make sure that all of the dungeon areas (rooms and corridors) are connected together. You can call Union() when 2 areas are connected. Continue to do that for all connected areas. At the end your count should be 1 if every room and corridor is accessible

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s