Object-oriented dungeon crawler game

I am creating a simple dungeon crawler game. My program runs and does what I want it to do so far, but I am starting to run into difficulty with adding new functionality, and I think one of the main issues is my class design.

I want to refactor my classes according to proper OOP principles, of which I am attempting to understand. I would like to have a better understanding of this now so that I will have an easier time going forward.

I am thinking about restructuring my classes according to the following:

Character:

  • id
  • name
  • currenthp
  • maximumhp
  • level
  • experience
  • Room (Character should know its location, correct?)
  • Monster (Character should also know its target, correct?)

Monster:

  • id
  • name
  • currenthp
  • maximumhp
  • level
  • Room (like Character, Monster should know its location?)
  • Character (like Character, Monster should know its target?)

Room:

  • id
  • location
  • The location of its doors
  • Dungeon? (should Room know what Dungeon it is in?)
  • Monster(s)? (should Room know what monsters are in it?)
  • Character? (should Roomknow what character is in it?)

Dungeon:

  • id
  • name
  • Rooms

Does the above seem like a reasonable design for my classes? My main question is: to what extent should each class know about the other classes?

Character class:

private class Character
{
    public string name { get; set; }
    public int level { get; set; }
    public Tuple<int, int> currentLocation { get; set; }
    public int currentHP { get; set; }
    public int maximumHP { get; set; }
    public Dungeon dungeon { get; set; }
    public Monster target { get; set; }

    public Character(string name, Dungeon dungeon)
    {
        this.name = name;
        level = 1;
        maximumHP = 100;
        currentHP = 100;
        this.dungeon = dungeon;

        switch (dungeon.Value)
        {
            case 1: //Dungeon 1
                currentLocation = new Tuple<int, int>(5, 0);
                break;
            case 2: //Dungeon 2
                currentLocation = new Tuple<int, int>(0, 0);
                break;
            case 3: //Dungeon 3
                currentLocation = new Tuple<int, int>(0, 0);
                break;
        }
    }

    public List<string> lookAroundRoom()
    {
        Room room = dungeon.DungeonLayout[currentLocation];
        List<string> targets = new List<string>();
        foreach (Monster monster in room.monster)
        {
            targets.Add(monster.Name);
        }
        return targets;
    }

    public Monster targetMonster(ListBox lst)
    {
        //I think this should be split into two methods
        //currently it does two things - sets the monster as the target
        //and then returns monster data
        Room room = dungeon.DungeonLayout[currentLocation];
        target = room.monster[lst.SelectedIndex];
        return target;
    }
}

Monster class:

public class Monster
{
    private static int autoIncrementID = 1;
    public int ID
    {
        get { return autoIncrementID; }
    }
    public string Name { get; set; }
    public int level { get; set; }
    public int currentHP { get; set; }
    public int maximumHP { get; set; }

    public Monster()
    {
        Name = $"Monster {autoIncrementID++}";
        currentHP = 20;
        maximumHP = 20;
        level = 1;
    }
}

public void MonsterDie(Room room, Monster monster)
{
    room.monster = null;
}

Room class:

public class Room
{
    public bool MoveNorth { get; set; }
    public bool MoveEast { get; set; }
    public bool MoveSouth { get; set; }
    public bool MoveWest { get; set; }
    public List<Monster> monster { get; set; }

    public Room(bool moveNorth = false, bool moveEast = false, bool moveSouth = false, bool moveWest = false)
    {
        MoveNorth = moveNorth;
        MoveEast = moveEast;
        MoveSouth = moveSouth;
        MoveWest = moveWest;

        monster = new List<Monster>();
        monster.Add(new Monster());
    }
}

Dungeon class:

public class Dungeon
{
    public string Name { get; set; }
    public int Value { get; set; }
    public Dictionary<Tuple<int, int>, Room> DungeonLayout { get; set; }

    public Dungeon(int value)
    {
        DungeonLayout = new Dictionary<Tuple<int, int>, Room>();

        switch (value)
        {
            case 1:
                Name = "Dungeon 1";
                Value = value;
                DungeonLayout.Add(new Tuple<int, int>(5, 0), new Room(false, false, false, true));
                DungeonLayout.Add(new Tuple<int, int>(4, 0), new Room(false, true, true, false));
                //Code truncated. The rest of this code contains 
                //more lines like the above two 
                //to generate the layout of the dungeon. 
        }
    }
}

Answer

First off, I wrote a series of articles about some of the problems of class design in this domain, though it really is about class design in general. Wizards and warriors is just the “fun” domain that makes it interesting:

https://ericlippert.com/2015/04/27/wizards-and-warriors-part-one/

You should read the whole series and understand all of it; the takeaway is: it is surprisingly hard to come up with good classes in the wizards and warriors domain, so don’t feel bad if you are struggling with this. There are many pitfalls.

The other answer has some good advice, and some that is questionable. (There is a small dungeon class and a dark dungeon class, so what class do you use if you want a small, dark dungeon? Don’t use the “inheritance pivot” on properties that are orthogonal. Just make a property “size” and a property “light” in the base class.)

Here’s more good advice.

Several of your parenthetical questions are about location awareness:

Character should know its location, correct?

The classic way to solve this problem is to make every object that exists in the dungeon inherit from a common base class: GameObject, say. Every GameObject has:

  • A parent, which is another GameObject; all game objects have a parent except the root, which is typically the dungeon. The parent of the key is the bag, the parent of the bag is the player, the parent of the player is the jail cell, the parent of the jail cell is the dungeon, which has no parent.
  • A list, possibly empty, of child GameObjects.

And, most important:

  • We require that the parent-child relationship be consistent! If the parent of the key is the bag, then the key must be a child of the bag.

Moving around the dungeon is now just a special case of the general problem of moving objects. When the dragon moves from the drawbridge to the moat, the dragon stops being a child of the drawbridge and starts being a child of the moat.

Getting these relationships coded correctly is of vital importance; you don’t want “put the bag inside the bag” to produce an infinite regress, and you certainly don’t want “put the library in the bag” to work. Think of all the ways things can go terribly wrong, and figure out a way to prevent them.

Looking at your class design: I notice that Character and Monster are almost exactly identical. This should tell you something. Characters and Monsters are the same thing. The difference between a character and a monster is that the characters actions are determined by the player, and the monsters actions are determined by an AI. But from the point of view of the game engine, they’re the same. So make them the same.

Notice that in your proposed design you have silently baked limitations of the game into the type system. A monster can have only one location; OK, that seems reasonable. That location is a room — too restrictive. You should be able to put the dragon in a cage, and the cage in a room. A monster can only target characters? Too restrictive; a dragon should be able to attack a werewolf. (Again, we see that characters and monsters are really the same thing.) A monster can only target one opponent? Too restrictive; what if the dragon wants to claw one character and fire breathe on another?

Think about what limitations you are baking in when you design your class hierarchies, and make sure you are only baking in things that really have to be invariants, like having a single location.

Finally, as I note in my series: use good OO design for more than just the game objects. There are lots of coding concepts in a game other than the objects manipulated by the player in the game; there are players and rules and commands and actions and many other concepts. Those are actually the core of your game engine; design them carefully too.

Attribution
Source : Link , Question Author : Iceape , Answer Author : Eric Lippert

Leave a Comment