Author Archives: Faron Bracy

Guest Blog Post – RogueSharp Tutorial Series Ported to Unity

Introduction

I’m excited that one of the visitors to the blog took it upon himself to port the code from the RogueSharp tutorial series to Unity. I asked if he would be interested in writing a guest blog post and he agreed! A huge thank you to Brian for all the hard work with the port and for the  information posted below.

– Faron

About the Author

First off, thank you Faron Bracy for developing the RogueSharp library and all the time you’ve spent developing the library and writing tutorials on it!

I have been developing with Unity for some time now and have been looking for a C# Rogue or 2D RPG library. There a couple Rogue/RPG libraries on the Unity asset store but the libraries seem to be too integrated or reliant on Unity. I was looking for a pure C# implementation and something I could work into Unity but change graphics engine later if I wanted. Thus RogueSharp.

About the Port to Unity

I developed a port of Faron’s RogueSharp v3 tutorial series with Unity except I ended up using RogueSharp V4! I originally went through the entire tutorial series using RLNet Console, which gave me a comfortable feel for the library and how I wanted to approach integrating with Unity. With the Unity integration and some of my own personal preferences, I’ve made a numbers changes to the original tutorial for the Unity port. Some of notable changes:

  1. Uses Unity (version 2018.2.1f1) instead of RLNet Console, so no more RLColor or RLConsole calls;
  2. The code has been reorganized mostly in a MVC pattern. There are no dependencies in the Model and Controller logic against Unity. Only View logic has any reliance on Unity. This could make ports on other consoles/backends easier (maybe?);
  3. Uses diagonal movement in RogueSharp v4 including monster pathfinding;
  4. Most static references removed, passes data by reference. Personal preference, I’m not big on lots of static functions;
  5. Uses RogueSharp v4, not v3 as in the original tutorial series. Only caused a few minor changes to the tutorial code;
  6. Object pooling used from Catlike coding,https://catlikecoding.com/unity/tutorials/ for Unity GameObjects that represent cells on the console. This was necessary as the height/width sizes of the console cause too many GameObects to be created (thousands). This caused significant slowdowns in the FPS, so I needed an optimization early in the process. As a result, the View logic will only display tiles/cells that are visible in the Camera and dynamically change the viewable tiles/cells as the player moves. This allows for arbitrarily large maps;
  7. There are few odds and ends added to the code that are placeholders for a future implementation that allows switching between ASCII characters and graphical tiles.

The port can be found at: https://github.com/Olivexe/RogueSharpTutorialUnityPort. Along with the project source code, I’ve put in a Unity Package file, named RogueSharpTutorialPort.unitypackage. You should be able to import the package file into a new Unity Project, click the sample scene, and hit play. It should play exactly as the tutorial. There is now a second branch in my port that I am slowly porting the remainder of the complete RogueSharp tutorial over to Unity.

Thank you,

Brian “Olivexe” Pleffner

Advertisements

RogueSharp v4.1.0 Released

Version 4.1.0 of RogueSharp was released

Announcements:

New features:

  • Optimized cave map generator (Thanks to James Neal)
  • DijkstraShortestPath.FindPath method which terminates upon finding destination for improved speed (Thanks to flend for this update)
  • Added AppVeyor continuous builds (Thanks to Glenn Hoeppner)
  • New constructors for GoalMap and PathFinder to allow for paths which include diagonal movement
  • Map.GetCellsInCircle and Map.GetBorderCellsInCircle methods use midpoint circle algorithm to get cells within a radius
  • Multiple new “Try” methods which work like their normal counterparts except they return null instead of throwing exceptions
  • Path.TryStepForward and Path.TryStepBackward which will return null instead of throwing a NoMoreStepsException
  • PathFinder.TryFindShortestPath will return null instead of throwing a PathNotFoundException
  • GoalMap.TryFindPath will return null instead of throwing a PathNotFoundException

Breaking changes:

  • Bug fix (4.1.0) – selecting border Cells along edge of map no longer selects center Cell
  • Point and Rectangle classes are now structs (Thanks to James Neal)
  • Updated all the appropriate references to Cell with ICell (Thanks to Courtney Strachan)
  • Map.ComputeFov and Map.AppendFov both return a ReadonlyCollection<ICell> for the cells in the field-of-view instead of returning void.
  • The Path returned from PathFinder.ShortestPath now includes the source cell in the returned Path. This behavior is consistent with how the GoalMap pathfinder works
  • Map.GetCellsInArea was renamed to Map.GetCellsInSquare
  • Map.GetBorderCellsInArea was renamed to Map.GetBorderCellsInSquare
  • Map.GetCellsInRadius was renamed to Map.GetCellsInDiamond
  • Map.GetBorderCellsInRadius was renamed to Map.GetBorderCellsInDiamond

RogueSharp 4.0 Pre-Release

Version 4.0.0-pre of RogueSharp was just released

In order to obtain pre-release versions via Nuget make sure to choose “Include prerelease” packages from the Nuget Package Manager.

PrereleaseNuget

Pre-release Nuget Package

RogueSharp 4.0.0-pre NuGet Package
RogueSharp 4.0.0-pre Source Code

Thank you to numerous contributors for this release including James Neal, flend, Glenn Hoeppner, and Courtney Strachan. Any feedback regarding this pre-release version is appreciated!

New features:

  • Optimized cave map generator (Thanks to James Neal)
  • DijkstraShortestPath.FindPath method which terminates upon finding destination for improved speed (Thanks to flend for this update)
  • Added AppVeyor continuous builds (Thanks to Glenn Hoeppner)
  • New constructors for GoalMap and PathFinder to allow for paths which include diagonal movement
  • Map.GetCellsInCircle and Map.GetBorderCellsInCircle methods use midpoint circle algorithm to get cells within a radius
  • Multiple new “Try” methods which work like their normal counterparts except they return null instead of throwing exceptions
  • Path.TryStepForward and Path.TryStepBackward which will return null instead of throwing a NoMoreStepsException
  • PathFinder.TryFindShortestPath will return null instead of throwing a PathNotFoundException
  • GoalMap.TryFindPath will return null instead of throwing a PathNotFoundException

Breaking changes:

  • Point and Rectangle classes are now structs (Thanks to James Neal)
  • Updated all the appropriate references to Cell with ICell (Thanks to Courtney Strachan)
  • Map.ComputeFov and Map.AppendFov both return a ReadonlyCollection<ICell> for the cells in the field-of-view instead of returning void.
  • The Path returned from PathFinder.ShortestPath now includes the source cell in the returned Path. This behavior is consistent with how the GoalMap pathfinder works
  • Map.GetCellsInArea was renamed to Map.GetCellsInSquare
  • Map.GetBorderCellsInArea was renamed to Map.GetBorderCellsInSquare
  • Map.GetCellsInRadius was renamed to Map.GetCellsInDiamond
  • Map.GetBorderCellsInRadius was renamed to Map.GetBorderCellsInDiamond

Pathfinder Diagonal Path Example:

By using the new constructor on the PathFinder class and providing a second argument which is the cost of diagonal movement you can now return paths that consider diagonals.

public Path FindPath( ICell source, ICell destination, IMap map )
{
  // 1.41 is the cost of diagonal movment compared to horizontal or vertical of 1
  var pathFinder = new PathFinder( map, 1.41 );

  // TryFindShortestPath is a new method that will return null
  // instead of throwing PathNotFoundException if there isn't a path.
  return pathFinder.TryFindShortestPath( source, destination );
}
DiagonalPathfinder

Diagonal Pathfinder

GoalMap Diagonal Path Example:

The GoalMap class also received a new constructor which will allow it to use diagonals.

/// Constructs a new instance of a GoalMap for the specified Map 
/// that will consider diagonal movements to be valid if allowDiagonalMovement is set to true.
/// "map" The Map that this GoalMap will be created for
/// "allowDiagonalMovement" True if diagonal movements are allowed. False otherwise
public GoalMap( IMap map, bool allowDiagonalMovement )
DiagonalGoalMap

Diagonal Goal Map

Map.GetCellsInCircle Example:

RogueSharp has always had a lot of different Cell selection methods as seen in this old post.

Cell Selection

Previous Cell Selection

What it has been missing is a good method for getting cells in a Circle. We now have two new methods which will help with this.

/// Get an IEnumerable of Cells in a circle around the center Cell up 
/// to the specified radius using Bresenham's midpoint circle algorithm
/// "xCenter" X location of the center Cell with 0 as the farthest left
/// "yCenter" Y location of the center Cell with 0 as the top
/// "radius" The number of Cells to get in a radius from the center Cell
public IEnumerable<ICell> GetCellsInCircle( int xCenter, int yCenter, int radius )


/// Get an IEnumerable of outermost border Cells in a circle around the center 
/// Cell up to the specified radius using Bresenham's midpoint circle algorithm
/// "xCenter"X location of the center Cell with 0 as the farthest left
/// "yCenter"Y location of the center Cell with 0 as the top
/// "radius"The number of Cells to get in a radius from the center Cell
public IEnumerable<ICell> GetBorderCellsInCircle( int xCenter, int yCenter, int radius )
CircleSelection

Circle Selection

Using the new circle selection code it is possible to create poor implementation of circular field-of-view even though RogueSharp doesn’t have native support for it. Here is an example of some code that will do it.

private static IEnumerable<ICell> GetFieldOfView( int x, int y, IMap map )
{
  List<ICell> circleFov = new List<ICell>();
  var fieldOfView = new FieldOfView( map );
  var cellsInFov = fieldOfView.ComputeFov( x, y, (int) ( _selectionSize * 1.5 ), true );
  var circle = map.GetCellsInCircle( x, y, _selectionSize ).ToList();
  foreach ( ICell cell in cellsInFov )
  {
    if ( circle.Contains( cell ) )
    {
      circleFov.Add( cell );
    }
  }
  return circleFov;
}

It will create field-of-view that looks like this:

CircularFieldOfView

Circular Field-of-View

RogueSharp V3 Tutorial – Stairs

Next Tutorial Post – Not Available Yet
Previous Tutorial Post – Doors

Goal

The purpose of this tutorial is start placing stairs in our dungeon. We want to be able to proceed down stairs to get into deeper and more difficult levels.

  • Stairs can either go up or down
  • The symbol for stairs going down will be a greater than sign “>”
  • The symbol for stairs going up will be a less than sign “<“
  • Each dungeon level will have one up and one down staircase
  • Stairs going down can be descended by the player by pressing the ‘>’ or ‘.’ key
  • Once a player has gone down to the next level they cannot go back
  • Stairs going up are only used to indicate where the player came from
  • When a player goes down stairs a brand new level is generated

Creating the Stairs Class

To begin we need a new class to represent the stairs as we outlined in our goals above. The stairs class should inherit from IDrawable because we will want to draw it on the map console. Create a new file named Stairs.cs in the Core folder and place the following code in it.

public class Stairs : IDrawable
{
  public RLColor Color
  {
    get; set;
  }
  public char Symbol
  {
    get; set;
  }
  public int X
  {
    get; set;
  }
  public int Y
  {
    get; set;
  }
  public bool IsUp
  {
    get; set;
  }

  public void Draw( RLConsole console, IMap map )
  {
    if ( !map.GetCell( X, Y ).IsExplored )
    {
      return;
    }

    Symbol = IsUp ? '<' : '>';

    if ( map.IsInFov( X, Y ) )
    {
      Color = Colors.Player;
    }
    else
    {
      Color = Colors.Floor;
    }

    console.Set( X, Y, Color, null, Symbol );
  }
}

Updating DungeonMap Class

Now that our Stairs class is created, we need to update DungeonMap.cs with a few changes to be able to use the Stairs. At the top of DungeonMap.cs add the following two properties for our StairsUp and StairsDown next to the existing Rooms and Doors properties.

public Stairs StairsUp { get; set; }
public Stairs StairsDown { get; set; }

Next update the DungeonMap constructor to call Clear() on the SchedulingSystem when a new DungeonMap is constructed. We do this because when we make a new level by going down stairs we want to make sure that all of the monsters from the previous level are removed from the schedule and do not continue to try to act.

public DungeonMap()
{
  Game.SchedulingSystem.Clear();

  // Previous code omitted...
}

Now we need a new method which will check to see if the player is standing on the stairs going down. Create a method called CanMoveDownToNextLevel() with the following code.

public bool CanMoveDownToNextLevel()
{
  Player player = Game.Player;
  return StairsDown.X == player.X && StairsDown.Y == player.Y;
}

In the Doors tutorial last time we forgot to update the Draw() method and had to have a blog reader point out the mistake. We’ll try not to make the same mistake this time. Update the Draw() method and add the following lines of code to draw the stairs.

// Add the following code after we finish drawing doors.
StairsUp.Draw( mapConsole, this );
StairsDown.Draw( mapConsole, this );

Updating the MapGenerator Class

The next class we need to update is MapGenerator. We need to make sure that when we generate new maps, stairs are created along with the rest of the dungeon features. Open MapGenerator.cs and create a new private method named CreateStairs().

private void CreateStairs()
{
  _map.StairsUp = new Stairs
  {
    X = _map.Rooms.First().Center.X + 1,
    Y = _map.Rooms.First().Center.Y,
    IsUp = true
  };
  _map.StairsDown = new Stairs
  {
    X = _map.Rooms.Last().Center.X,
    Y = _map.Rooms.Last().Center.Y,
    IsUp = false
  };
}

We are not doing anything too fancy to generate the stairs. We are creating the stairs up in the center of the first room that was generated. This is the same room that the player starts in and the player is also in the center of the room, so we’ll offset the X coordinate by 1 to put the stairs next to the player. The last room we generated gets stairs going down and again we place them in the center of the room.

Make sure to call the CreateStairs() method from the existing CreateMap() method right before calling PlacePlayer();

public DungeonMap CreateMap()
{
  // Previous code omitted...

  // Call right before calling PlacePlayer();
  CreateStairs();

  // Previous code
  PlacePlayer();
}

Also we want to change the signature of the MapGenerator constructor and add an additional integer parameter called mapLevel.

public MapGenerator( int width, int height, int maxRooms, int roomMaxSize, int roomMinSize, int mapLevel )
{
  // Keep all existing code in the constructor
}

If you have any sort of static analysis on like FxCop it will complain about having an unused parameter in the method. We really shouldn’t add it until we are prepared to use it, but rest assured we will use it very soon.

Updating the Game Class

Open up Game.cs which will be the final class that we need to update. Start by adding a new private static int member variable in with the rest of the member variables at the top of the class.

private static int _mapLevel = 1;

Next change the line was setting the console title in the Main() method. Also add the _mapLevel parameter to line where we instantiate a new MapGenerator.

// Old code was...
// string consoleTitle = $"RougeSharp V3 Tutorial - Level 1 - Seed {seed}";

// New code is
string consoleTitle = $"RougeSharp V3 Tutorial - Level {_mapLevel} - Seed {seed}";


// Old code was...
// MapGenerator mapGenerator = new MapGenerator( _mapWidth, _mapHeight, 20, 13, 7 );

// New code is
MapGenerator mapGenerator = new MapGenerator( _mapWidth, _mapHeight, 20, 13, 7, _mapLevel );

Finally in the OnRootConsoleUpdate(…) method where we are checking to see which key was pressed ad the following else if onto the end of the if block.

else if ( keyPress.Key == RLKey.Period )
{
  if ( DungeonMap.CanMoveDownToNextLevel() )
  {
    MapGenerator mapGenerator = new MapGenerator( _mapWidth, _mapHeight, 20, 13, 7, ++_mapLevel );
    DungeonMap = mapGenerator.CreateMap();
    MessageLog = new MessageLog();
    CommandSystem = new CommandSystem();
    _rootConsole.Title = $"RougeSharp RLNet Tutorial - Level {_mapLevel}";
    didPlayerAct = true;
  }
}

This last bit of code just checks for the “>” or “.” key being pressed. It then calls into DungeonMap.CanMoveDownToNextLevel() which will return true if the Player is standing on a stairway leading down. We then generate a new map and increment the _mapLevel.

If you run the game now you should be able to explore until you find stairs and descend deeper into the dungeon.

stairs

Going down stairs

Closing Thoughts

Although we got our stairs working we are still missing some important things. Currently levels don’t get any more difficult the deeper we go.

As always the code for the tutorial series so far can be found on Bitbucket:
https://bitbucket.org/FaronBracy/roguesharpv3tutorial/commits/tag/16Stairs

Bored waiting for the next tutorial post? The completed project is already available on Bitbucket.

RogueSharp V3 Tutorial – Doors

Next Tutorial Post – Stairs
Previous Tutorial Post – Monster Behaviors

Goal

The purpose of this tutorial is to start placing doors in our dungeon. We want the doors to start out simple but be able to be expanded upon in the future.

  • Doors can have 2 states: open or closed
  • All doors start as closed
  • A closed door blocks field-of-view
  • A door can be opened by any actor (Player or Monster)
  • Once a door is opened it cannot be closed again
  • An actor entering the same cell as a door will open it automatically
  • The symbol for a closed door will be a plus sign “+”
  • The symbol for an open door will be a minus sign “-“

Setting Door Colors

We already set up the swatch of colors to use in our game but we never specified which colors doors should be. Let’s do that now. Open Colors.cs and add the following code to pick colors from the Swatch for different parts of the door.

public static RLColor DoorBackground = Swatch.ComplimentDarkest;
public static RLColor Door = Swatch.ComplimentLighter;
public static RLColor DoorBackgroundFov = Swatch.ComplimentDarker;
public static RLColor DoorFov = Swatch.ComplimentLightest;

Creating the Door Class

Next we need a new class to represent all of the properties of a door that we outlined in our goals at the beginning. The door class should inherit from IDrawable because we will want to draw it on the map console. Create a new file named Door.cs in the Core folder and place the following code in it.

public class Door : IDrawable
{
  public Door()
  {
    Symbol = '+';
    Color = Colors.Door;
    BackgroundColor = Colors.DoorBackground;
  }
  public bool IsOpen { get; set; }

  public RLColor Color { get; set; }
  public RLColor BackgroundColor { get; set; }
  public char Symbol { get; set; }
  public int X { get; set; }
  public int Y { get; set; }

  public void Draw( RLConsole console, IMap map )
  {
    if ( !map.GetCell( X, Y ).IsExplored )
    {
      return;
    }

    Symbol = IsOpen ? '-' : '+';
    if ( map.IsInFov( X, Y ) )
    {
      Color = Colors.DoorFov;
      BackgroundColor = Colors.DoorBackgroundFov;
    }
    else
    {
      Color = Colors.Door;
      BackgroundColor = Colors.DoorBackground;
    }

    console.Set( X, Y, Color, BackgroundColor, Symbol );
  }
}

Notice that we use the colors and symbols for the door that we determined earlier.

Updating the DungeonMap Class

Before we can start placing doors in our dungeon it’s important to add a few helper methods to our DungeonMap class for working with doors. Open DungeonMap.cs and add the following lines of code.

First add a new public property to the class at the top where we already have a list of rooms.

public List<Door> Doors { get; set; }

Be sure to initialize this list in the constructor

public DungeonMap()
{
  // Previous constructor code omitted...

  Doors = new List<Door>();
}

Next we need to add two new methods to the class. GetDoor(…) and OpenDoor(…)

// Return the door at the x,y position or null if one is not found.
public Door GetDoor( int x, int y )
{
  return Doors.SingleOrDefault( d => d.X == x && d.Y == y );
}

// The actor opens the door located at the x,y position
private void OpenDoor( Actor actor, int x, int y )
{
  Door door = GetDoor( x, y );
  if ( door != null && !door.IsOpen )
  {
    door.IsOpen = true;
    var cell = GetCell( x, y );
    // Once the door is opened it should be marked as transparent and no longer block field-of-view
    SetCellProperties( x, y, true, cell.IsWalkable, cell.IsExplored );

    Game.MessageLog.Add( $"{actor.Name} opened a door" );
  }
}

Last we need to update SetActorPosition(…) and call a the new method OpenDoor(…) which we just made. Call this immediately after SetIsWalkable(…).

// Returns true when able to place the Actor on the cell or false otherwise
public bool SetActorPosition( Actor actor, int x, int y )
{
  // Previous code omitted...

  // Try to open a door if one exists here
  OpenDoor( actor, x, y );
}

Edit 2/25/2017 – Thanks to rmcrackan for pointing out that I forgot to show updating the Draw(…) method to have a foreach loop for drawing each of the doors.

foreach ( Door door in Doors )
{
  door.Draw( mapConsole, this );
}

Door Placement Strategy

Now that we are reasonably confident that we can draw doors and we have a few helper methods for working with them, how do we know where to put them? Think about where doors are placed in regard to rooms. They should be on an outer wall. So we should start by getting all of the cells along the boundaries of our rooms.

doorstrategy1

Check room boundary

Once we have all of those cells we should look for any open floors that would be a good candidate for a door. And how do we know if a door should go there or not? We look at the floor’s neighboring cells to see if it fits.

doorstrategy2

Check neighboring cells

A good Cell for a door placement is one that has two walls across from each other and two floors as opposing neighbors.

Updating the MapGenerator Class

Lets take the strategy that we just outlined for creating door and actually write the code. Open MapGenerator.cs and add the following new methods.

private void CreateDoors( Rectangle room )
{
  // The the boundries of the room
  int xMin = room.Left;
  int xMax = room.Right;
  int yMin = room.Top;
  int yMax = room.Bottom;

  // Put the rooms border cells into a list
  List<Cell> borderCells = _map.GetCellsAlongLine( xMin, yMin, xMax, yMin ).ToList();
  borderCells.AddRange( _map.GetCellsAlongLine( xMin, yMin, xMin, yMax ) );
  borderCells.AddRange( _map.GetCellsAlongLine( xMin, yMax, xMax, yMax ) );
  borderCells.AddRange( _map.GetCellsAlongLine( xMax, yMin, xMax, yMax ) );

  // Go through each of the rooms border cells and look for locations to place doors.
  foreach ( Cell cell in borderCells )
  {
    if ( IsPotentialDoor( cell ) )
    {
      // A door must block field-of-view when it is closed.
      _map.SetCellProperties( cell.X, cell.Y, false, true );
      _map.Doors.Add( new Door
      {
        X = cell.X,
        Y = cell.Y,
        IsOpen = false
      } );
    }
  }
}

// Checks to see if a cell is a good candidate for placement of a door
private bool IsPotentialDoor( Cell cell )
{
  // If the cell is not walkable
  // then it is a wall and not a good place for a door
  if ( !cell.IsWalkable )
  {
    return false;
  }

  // Store references to all of the neighboring cells 
  Cell right = _map.GetCell( cell.X + 1, cell.Y );
  Cell left = _map.GetCell( cell.X - 1, cell.Y );
  Cell top = _map.GetCell( cell.X, cell.Y - 1 );
  Cell bottom = _map.GetCell( cell.X, cell.Y + 1 );

  // Make sure there is not already a door here
  if ( _map.GetDoor( cell.X, cell.Y ) != null ||
      _map.GetDoor( right.X, right.Y ) != null ||
      _map.GetDoor( left.X, left.Y ) != null ||
      _map.GetDoor( top.X, top.Y ) != null ||
      _map.GetDoor( bottom.X, bottom.Y ) != null )
  {
    return false;
  }

  // This is a good place for a door on the left or right side of the room
  if ( right.IsWalkable && left.IsWalkable && !top.IsWalkable && !bottom.IsWalkable )
  {
    return true;
  }

  // This is a good place for a door on the top or bottom of the room
  if ( !right.IsWalkable && !left.IsWalkable && top.IsWalkable && bottom.IsWalkable )
  {
    return true;
  }
  return false;
}

That’s quite a bit of code but I hope that the comments help to understand what it is accomplishing. Now that we have the methods in place, we need to remember to call them. In the CreateMap() method we have a foreach where we call CreateRoom() on each room in the map. We also need to put a call for CreateDoors() in there.

// Iterate through each room that we wanted placed
// and dig out the room and create doors for it.
foreach ( Rectangle room in _map.Rooms )
{
  CreateRoom( room );
  CreateDoors( room );
}

If we run the program now we should see our doors in place and working as expected!

openingdoors

Opening Doors

Final Thoughts

The 7 Day Roguelike Challenge starts this year on March 12th. This year will be the 13th challenge. It’s a fun event and I urge you to participate. If you don’t like RogueSharp or C# there are plenty of other libraries out there for other languages. You could also start from scratch and not use any existing library.

7DRL Annoucement- http://7drl.org/2017/01/18/this-years-challenge-4-12-march-2017/

As always the code for the tutorial series so far can be found on Bitbucket:
https://bitbucket.org/FaronBracy/roguesharpv3tutorial/commits/tag/15Doors

Bored waiting for the next tutorial post? The completed project is already available on Bitbucket.

RogueSharp V3 Tutorial – Monster Behaviors

Next Tutorial Post – Doors
Previous Tutorial Post – Scheduling System

Goal

There is going to be a lot of code in this tutorial. We have to start using the scheduling system we built last time. We’ll also be laying the foundation for creating monster behaviors. Our first behavior will be a simple move and attack, but we can expand on this in the future to create lots of interesting monsters.

simplebehaviors

Simple Behaviors

Defining a Behavior Interface

First we need to define the interface that all of our future Behaviors will use. Create a new file named IBehavior.cs in the Interfaces folder. Add the following code:

public interface IBehavior
{
  bool Act( Monster monster, CommandSystem commandSystem );
}

We’re going to keep it simple and just have a simple Act(…) method that will take the Monster performing the action and the CommandSystem as parameters.

Creating a Move and Attack Behavior

Our first concrete behavior that will implement our IBehavior interface will be StandardMoveAndAttack.

Briefly lets go over what the StandardMoveAndAttack behavior will do.

  • A monster should perform a standard melee attack on the player if the player is adjacent to the monster.
  • If the player is not within attack range, the monster should move closer to the player via the shortest available path.
  • Only monsters that are aware of the player should chase him.
  • Once the monster is alerted and begins the chase, if the player evades him and remains out of visual range for a period of time the monster should stop pursuit.
  • If the monster can see the player but does not have a valid path (possibly because of being blocked by other monsters) it should wait a turn.

Because we want to have many additional behaviors in the future we should make a new folder named Behaviors and then create the file StandardMoveAndAttack.cs in that folder. Add the following code:

public class StandardMoveAndAttack : IBehavior
{
  public bool Act( Monster monster, CommandSystem commandSystem )
  {
    DungeonMap dungeonMap = Game.DungeonMap;
    Player player = Game.Player;
    FieldOfView monsterFov = new FieldOfView( dungeonMap );

    // If the monster has not been alerted, compute a field-of-view 
    // Use the monster's Awareness value for the distance in the FoV check
    // If the player is in the monster's FoV then alert it
    // Add a message to the MessageLog regarding this alerted status
    if ( !monster.TurnsAlerted.HasValue )
    {
      monsterFov.ComputeFov( monster.X, monster.Y, monster.Awareness, true );
      if ( monsterFov.IsInFov( player.X, player.Y ) )
      {
        Game.MessageLog.Add( $"{monster.Name} is eager to fight {player.Name}" );
        monster.TurnsAlerted = 1;
      }
    }
    
    if ( monster.TurnsAlerted.HasValue )
    {
      // Before we find a path, make sure to make the monster and player Cells walkable
      dungeonMap.SetIsWalkable( monster.X, monster.Y, true );
      dungeonMap.SetIsWalkable( player.X, player.Y, true );

      PathFinder pathFinder = new PathFinder( dungeonMap );
      Path path = null;

      try
      {
        path = pathFinder.ShortestPath( 
        dungeonMap.GetCell( monster.X, monster.Y ), 
        dungeonMap.GetCell( player.X, player.Y ) );
      }
      catch ( PathNotFoundException )
      {
        // The monster can see the player, but cannot find a path to him
        // This could be due to other monsters blocking the way
        // Add a message to the message log that the monster is waiting
        Game.MessageLog.Add( $"{monster.Name} waits for a turn" );
      }

      // Don't forget to set the walkable status back to false
      dungeonMap.SetIsWalkable( monster.X, monster.Y, false );
      dungeonMap.SetIsWalkable( player.X, player.Y, false );

      // In the case that there was a path, tell the CommandSystem to move the monster
      if ( path != null )
      {
        try
        {
          // TODO: This should be path.StepForward() but there is a bug in RogueSharp V3
          // The bug is that a Path returned from a PathFinder does not include the source Cell
          commandSystem.MoveMonster( monster, path.Steps.First() );
        }
        catch ( NoMoreStepsException )
        {
          Game.MessageLog.Add( $"{monster.Name} growls in frustration" );
        }
      }

      monster.TurnsAlerted++;

      // Lose alerted status every 15 turns. 
      // As long as the player is still in FoV the monster will stay alert
      // Otherwise the monster will quit chasing the player.
      if ( monster.TurnsAlerted > 15 )
      {
        monster.TurnsAlerted = null;
      }
    }
    return true;
  }
}

Be aware that if you try to Build right now there will be errors. We never added the TurnsAlerted property to our Monster class. Additionally we are missing the MoveMonster method on our CommandSystem.

Note: You may also notice the TODO comment. I discovered a bug in V3 RogueSharp and have a fix for it but for now this is a workaround.

Monster Class Updates

We should make sure we get back to a state where the project will build as quickly as possible. With that in mind open Monster.cs and add a new TurnsAlerted property as well as a PerformAction method that we’ll use later.

public int? TurnsAlerted { get; set; }

public virtual void PerformAction( CommandSystem commandSystem )
{
  var behavior = new StandardMoveAndAttack();
  behavior.Act( this, commandSystem );
}

Did you notice the question mark on the int? property? That’s shorthand for a Nullable<int> type. Since integers are value types they cannot normally be null, however we want a null integer to represent that the monster has not been alerted.

Notice that the PerformAction method is virtual also. Since this is on our Monster base class, we’ll give any monster the StandardMoveAndAttack behavior by default. We can override this in any of our Monster sub-classes to get more interesting behaviors.

Actor Class Updates

At this point we’re still not building, but we are getting closer. Open Actor.cs and make sure that the Actor class implements IScheduleable so that we can add them to our scheduling system.

public class Actor : IActor, IDrawable, IScheduleable
{
  // ... Previous Actor code omitted

  // IScheduleable
  public int Time
  {
    get
    {
      return Speed;
    }
  }
}

CommandSystem Class Updates

We should get our CommandSystem updated now so we can finally build again. Open CommandSystem.cs and add the IsPlayerTurn property as well as the ActivateMonsters() and MoveMonster(…) methods.

Update 1/31/2017 – Added the EndPlayerTurn() method which was accidentally omitted from the code below. Thank you to Zacharry Field for bringing this to my attention.

public bool IsPlayerTurn { get; set; }

public void EndPlayerTurn()
{
  IsPlayerTurn = false;
}

public void ActivateMonsters()
{
  IScheduleable scheduleable = Game.SchedulingSystem.Get();
  if ( scheduleable is Player )
  {
    IsPlayerTurn = true;
    Game.SchedulingSystem.Add( Game.Player );
  }
  else
  {
    Monster monster = scheduleable as Monster;

    if ( monster != null )
    {
      monster.PerformAction( this );
      Game.SchedulingSystem.Add( monster );
    }

    ActivateMonsters();
  }
}

public void MoveMonster( Monster monster, Cell cell )
{
  if ( !Game.DungeonMap.SetActorPosition( monster, cell.X, cell.Y ) )
  {
    if ( Game.Player.X == cell.X && Game.Player.Y == cell.Y )
    {
      Attack( monster, Game.Player );
    }
  }
}

The ActivateMonsters() method is intended to be called after the Player takes a turn. This will proceed to get the next scheduled Actor from the SchedulingSystem. If this happens to be the Player again, we’ll wait for the Player to make a move. Otherwise we’ll have the Monster perform an action and then call ActivateMonsters() again recursively. This will keep having Monsters perform their actions until it is once again the Player’s turn.

Add Player and Monsters to Scheduling System

Open DungeonMap.cs and update the AddPlayer(…), AddMonster(…), and RemoveMonster(…) methods to use the SchedulingSystem. Just add the calls at the end of each method.

public void AddPlayer( Player player )
{
  // ...previous code omitted
  Game.SchedulingSystem.Add( player );
}

public void AddMonster( Monster monster )
{
  // ...previous code omitted
  Game.SchedulingSystem.Add( monster );
}

public void RemoveMonster( Monster monster )
{
  // ...previous code omitted
  Game.SchedulingSystem.Remove( monster );
}

Updating the Game Class

We’re almost done. The rest of the updates will be in Game.cs so open up the file and add a new public property.

public static SchedulingSystem SchedulingSystem { get; private set; }

Make sure to instantiate a new SchedulingSystem in the Main() method.

SchedulingSystem = new SchedulingSystem();

The last set of updates will be made in the OnRootConsoleUpdate(…) method. We need to ensure that it is the player’s turn before handling key presses. We also need to activate monsters after the player has taken their turn.

private static void OnRootConsoleUpdate( object sender, UpdateEventArgs e )
{
  bool didPlayerAct = false;
  RLKeyPress keyPress = _rootConsole.Keyboard.GetKeyPress();

  if ( CommandSystem.IsPlayerTurn )
  {
    if ( keyPress != null )
    {
      if ( keyPress.Key == RLKey.Up )
      {
        didPlayerAct = CommandSystem.MovePlayer( Direction.Up );
      }
      else if ( keyPress.Key == RLKey.Down )
      {
        didPlayerAct = CommandSystem.MovePlayer( Direction.Down );
      }
      else if ( keyPress.Key == RLKey.Left )
      {
        didPlayerAct = CommandSystem.MovePlayer( Direction.Left );
      }
      else if ( keyPress.Key == RLKey.Right )
      {
        didPlayerAct = CommandSystem.MovePlayer( Direction.Right );
      }
      else if ( keyPress.Key == RLKey.Escape )
      {
        _rootConsole.Close();
      }
    }

    if ( didPlayerAct )
    {
      _renderRequired = true;
      CommandSystem.EndPlayerTurn();
    }
  }
  else
  {
    CommandSystem.ActivateMonsters();
    _renderRequired = true;
  }
}

Everything should build now and if you run the game you’ll see that monsters will now chase the player around and attack. The scheduling system that we implemented last time is all hooked up now too.

Final Thoughts

This was a long post with a lot of code but it should pave the way for adding more monsters and more interesting behaviors in the future.

As always the code for the tutorial series so far can be found on Bitbucket:
https://bitbucket.org/FaronBracy/roguesharpv3tutorial/commits/tag/14MonsterBehaviors

Bored waiting for the next tutorial post? The completed project is already available on Bitbucket.

RogueSharp V3 Tutorial – Scheduling System

Next Tutorial Post – Monster Behaviors
Previous Tutorial Post – Simple Combat

Goal

During this tutorial we will create a Scheduling System. Some roguelikes also refer to these as time systems. You can find out how lots of developers create their time systems by reading the Roguelike Dev Subreddit FAQ Friday #41.

Scheduling System Details

Most roguelikes including this tutorial are turn-based, meaning each Actor acts on their turn. The simplest way to achieve this is to just have a giant queue structure and have everyone act in order one-by-one. When the actor is removed from the queue and takes their turn then they get re-added to the back of the queue. The player goes, and then each monster takes a turn, then the player takes their turn again and so on.

Unfortunately if we did something that simple it wouldn’t be very interesting. All Actors would have the same speed. Effectively this would mean that you could never run away from monsters or catch a fleeing monster.

To get around this issue we will give each Actor a Speed statistic which will represent how often they can take actions. You may even remember in a previous tutorial where we defined stats we already allocated a Speed stat. We just never got around to using it, but we are about to fix that!

The concept behind our scheduling system is that it will keep track of the time or number of turns that have passed as well as the current time. The Speed of the actor will determine when they get inserted into the timeline. Note that in this case having a lower speed is better.

SchedulingSystem

Example Schedule

In the Example Schedule above notice that during the first 12 turns the player with a speed of 4 will have acted 3 times. The goblin with a speed of 3 will have acted 4 times. And the Ooze with a speed of 6 will have only acted twice. This means that a player would be able to easily outrun an ooze, but not the goblin. The goblin moves twice as fast as the ooze.

Data Structure

To represent this data structure in code we will use a SortedDictionary<int, List<IScheduleable>>. This may look a little odd at first, but all it really means is that Key in the dictionary is an integer that represents the time. The Value of each dictionary entry is a List<IScheduleable>. But what exactly is an IScheduleable? It’s a custom interface that we’ll make and it will just have a single property Time. What we are saying is that we can put anything on the schedule as long as it has a Time which represents how many turns pass until its time comes up again on the schedule. Add the IScheduleable.cs file to the Interfaces folder and add the following code.

public interface IScheduleable
{
  int Time { get; }
}

So now that we see what an IScheduleable is, the next question is why do we have a List<> of them for each key in the dictionary? The answer goes back to our to our Example Schedule. If you look at it closer you’ll notice that more than one Actor can act on the same time interval. At Time 6 both the Ooze and Goblin act. At time 12 all three act.

Let’s go ahead and create SchedulingSystem.cs in the Systems folder.

public class SchedulingSystem
{
  private int _time;
  private readonly SortedDictionary<int, List<IScheduleable>> _scheduleables;

  public SchedulingSystem()
  {
    _time = 0;
    _scheduleables = new SortedDictionary<int, List<IScheduleable>>();
  }

  // Add a new object to the schedule 
  // Place it at the current time plus the object's Time property.
  public void Add( IScheduleable scheduleable )
  {
    int key = _time + scheduleable.Time;
    if ( !_scheduleables.ContainsKey( key ) )
    {
      _scheduleables.Add( key, new List<IScheduleable>() );
    }
    _scheduleables[key].Add( scheduleable );
  }

  // Remove a specific object from the schedule.
  // Useful for when an monster is killed to remove it before it's action comes up again.
  public void Remove( IScheduleable scheduleable )
  {
    KeyValuePair<int, List<IScheduleable>> scheduleableListFound 
      = new KeyValuePair<int, List<IScheduleable>>( -1, null );

    foreach ( var scheduleablesList in _scheduleables )
    {
      if ( scheduleablesList.Value.Contains( scheduleable ) )
      {
        scheduleableListFound = scheduleablesList;
        break;
      }
    }
    if ( scheduleableListFound.Value != null )
    {
      scheduleableListFound.Value.Remove( scheduleable );
      if ( scheduleableListFound.Value.Count <= 0 )
      {
        _scheduleables.Remove( scheduleableListFound.Key );
      }
    }
  }

  // Get the next object whose turn it is from the schedule. Advance time if necessary
  public IScheduleable Get()
  {
    var firstScheduleableGroup = _scheduleables.First();
    var firstScheduleable = firstScheduleableGroup.Value.First();
    Remove( firstScheduleable );
    _time = firstScheduleableGroup.Key;
    return firstScheduleable;
  }

  // Get the current time (turn) for the schedule
  public int GetTime()
  {
    return _time;
  }

  // Reset the time and clear out the schedule
  public void Clear()
  {
    _time = 0;
    _scheduleables.Clear();
  }
}

Quite a bit of code and if we run the game again nothing will have changed since the last tutorial. We won’t get a chance to actually see our scheduling system in action until next time.

As always the code for the tutorial series so far can be found on Bitbucket:
https://bitbucket.org/FaronBracy/roguesharpv3tutorial/commits/tag/13SchedulingSystem

Bored waiting for the next tutorial post? The completed project is already available on Bitbucket.