As mentioned in my Refactoring for AI post, things are starting to get a bit complicated. The fundamental problem lies in the fact that my code is based on a dungeon generator design and not on a game design.
I make use of a generic map class that encapsulates an array of tiles. For dungeon generation I use an interface IDungeonTile to describe my dungeon tile behaviour.
public interface IDungeonTile
{
bool IsObstacle { get; }
bool IsWalkable { get; }
}
The dungeon tiles know if they are obstacles and if they are walkable. My dungeon generator would start with a map filled with tiles full of rock (i.e. where IsObstable is true and IsWalkable is false) and then start to carve out corridors, place rooms and add doors.
My tiles also handled the logic for when a player would bump into them. Closed door tiles would know to open themselves, walls would repel the player etc. Notice that I only talk about the player, this was due to the fact that it was all I was thinking about at the time. The following code shows the logic of the ClosedDoorTile class for when something bumps into it.
public bool HandleBump(DungeonMap dungeon, Point location)
{
if (IsObstacle)
{
dungeon[location] = new OpenDoorTile();
return false;
}
return true;
}
My path finding implementation started to raise the first real issues of this design. For the A* algorithm you need to provide a movement cost for the various terrain features. The question I had to ask myself was, what determined the movement cost of over a specific type of terrain? You could simplify this to whether a certain terrain type is an obstacle or not. My current design, where the IDungeonTile objects determined if they were obstacles or not, meant that I cold only support one type of movement cost for all entities (or actors).
My A* implementation required some further information that needed to be abstracted into some kind of common behaviour. For this I created the IMovementBehaviour interface listed below.
public interface IMovementBehaviour
{
/// <summary>
/// The directions available for movement. Use this to constrain the movement behaviour to certain directions when moving.
/// </summary>
Direction[] DirectionsAvailable { get; }
/// <summary>
/// Retrieves the movement cost for this behaviour based on the terrain type.
/// </summary>
/// <param name=”dungeonTile”>The dungeon terrain type to evaluate</param>
/// <returns>The movement cost calculated for the dungeon terrain type</returns>
double GetTerrainMovementCost(IDungeonTile dungeonTile);
/// <summary>
/// Determines if this movement behaviour can walk on a specific terrain type
/// </summary>
/// <param name=”dungeonTile”>The dungeon terrain type to evaluate</param>
/// <returns>Returns true if the behaviour can walk on the terrain type</returns>
bool CanWalkOnTerrain(IDungeonTile dungeonTile);
/// <summary>
/// Handles the interaction with a dungeon terrain type at a specific location
/// </summary>
/// <param name=”dungeon”>The dungeon representing the terrain</param>
/// <param name=”location”>The location to bump into</param>
/// <returns>Return true if the bump was successful</returns>
bool HandleBump(DungeonMap dungeon, Point location);
}
The idea behind the movement behaviours was to abstract how something moves around the dungeon away from the terrain itself. I envisaged that a PlayerMovementBehaviour would react completely different to a GhostMovementBehaviour for example. With the IMovementBehaviour interface I was able to do this by
- limiting the directions available for movement,
- terrain movement cost based on terrain type,
- determining if a particular terrain type was movable,
- and handling what happened if a particular “movement behaviour” bumped into something.
(I am not completely convinced that HandleBump should belong with movement behaviour.)
My IDungeonTiles now no longer define if they are obstacles or not. Rather I envisage them to define some kind of property such as whether they are solid rock, water, fire, etc. The movement behaviour would use these properties to determine if an actor can walk (or move) on the terrain and the associated movement cost for the terrain type.
Of course I now have to refactor all my original code to make use of movement behaviours. The end result will be a more flexible system which is no longer tied to a specific type of actor.