С++/ООП дизайн игры

Я практикую свои навыки новичка в С++, создавая небольшую игру с использованием SFML и OpenGL. Часть программирования по большей части шла нормально, но у меня есть вопросы относительно фактического дизайна кода/класса.

У меня есть один класс, MainLoop, который содержит игровой цикл и владеет одним экземпляром каждого из следующих классов: Events, Graphics, Commands, Game и UI. Сначала я хотел, чтобы все они были одним классом (с функциями, разделенными в разных файлах .cpp), но мне сказали, что это неправильный подход для ООП/С++. Однако, несмотря на то, что я вижу хорошие стороны в их разделении (инкапсуляция, модульность, отладка), похоже, я также сталкиваюсь с множеством плохих вещей. Позвольте мне привести пример с пользователем, нажимающим кнопку пользовательского интерфейса.

Во-первых, MainLoop получает событие из класса окна SFML. MainLoop отправляет его в мой собственный класс Event, который интерпретирует событие и отправляет его в класс пользовательского интерфейса, чтобы проверить, «нажал» ли он какую-либо из кнопок. Если это правда, класс пользовательского интерфейса затем отправляет его классу Command, который интерпретирует команду кнопки. Затем, наконец, командный класс отправляет его классу Game или куда-либо еще, куда ему нужно.

Все это кажется мне очень неуклюжим, а также, по крайней мере, так, как я это делал в данный момент, требовало много предварительных объявлений (и прежде чем я узнал о них, у меня появилось множество циклических зависимостей) . Я сомневаюсь, что это также хорошо влияет на производительность.

В любом случае, есть ли здесь какой-то трюк, который я упускаю? Как эти классы должны быть связаны, как они должны общаться? Как я должен пересылать команды, скажем, из класса Event в класс пользовательского интерфейса? Должен ли я действительно иметь форвардные объявления, включения и прочее везде, и разве это не разрушает модульность? Должен ли я запускать все это через класс MainLoop и пересылать результаты, используя целые числа/поплавки/символы, которые вместо этого не требуют объявлений? Я тут немного в растерянности.


person user1870725    schedule 02.12.2012    source источник


Ответы (2)


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

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

Затем вам нужен абстрактный класс GameView, который должен предоставлять базовый интерфейс извне, например:

  • drawMe(), рисующий вид
  • receivedMouseEvent(Event e), который будет получать события мыши
  • activate() и deactivate() для выполнения действий, которые должны выполняться при нажатии или выталкивании представления в диспетчере представлений.

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

Хорошо иметь подклассы для управления элементами пользовательского интерфейса, например, класс ActiveArea, который реагирует на клики, Button, который наследуется от ActiveArea, а также может предоставлять графику с двумя состояниями. Эти элементы должны содержаться в списке интерактивных элементов, который хранится в абстрактном представлении, чтобы каждое конкретное представление могло без проблем добавлять свои кнопки в общую реализацию. Таким образом, вы можете получить что-то вроде (метакод)

void AbstractView::receiveEvent(Event e) {
  for (ActiveArea *area in areas)
    if (area.isInside(e)) {
      area->action();
      return;
    }

  innerReceiveEvent(e); //which should be a pure virtual function that will call a method specified in concrete views
}

Таким образом, у вас будет каждое представление, управляющее своим собственным состоянием, и диспетчер представлений, который позаботится о рисовании и управлении событиями, например

void ViewManager::draw() {
  for (AbstractView *view in views) // from top to bottom of the stack
    view.draw();
}
person Jack    schedule 02.12.2012
comment
Я не рассматривал такой подход, но он звучит полезно. До сих пор я просто использовал простой глобальный 'GameState', который я реализую в Events/UI/Commands, чтобы решать, что показывать и как реагировать на ввод. т.е. если GameState==MainMenu, нарисуйте эти объекты пользовательского интерфейса, если в основном представлении игры, нарисуйте их и так далее. Ваш подход может быть немного чище и полезнее. А ты их совсем не уничтожаешь? Вы просто делаете их активными/неактивными? - person user1870725; 02.12.2012
comment
Это в основном зависит от ваших конкретных проблем, если представления тяжелые, вы должны их уничтожить, в противном случае вы можете просто их деактивировать. Конечно, первый подход требует, чтобы у вас было постоянное состояние представления (которое может быть внутренним объектом), чтобы вы могли восстановить его при необходимости. - person Jack; 03.12.2012
comment
Я также использовал подход, который вы предложили, но я почувствовал необходимость переключиться на что-то более похожее на тот, который я объяснил однажды, что сложность всего этого выросла до определенного порога, код стал настолько беспорядочным, что его стоило рефакторинг - person Jack; 03.12.2012
comment
Я думаю, что попробую свои силы в реализации чего-то подобного, поскольку я вижу, как мой текущий подход в конечном итоге станет беспорядочным. Похоже, потребуется некоторое планирование, чтобы все заработало правильно. Спасибо за совет! - person user1870725; 03.12.2012

Я могу себе представить, что это кажется тяжелым, но это правильный способ сделать это. Обратите внимание, что вызовы функций совсем не тяжелые, и это значительно облегчает чтение. Или, по крайней мере, должен. ;-)

Каждый класс должен иметь заголовочный файл, содержащий определение класса, но не реализацию его функций-членов. Любой файл должен иметь возможность включать любой заголовочный файл класса. Только если вы используете шаблоны (где реализация должна быть в заголовочном файле), могут быть циклические зависимости, но из вашего описания я не думаю, что они у вас есть. Заголовки не должны включать друг друга. Если вам нужно передать указатели или ссылки на другие классы в аргументах функции, можно объявить другие классы в начале вашего заголовка. Вы должны иметь возможность включить любое включение вверху исходного файла. Если нет, пожалуйста, дайте больше информации о том, почему вы считаете, что это необходимо в вашем случае.

person Bas Wijnen    schedule 02.12.2012
comment
Я согласен.. и я по умолчанию передаю объявление, где это возможно.. Имейте в виду, думая о классах и их общедоступных функциях, что цель объектно-ориентированного подхода состоит в том, чтобы управлять сложностью (для себя и любого другого человека, работающего над кодом) путем создания слоев абстракция. А для производительности обычно выбранные вами алгоритмы/структуры данных оказывают большее влияние, чем вызов функций. Хотя с играми и другим программным обеспечением в реальном времени вам иногда приходится идти на компромисс ради производительности. - person Emile Vrijdags; 02.12.2012
comment
Мне кажется странным везде использовать предварительные объявления. Разве каждый класс не должен оптимально «стоять сам по себе»? Ради модульности, я имею в виду. т.е. если моему классу Graphics нужен мой класс пользовательского интерфейса (например, чтобы получить координаты элементов пользовательского интерфейса для рисования), то я не смогу использовать свой класс Graphics в другом приложении, не включив также класс пользовательского интерфейса... Такие вещи. Кроме того, в том же отношении должны ли команды переходить непосредственно из одного подкласса (например, событий) в другой (например, пользовательский интерфейс) или они должны передаваться через мой класс MainLoop, сохраняя, по крайней мере, некоторую модульность? - person user1870725; 02.12.2012
comment
Вашему классу Graphics понадобится класс пользовательского интерфейса, он не обязательно должен быть вашим. Он только должен определять тот же интерфейс (или, по крайней мере, те части, которые вы используете). Что касается прохождения основного цикла, все зависит от того, какой класс чем управляет. Если вашему графическому классу нужно что-то нарисовать на экране, я не вижу проблем в том, чтобы позволить ему сделать прямой вызов пользовательского интерфейса. Но если нажатие кнопки должно запускать какую-то команду, я не думаю, что пользовательский интерфейс (который обнаруживает нажатие кнопки) должен знать о значении своих кнопок. Поэтому имеет смысл позволить MainLoop решать, какую команду вызывать. - person Bas Wijnen; 02.12.2012
comment
Спасибо! На самом деле не очень удобно использовать так много указателей, ссылок и предварительных объявлений повсюду, но я думаю, что все это начнет казаться более естественным, когда я получу больше практики в больших проектах, подобных этому. - person user1870725; 03.12.2012