views:

237

answers:

5

I've been learning C# over the summer and now feel like making a small project out of what I've done so far. I've decided on a sort of text based adventure game.

The basic structure of the game will involve having a number of sectors(or rooms). Upon entry into a room, a description will be outputted and a number of actions and such you may take; the ability to examine, pick up, use stuff in that room; possibly a battle system, etc etc. A sector may be connected up to 4 other sectors.

Anyway, scribbling ideas on paper on how to design the code for this, I'm scratching my head over the structure of part of my code.

I've decided on a player class, and a 'level' class that represents a level/dungeon/area. This level class would consist of a number of interconnected 'sectors'. At any given time, the player would be present in one certain sector in the level.

So here's the confusion:

Logically, one would expect a method such as

 player.Move(Dir d)

Such a method should change the "current sector" field in the level object. This means class Player would need to know about class Level. Hmmm. And Level may have to manipulate the Player object (eg. player enters room, ambushed by something, loses something from inventory.) So now Level also needs to hold a reference to the Player object?

This doesn't feel nice, having everything hold a reference to everything else.

At this point I remembered reading about delegates from the book I'm using. Though I know about function pointers from C++, the chapter on delegates was presented with examples with a sort of 'event based' programming viewpoint, with which I did not have much enlightenment about.

That gave me the idea to design the classes as such:

Player:

class Player
{
    //...

    public delegate void Movement(Dir d);   //enum Dir{NORTH, SOUTH, ...}

    public event Movement PlayerMoved;

    public void Move(Dir d)
    {        
        PlayerMoved(d);

        //Other code...
    }

}

Level:

class Level
{
    private Sector currSector;
    private Player p;
    //etc etc...

    private void OnMove(Dir d)
    {
        switch (d)
        {
            case Dir.NORTH:
                //change currSector
                //other code
                break;

                //other cases
        }
    }

    public Level(Player p)
    {
        p.PlayerMoved += OnMove;  
        currSector = START_SECTOR;
        //other code
    }

    //etc...
}

So firstly, is this an alright way to do this? Secondly, if the delegate chapter was not presented the way it was, I would not have thought of using such 'events'. So what would be a good way to implement this without using callbacks?

I have a habit of making highly detailed posts... sorry v__v

+4  A: 

What about a 'Game' class which would hold the majority of the information like a Player and a current room. For an operation such as moving the player, the Game class could move the player to a different room based on the room's level map.

The game class would manage all the interactions between the various components of the games.

Using events for something like this brings the danger that your events will get tangled. If you're not careful you'll end up with events firing each other off and overflowing your stack, which will lead to flags to turn events off under special circumstances, and a less understandable program.

UDPATE:

To make the code more manageable, you could model some of the interactions between the main classes as classes themselves, such as a Fight class. Use interfaces to enable your main classes to perform certain interactions. (Note that I have taken the liberty of inventing a few things you may not want in your game).

For example:

// Supports existance in a room.
interface IExistInRoom { Room GetCurrentRoom(); }

// Supports moving from one room to another.
interface IMoveable : IExistInRoom { void SetCurrentRoom(Room room); }

// Supports being involved in a fight.
interface IFightable
{
  Int32 HitPoints { get; set; }
  Int32 Skill { get; }
  Int32 Luck { get; }
}

// Example class declarations.
class RoomFeature : IExistInRoom
class Player : IMoveable, IFightable
class Monster : IMoveable, IFightable

// I'd proably choose to have this method in Game, as it alters the
// games state over one turn only.
void Move(IMoveable m, Direction d)
{
  // TODO: Check whether move is valid, if so perform move by
  // setting the player's location.
}

// I'd choose to put a fight in its own class because it might
// last more than one turn, and may contain some complex logic
// and involve player input.
class Fight
{
  public Fight(IFightable[] participants)

  public void Fight()
  {
    // TODO: Logic to perform the fight between the participants.
  }
}

In your question, you identified the fact that you'd have many classes which have to know about each other if you stuck something like a Move method on your Player class. This is because something like a move neither belongs to a player or to a room - the move affects both objects mutually. By modelling the 'interactions' between the main objects you can avoid many of those dependencies.

Alex Humphrey
For moving, I'd have a method in Game that did: new_loc = player.GetNewLocation(direction), level.CheckLocation(new_loc), player.SetLocation(new_loc), level.SetPlayerLocation(player). I'd also make the Game object a scripted object (like Lua, Python, etc).
Skizz
When I came up with the above implementation, I suddenly felt a lot of things could be implemented via such events and tried to remind myself not to get carried away. I guess it's just the effect of using a new 'toy'.
Rao
If you followed this method of design through, wouldn't your Game class end up enormous, with all kinds of data and functions that were only related to each other through virtue of being in the same application (Game)?
qstarin
^ I was thinking that too. I had aimed that any such Game class should have minimal glue-together-code.
Rao
@qstarin - in my head the majority of the data would just be a single reference to a list of levels which each contained rooms which each contained items/monsters/player. The Game class would take responsibility for moving those things around - not all the logic would have to be in the Game class. For instance, you could have a Fight class to contain logic for governing battles between a player and monster - not everything has to be in Game. Looking at my answer, I guess I should have taken more time to explain.
Alex Humphrey
@qstarin - just read your answer, and I guess the Fight class I mentioned above would be FightCommand using the coding style in your proposed answer.
Alex Humphrey
This could work well, just be careful `Game` doesn't become a "God class" controlling everything (advice taken from Code Complete 2nd ed.).
Justin Ardini
Alex, thanks for that detailed update. +1 for clarifying things. Right now, I've chosen a certain path and will go along it for now. If I hit a dead end, I'll consider shifting to your design. It certainly looks elegant at first few glances.Damn OO design dilemma >_<.
Rao
A: 

So firstly, is this an alright way to do this?

Absolutely!

Secondly, if the delegate chapter was not presented the way it was, I would not have thought of using such 'events'. So what would be a good way to implement this without using callbacks?

I know a lot of other ways to implement this, but no any other good way without some kind of callback mechanism. IMHO it is the most natural way to create a decoupled implementation.

Doc Brown
+2  A: 

Sounds like a scenario I often use a Command class or Service class for. For example, I might create a MoveCommand class that performs the operations and coordinations on and between Levels and Persons.

This pattern has the advantage of further enforcing the Single Responsibility Principal (SRP). SRP says that a class should only have one reason to change. If the Person class is responsible for moving it will undoubtedly have more than one reason to change. By breaking the logic of a Move off into its own class, it is better encapsulated.

There are several ways to implement a Command class, each fitting different scenarios better. Command classes could have an Execute method that takes all necessary parameters:

 public class MoveCommand {
    public void Execute(Player currentPlayer, Level currentLevel) { ... }
 }

 public static void Main() {
     var cmd = new MoveCommand();
     cmd.Execute(player, currentLevel);
}

Or, sometimes I find it more straightforward, and flexible, to use properties on the command object, but it makes it easier for client code to misuse the class by forgetting to set properties - but the advantage is that you have the same function signature for Execute on all command classes, so you can make an interface for that method and work with abstract Commands:

 public class MoveCommand {
    public Player CurrentPlayer { get; set; } 
    public Level CurrentLevel { get; set; }
    public void Execute() { ... }
 }

 public static void Main() {
     var cmd = new MoveCommand();
     cmd.CurrentPlayer = currentPlayer;
     cmd.CurrentLevel = currentLevel;
     cmd.Execute();
}

Lastly, you could provide the parameters as constructor arguments to the Command class, but I'll forgo that code.

In any event, I find using Commands or Services a very powerful way to handle operations, like Move.

qstarin
+1 mate, this would work pretty nicely, and you'd end up with code that was very uniform and easy to produce/maintain.
Alex Humphrey
Thank you. It works well for me implementing functionality in MVC projects, and what I really like is how it also supports Open-Closed Principal. I change behavior by implementing a new Command class, rather than changing method implementations in a [usually overgrown] Service class.
qstarin
I've been mulling over this answer. What are some constraints on the design of the Player, Level classes using this pattern?Ideally, what kind of methods should MoveCommand.Execute call?
Rao
@Rao - it depends how you're going to track the player. I've added some information which may help to my answer, but it applies to this answer also.
Alex Humphrey
+1  A: 

For a text-based game, you're almost certainly going to have a CommandInterpretor (or similar) object, which evaluates the user's typed commands. With that level of abstraction, you don't have to implement every possible action on your Player object. Your interpreter might push some typed commands to your Player object ("show inventory"), some commands to the currently-occupied Sector object ("list exits"), some commands to the Level object ("move player North"), and some commands to specialty objects ("attack" might be pushed to a CombatManager object).

In that way, the Player object becomes more like the Character, and the CommandInterpretor is more respresentational of the actual human player sitting at the keyboard.

mikemanne
Everything is still in the scribbling-on-paper phase, and hence I will meditate deeply on your answer and that of qstarin :)
Rao
+1  A: 

Avoid getting emotionally or intellectually mired in what the "right" way to do something is. Focus instead on doing. Don't put too much value on the code you've already written, because any or all of it may need to change to support things that you want to do.

IMO there's way too much energy being spent on patterns and cool techniques and all of that jazz. Just write simple code to do the thing you want to do.

The level "contains" everything within it. You can start there. The level shouldn't necessarily drive everything, but everything is in the level.

The player can move, but only within the confines of the level. Therefore, the player needs to query the level to see if a move direction is valid.

The level isn't taking items from the player, nor is the level dealing damage. Other objects in the level are doing these things. Those other objects should be searching for the player, or maybe told of the player's proximity, and then they can do what they want directly to the player.

It's ok for the level to "own" the player and for the player to have a reference to its level. This "makes sense" from an OO perspective; you stand on Planet Earth and can affect it, but it is dragging you around the universe while you're digging holes.

Do Simple Things. Any time something gets complicated, figure out how to make it simple. Simple code is easier to work with and is more resistant to bugs.

dash-tom-bang
I'll keep that in mind.
Rao