Запрос о дизайне текстовой приключенческой игры на основе классов.
Я изучаю С# в течение лета, и теперь мне хочется сделать небольшой проект из того, что я сделал до сих пор. Я решил создать своего рода текстовую приключенческую игру.
Основная структура игры будет включать в себя несколько секторов (или комнат). При входе в комнату будет выведено описание и ряд действий и таких действий, которые вы можете предпринять; способность исследовать, подбирать, использовать материал в этой комнате; возможно, боевая система и т.д. Сектор может быть подключен до 4 других секторов.
Во всяком случае, набросанные на бумаге идеи о том, как разработать код для этого, я почесываю голову над структурой части моего кода.
Я выбрал класс игрока и класс уровня, который представляет уровень/подземелье/область. Этот класс уровней будет состоять из нескольких взаимосвязанных "секторов". В любой момент времени игрок будет присутствовать в одном определенном секторе на уровне.
Итак, вот путаница:
Логически, можно было бы ожидать такой метод, как player.Move(Dir d)
Такой метод должен изменить поле текущего сектора в объекте уровня. Это означает, что класс Player должен знать о уровне класса. Хммм.
И Level, возможно, придется манипулировать объектом Player (например, игрок входит в комнату, попадает в заложники чем-то, теряет что-то из инвентаря). Итак, теперь Level также должен содержать ссылку на объект 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...
}
}
Уровень:
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
Ответы
Ответ 1
Как насчет класса "Игра", который будет содержать большую часть информации, например, игрока и текущей комнаты. Для такой операции, как перемещение игрока, класс игры может переместить игрока в другую комнату на основе карты уровня комнаты.
Класс игры будет управлять всеми взаимодействиями между различными компонентами игр.
Использование событий для чего-то подобного приводит к тому, что ваши события будут запутаны. Если вы не будете осторожны, вы столкнетесь с событиями, которые увольняют друг друга и переполняют ваш стек, что приведет к появлению флагов для выключения событий в особых обстоятельствах и менее понятной программы.
UDPATE:
Чтобы сделать код более управляемым, вы могли бы моделировать некоторые из взаимодействий между основными классами как сами классы, такие как класс боя. Используйте интерфейсы, чтобы ваши основные классы могли выполнять определенные взаимодействия. (Обратите внимание, что я взял на себя смелость придумать несколько вещей, которые вы, возможно, не захотите в своей игре).
Например:
// 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 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
Звучит как сценарий, в котором я часто использую класс Command или Service class for. Например, я могу создать класс MoveCommand, который выполняет операции и координацию между Уровнями и Лицами.
Этот шаблон имеет то преимущество, что он обеспечивает соблюдение Принципа единой ответственности (SRP). SRP говорит, что у класса должна быть только одна причина для изменения. Если класс Person ответственен за его перемещение, у него, несомненно, будет несколько причин для изменения. Разбирая логику "Переместить" в свой класс, он лучше инкапсулируется.
Существует несколько способов реализации класса Command, каждый из которых лучше подходит для разных сценариев. Классы команд могут иметь метод Execute, который принимает все необходимые параметры:
public class MoveCommand {
public void Execute(Player currentPlayer, Level currentLevel) { ... }
}
public static void Main() {
var cmd = new MoveCommand();
cmd.Execute(player, currentLevel);
}
Или иногда я нахожу его более простым и гибким, чтобы использовать свойства в объекте команды, но это облегчает для клиентского кода неправильное использование класса, забыв установить свойства - но преимущество в том, что у вас есть тот же функциональную подпись для Execute для всех классов команд, поэтому вы можете создать интерфейс для этого метода и работать с абстрактными командами:
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, но я откажусь от этого кода.
В любом случае я нахожу использование Commands или Services очень мощным способом обработки операций, таких как Move.
Ответ 3
Для текстовой игры вы почти наверняка будете иметь объект CommandInterpretor (или аналогичный), который оценивает введенные пользователем команды. При таком уровне абстракции вам не нужно выполнять все возможные действия над вашим объектом Player. Ваш интерпретатор может подтолкнуть некоторые напечатанные команды к вашему объекту Player ( "показать инвентарь" ), некоторые команды для текущего объекта сектора ( "list exits" ), некоторые команды для объекта Level ( "move player North" ), а некоторые команды к объектам специальности ( "атака" может быть перенесена в объект CombatManager).
Таким образом, объект Player становится больше похожим на Character, а CommandInterpretor более репрезентален от фактического человека, сидящего на клавиатуре.
Ответ 4
Избегайте эмоционального или интеллектуального погрязшего в том, что "правильный" способ сделать что-то есть. Вместо этого сосредоточьтесь на этом. Не добавляйте слишком много значения в код, который вы уже написали, потому что любой или все его, возможно, придется изменить для поддержки того, что вы хотите сделать.
ИМО там слишком много энергии тратится на узоры и классные приемы и весь этот джаз. Просто напишите простой код, чтобы сделать то, что вы хотите сделать.
Уровень "содержит" все в нем. Вы можете начать там. Уровень не обязательно должен вести все, но все находится на уровне.
Игрок может двигаться, но только в пределах уровня. Поэтому игроку необходимо запросить уровень, чтобы увидеть, действительно ли направление движения.
Уровень не берет предметы у игрока и не наносит урона. Другие объекты на уровне делают эти вещи. Эти другие объекты должны искать игрока или, возможно, рассказывать о близости игрока, а затем они могут делать то, что хотят прямо от игрока.
Это нормально для уровня, чтобы "владеть" игроком и для игрока иметь ссылку на его уровень. Это "имеет смысл" с точки зрения ОО; вы стоите на Планете Земля и можете влиять на нее, но она тащит вас по вселенной, пока вы копаете дыры.
Простые вещи. Каждый раз, когда что-то осложняется, выясните, как сделать его простым. Простой код легче работать и более устойчив к ошибкам.
Ответ 5
Итак, во-первых, это хороший способ сделать это?
Абсолютно!
Во-вторых, если глава делегата была не было представлено так, как было, я бы не подумали об использовании таких 'Мероприятия'. Так что было бы хорошим способом реализовать это без использования обратные вызовы?
Я знаю много других способов реализовать это, но никакого другого хорошего способа без какого-либо механизма обратного вызова. ИМХО это самый естественный способ создания развязанной реализации.