Дизайн игры в ОО манере - PullRequest
       3

Дизайн игры в ОО манере

21 голосов
/ 09 февраля 2010

Я разрабатываю простую игру, которая использует Java 2D и физику Ньютона. В настоящее время мой основной «игровой цикл» выглядит примерно так:

do {
  for (GameEntity entity : entities) {
    entity.update(gameContext);
  }

  for (Drawable drawable : drawables) {
    drawable.draw(graphics2d);
  }
} while (gameRunning);

Когда объект получает указание обновить себя, он корректирует свою скорость и положение в зависимости от действующих на него сил. Тем не менее, мне нужно, чтобы сущности проявляли другое поведение; например если игрок выстрелил в «плохого парня», сущность должна быть уничтожена и удалена из игрового мира.

Мой вопрос : Как лучше всего достичь этого объектно-ориентированным способом? Все примеры, которые я до сих пор видел, включают игровой цикл в класс God, называемый чем-то вроде Game, который выполняет следующие шаги: обнаружение столкновений, check-if-bad-guy-kill-check, check-if-player- убит, перекрасит и т. д. и инкапсулирует все состояние игры (оставшиеся жизни и т. д.). Другими словами, это очень процедурная и вся логика в классе Game . Кто-нибудь может порекомендовать лучший подход?

Вот варианты, о которых я думал до сих пор:

  • Передайте GameContext каждому объекту, из которого объект может удалить себя, если требуется, или обновить состояние игры (например, "не работает", если игрок убит).
  • Зарегистрируйте каждого GameEntity в качестве слушателя центрального Game класса и используйте подход, ориентированный на события; например столкновение привело бы к стрельбе CollisionEvent между двумя участниками столкновения.

Ответы [ 6 ]

14 голосов
/ 10 февраля 2010

Я работал в тесном контакте с двумя коммерческими игровыми движками, и они следуют аналогичной схеме:

  • Объекты представляют собой компоненты или аспекты игровой сущности (например, физической, воспроизводимой и т. Д.), А не целую сущность. Для каждого типа компонента существует огромный список компонентов, по одному для каждого экземпляра сущности, в которой есть компонент.

  • Сам тип игровой сущности - это просто уникальный идентификатор. У каждого гигантского списка компонентов есть карта для поиска компонента (если таковой существует), который соответствует идентификатору объекта.

  • Если компонент требует обновления, он вызывается службой или системным объектом. Каждый сервис обновляется прямо из игрового цикла. В качестве альтернативы вы можете вызывать службы из объекта планировщика, который определяет порядок обновления из графа зависимостей.

Вот преимущества этого подхода:

  • Вы можете свободно комбинировать функциональность без написания новых классов для каждого сочетание или использование сложного наследования деревья.

  • Функциональности, которая Вы можете предположить обо всей игре сущности, которые вы могли бы поместить в игру базовый класс сущности (что делает свет общего с гоночной машиной или небо ящик?)

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

6 голосов
/ 09 февраля 2010

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

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

Мы интенсивно использовали управляемый событиями код (шаблон слушателя) и множество таймеров.

Например, у нас был базовый класс для пересекаемого объекта, который мог прослушивать событие столкновения. Мы вложили его в коробку здоровья. При столкновении, если его ударил объект игрока, он отправил команду на коллайдер, чтобы он получил здоровье, отправил сообщение для передачи звука всем, кто мог его слышать, деактивировал столкновения, активировал анимацию, чтобы удалить графику из граф сцены, и установите таймер, чтобы восстановить себя позже. Звучит сложно, но на самом деле это не так.

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

3 голосов
/ 09 февраля 2010

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

Чрезмерное упрощение, подражающее вашему примеру, просто чтобы подчеркнуть мою точку зрения:

mainloop:
  moveEntities()
  resolveCollisions()   [objects may "disappear"/explode here]
  drawEntities()        [drawing before or after cleanEntitites() ain't an issue, a dead entity won't draw itself]
  cleanDeadEntities()

Теперь у вас есть класс Bubble:

Bubble implements Drawable {

handle( Needle needle ) {
    if ( needle collide with us ) {
        exploded = true;
    }
}

draw (...) {
   if (!exploded) {
      draw();
     }
  }
}

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

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

Так что я не согласен с вашим утверждением, которое вы написали жирным шрифтом, что «вся логика происходит в основном классе».

Это просто не правильно.

Что касается хорошего дизайна: если вы можете легко представить другой «вид» вашей игры (например, мини-карту) и если вы можете легко закодировать «идеальное воспроизведение кадра за кадром», то ваш дизайн вероятно, не так уж и плохо (то есть: записав только входные данные и время, когда они произошли, вы сможете воссоздать игру точно так, как в нее играли. Именно так Age of Empires, Warcraft 3 и т. д. делают свое воспроизведение : записываются только пользовательские данные и время, когда они произошли (поэтому файлы воспроизведения, как правило, такие крошечные)).

2 голосов
/ 23 февраля 2011

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

Как дополнение к отличному ответу Эвана Роджерса, вас могут заинтересовать следующие статьи:

http://www.gamasutra.com/blogs/MeganFox/20101208/6590/Game_Engines_101_The_EntityComponent_Model.php

http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/

2 голосов
/ 09 февраля 2010

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

Я сам выучил пару трюков.

1 голос
/ 09 февраля 2010

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

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

...