Запрос относительно дизайна текстовой приключенческой игры. - PullRequest
14 голосов
/ 16 июля 2010

Я изучал C # летом и теперь чувствую, что хочу сделать небольшой проект из того, что я сделал до сих пор. Я выбрал своего рода текстовую приключенческую игру.

Основная структура игры будет включать в себя несколько секторов (или комнат). При входе в комнату будет выведено описание и ряд действий, которые вы можете предпринять; способность исследовать, подбирать, использовать вещи в этой комнате; возможно, боевая система и т. д. и т. д. Сектор может быть связан с 4 другими секторами.

Во всяком случае, набрасывая идеи на бумаге о том, как разработать код для этого, я ломаю голову над структурой части моего кода.

Я выбрал класс игрока и класс «уровень», который представляет уровень / подземелье / область. Этот класс уровня будет состоять из нескольких взаимосвязанных «секторов». В любой момент времени игрок будет присутствовать в одном определенном секторе на уровне.

Так вот путаница:

Логически можно ожидать такой метод, как player.Move(Dir d)
Такой метод должен изменить поле «текущий сектор» в объекте уровня. Это означает, что класс Игрок должен знать о классе Уровень . Хммм. И Level , возможно, придется манипулировать объектом Player (например, игрок входит в комнату, попал в засаду, теряет что-то из инвентаря). Так что теперь Level также необходимо удерживать ссылку на объект Player ?

Это не очень приятно; все, что должно содержать ссылку на все остальное.

В этот момент я вспомнил, что читал о делегатах из книги, которую я использую. Хотя я знаю о указателях функций из C ++, глава о делегатах была представлена ​​с примерами с некой «основанной на событиях» точки зрения программирования, с которой у меня не было особого понимания.

Это дало мне идею спроектировать классы следующим образом:

Игрок:

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

}

Уровень:

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

Это хороший способ сделать это?
Если бы глава делегата не была представлена ​​так, как она была, я бы не подумал об использовании таких «событий». Итак, что было бы хорошим способом реализовать это без использования обратных вызовов?

У меня есть привычка делать очень подробные сообщения ... извините, v__v

Ответы [ 5 ]

5 голосов
/ 16 июля 2010

А как насчет класса «Game», который будет содержать большую часть информации, такой как игрок и текущая комната. Для такой операции, как перемещение игрока, класс Game может переместить игрока в другую комнату на основе карты уровня комнаты.

Класс игры будет управлять всеми взаимодействиями между различными компонентами игр.

Использование событий для чего-то подобного приносит опасность того, что ваши события запутаются. Если вы не будете осторожны, вы получите события, запускающие друг друга и переполняющие ваш стек, что приведет к тому, что флаги отключат события при особых обстоятельствах, и менее понятной программе.

UDPATE:

Чтобы сделать код более управляемым, вы можете смоделировать некоторые взаимодействия между основными классами как сами классы, такие как класс Fight. Используйте интерфейсы, чтобы ваши основные классы могли выполнять определенные взаимодействия. (Обратите внимание, что я позволил себе изобрести несколько вещей, которые вам могут не понравиться в вашей игре).

Например:

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

В своем вопросе вы указали тот факт, что у вас будет много классов, которые должны знать друг о друге, если вы добавите что-то вроде метода Move в свой класс Player. Это потому, что что-то вроде движения не принадлежит ни игроку, ни комнате - движение влияет на оба объекта взаимно. Моделируя «взаимодействия» между основными объектами, вы можете избежать многих из этих зависимостей.

2 голосов
/ 16 июля 2010

Похоже на сценарий, для которого я часто использую класс Command или Service.Например, я мог бы создать класс MoveCommand, который выполняет операции и координирование между уровнями и персонами.

Этот шаблон имеет преимущество, заключающееся в дальнейшем применении принципала единой ответственности (SRP).SRP говорит, что у класса должна быть только одна причина для изменения.Если класс Person отвечает за перемещение, у него, несомненно, будет несколько причин для изменения.Разрывая логику Move в своем собственном классе, он лучше инкапсулируется.

Существует несколько способов реализации класса Command, каждый из которых лучше соответствует различным сценариям.Командные классы могут иметь метод Execute, который принимает все необходимые параметры:

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

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

Или, иногда я нахожу более простым и гибким использование свойств объекта команды, но это упрощает клиентский коднеправильно использовать класс, забыв установить свойства - но преимущество заключается в том, что у вас есть одинаковая сигнатура функции для «Выполнить» во всех классах команд, поэтому вы можете создать интерфейс для этого метода и работать с абстрактными командами:

 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();
}

Наконец, вы можете предоставить параметры в качестве аргументов конструктора для класса Command, но я воздержусь от этого кода.

В любом случае, я считаю использование команд или служб очень мощным способом обработки операций, таких какMove.

1 голос
/ 16 июля 2010

Избегайте эмоционального или интеллектуального замешательства в том, что такое «правильный» способ сделать что-то.Вместо этого сосредоточьтесь на , делая .Не придайте слишком большого значения коду, который вы уже написали, потому что может понадобиться изменить его или все, чтобы поддержать то, что вы хотите сделать.

ИМО, слишком много энергии тратится нашаблоны и классные приемы и все такое прочее.Просто напишите простой код, чтобы делать то, что вы хотите.

Уровень «содержит» все, что в нем.Вы можете начать там.Уровень не обязательно должен управлять всем, но все находится на уровне.

Игрок может двигаться, но только в пределах уровня.Поэтому игрок должен запросить уровень, чтобы увидеть, является ли направление движения действительным.

Уровень не получает предметы от игрока, а также уровень не наносит урон.Другие объекты на уровне делают эти вещи.Эти другие объекты должны искать игрока или, возможно, сказать о близости игрока, и тогда они могут делать то, что хотят, непосредственно игроку.

Это нормально для уровня, чтобы «владеть» игроком иигрок должен иметь ссылку на свой уровень.Это «имеет смысл» с точки зрения ОО;вы стоите на планете Земля и можете влиять на нее, но она тащит вас по вселенной, пока вы копаете ямы.

Делайте простые вещи.Каждый раз, когда что-то усложняется, придумайте, как это сделать проще.С простым кодом легче работать и он более устойчив к ошибкам.

1 голос
/ 16 июля 2010

Для текстовой игры у вас почти наверняка будет объект CommandInterpretor (или аналогичный), который оценивает введенные пользователем команды.При таком уровне абстракции вам не нужно реализовывать все возможные действия с вашим объектом Player.Ваш интерпретатор может передать некоторые введенные команды вашему объекту Player («показать инвентарь»), некоторые команды занятому в данный момент объекту Sector («список выходит»), некоторые команды объекту Level («переместить игрока на север»), а некоторыеКоманды к специальным объектам («атака» может быть передана объекту CombatManager).

Таким образом, объект Player становится более похожим на персонажа, а CommandInterpretor является более представительным для реального игрока-человека, сидящего заклавиатура.

0 голосов
/ 16 июля 2010

Итак, во-первых, это хороший способ сделать это?

Абсолютно! * * 1005

Во-вторых, если глава делегата была не представил так, как было бы, я бы не думал об использовании таких 'События'. Так что было бы хорошим способом реализовать это без использования обратные вызовы?

Я знаю много других способов реализовать это, но нет другого хорошего способа без какого-либо механизма обратного вызова. ИМХО, это самый естественный способ создания развязанной реализации.

...