简体   繁体   English

关于基于类的文本冒险游戏设计的质疑。

[英]Query Regarding Design of Class-based Text Adventure Game.

I've been learning C# over the summer and now feel like making a small project out of what I've done so far. 我在夏天一直在学习C#,现在感觉就像我到目前为止做的一个小项目。 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. 可能是战斗系统等等。扇区可以连接多达4个其他扇区。

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) 从逻辑上讲,人们会期待一种方法,比如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 . 这意味着类Player需要知道类级别 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? 并且Level可能必须操纵Player对象(例如,玩家进入房间,被某些东西伏击,从库存中丢失一些东西。)所以现在Level还需要持有对玩家对象的引用?

This doesn't feel nice; 这感觉不太好; everything having to 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. 虽然我知道C ++中的函数指针,但有关代表的章节中提供了一些带有“基于事件”编程观点的例子,我对此并没有多少启示。

That gave me the idea to design the classes as follows: 这给了我设计类的想法如下:

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...
}

Is this an alright way to do this? 这是一个好的方法吗?
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 我习惯于发表非常详细的帖子......抱歉v__v

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: 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. 为了使代码更易于管理,您可以将主类之间的一些交互建模为类本身,例如Fight类。 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. 在你的问题中,你确定了如果你在Player类上遇到像Move方法这样的东西,你会有许多必须互相了解的类。 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. 通过对主要对象之间的“交互”进行建模,可以避免许多依赖关系。

Sounds like a scenario I often use a Command class or Service class for. 听起来像我经常使用Command类或Service类的场景。 For example, I might create a MoveCommand class that performs the operations and coordinations on and between Levels and Persons. 例如,我可能会创建一个MoveCommand类,用于在“层次”和“人员”之间执行操作和协调。

This pattern has the advantage of further enforcing the Single Responsibility Principal (SRP). 此模式具有进一步强制执行单一责任委托人(SRP)的优势。 SRP says that a class should only have one reason to change. SRP说,一个班级应该有一个改变的理由。 If the Person class is responsible for moving it will undoubtedly have more than one reason to change. 如果Person类负责移动它无疑会有不止一个改变的理由。 By breaking the logic of a Move off into its own class, it is better encapsulated. 通过将Move的逻辑分解为自己的类,可以更好地封装。

There are several ways to implement a Command class, each fitting different scenarios better. 有几种方法可以实现Command类,每种方法都可以更好地适应不同的场景。 Command classes could have an Execute method that takes all necessary parameters: 命令类可以有一个Execute方法,它接受所有必要的参数:

 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. 最后,您可以将参数作为构造函数参数提供给Command类,但我将放弃该代码。

In any event, I find using Commands or Services a very powerful way to handle operations, like Move. 无论如何,我发现使用命令或服务是一种非常强大的方法来处理操作,比如Move。

For a text-based game, you're almost certainly going to have a CommandInterpretor (or similar) object, which evaluates the user's typed commands. 对于基于文本的游戏,您几乎肯定会有一个CommandInterpretor(或类似)对象,它会评估用户的类型化命令。 With that level of abstraction, you don't have to implement every possible action on your Player object. 通过这种抽象级别,您无需在Player对象上实现所有可能的操作。 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). 您的解释器可能会将某些类型的命令推送到您的Player对象(“显示库存”),将一些命令推送到当前占用的Sector对象(“list exits”),将某些命令推送到Level对象(“move player North”),以及一些命令到专业对象(“攻击”可能被推送到CombatManager对象)。

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. 通过这种方式,Player对象变得更像Character,而CommandInterpretor更像是坐在键盘上的实际人类玩家的代表。

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. IMO在模式和酷炫技术以及所有爵士乐方面花费了太多精力。 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; 从OO的角度来看,这“有意义”; 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. 简单的代码更易于使用,并且更能抵御错误。

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. 恕我直言,这是创建解耦实现的最自然的方式。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM