Тактика перемещения логики рендеринга из класса GameObject

10

При создании игр вы часто создаете следующий игровой объект, от которого наследуются все сущности:

public class GameObject{
    abstract void Update(...);
    abstract void Draw(...);
}

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

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

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

Возможно, лучшим способом было бы полностью удалить метод Draw из класса GameObject и создать класс Renderer. GameObject по-прежнему должен содержать некоторые данные о его визуальных элементах, например, какую модель он должен представлять и какие текстуры должны быть нарисованы на модели, но как это сделать, будет оставлено для визуализации. Однако при рендеринге часто встречается много граничных случаев, поэтому, хотя это устранит тесную связь между GameObject и Renderer, Renderer все равно должен был бы знать все игровые объекты, которые делали бы его жирным, все знали и тесно связаны. Это нарушило бы немало хороших практик. Возможно, Data-Oriented-Design справится с этой задачей. Игровые объекты, безусловно, будут данными, но как будет управлять этим средством визуализации? Я не уверен.

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

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

  • Нет логики рендеринга в игровом объекте
  • Слабая связь между игровыми объектами и движком рендеринга
  • Не все знающие рендерер
  • Предпочтительно переключение во время выполнения между движками рендеринга

Идеальная настройка проекта - это отдельная «игровая логика» и проект логики рендеринга, которые не должны ссылаться друг на друга.

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

Рой Т.
источник

Ответы:

7

Быстрый первый шаг к расцеплению:

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

Renderer отвечает за загрузку и поддержание ссылок «human_male» и передачу обратно объектам дескриптора для использования.

Пример в ужасном псевдокоде:

GameObject( initialization parameters )
  me.render_handle = Renderer_Create( parameters.render_string )

- elsewhere
Renderer_Create( string )

  new data handle = Resources_Load( string );
  return new data handle

- some time later
GameObject( something happens to me parameters )
  me.state = something.what_happens
  Renderer_ApplyState( me.render_handle, me.state.effect_type )

- some time later
Renderer_Render()
  for each renderable thing
    for each rendering back end
        setup graphics for thing.effect
        render it

- finally
GameObject_Destroy()
  Renderer_Destroy( me.render_handle )

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

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

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

Патрик Хьюз
источник
Это очень отличается от того, что я делал раньше, похоже, у него есть небольшой потенциал. К счастью, я не ограничен каким-либо существующим движком, поэтому я могу просто возиться. Я также посмотрю термины, которые вы упомянули, хотя инъекция зависимости всегда причиняет боль моему мозгу: P.
Рой Т.
2

Что я сделал для своего собственного движка - это сгруппировал все в модули. Итак, у меня есть свой GameObjectкласс, и он содержит ручку для:

  • ModuleSprite - рисование спрайтов
  • ModuleWeapon - огнестрельное оружие
  • ModuleScriptingBase - скриптинг
  • ModuleParticles - эффекты частиц
  • ModuleCollision - обнаружение столкновений и реагирование

Итак, у меня есть Playerкласс и Bulletкласс. Оба являются производными от GameObjectи добавляются в Scene. Но Playerимеет следующие модули:

  • ModuleSprite
  • ModuleWeapon
  • ModuleParticles
  • ModuleCollision

И Bulletимеет эти модули:

  • ModuleSprite
  • ModuleCollision

Этот способ организации вещей избегает «Алмаза Смерти», где у вас есть a Vehicle, a VehicleLandи a, VehicleWaterа теперь вы хотите a VehicleAmphibious. Вместо этого у вас есть Vehicleи он может иметь ModuleWaterи ModuleLand.

Дополнительный бонус: вы можете создавать объекты, используя набор свойств. Все, что вам нужно знать, это базовый тип (Player, Enemy, Bullet и т. Д.), А затем создавать дескрипторы для модулей, которые вам нужны для этого типа.

В моей сцене я делаю следующее:

  • Позвони Updateдля всех GameObjectручек.
  • Сделайте проверку столкновения и ответ столкновения для тех, у кого есть ModuleCollisionручка.
  • Позвоните UpdatePostдля всех GameObjectручек, чтобы сообщить об их окончательном положении после физики.
  • Уничтожьте объекты, у которых установлен флаг.
  • Добавить новые объекты из m_ObjectsCreatedсписка в m_Objectsсписок.

И я мог бы организовать это дальше: по модулям, а не по объектам. Затем я бы отображал список ModuleSprite, обновлял кучу ModuleScriptingBaseи делал коллизии со списком ModuleCollision.

knight666
источник
Звучит как композиция по максимуму! Очень хорошо. Я не вижу много рендеринга конкретных советов здесь, хотя. Как вы справляетесь с этим, просто добавляя различные модули?
Рой Т.
О да. Это обратная сторона этой системы: если у вас есть особые требования GameObject(например, способ визуализации «змеи» спрайтов), вам нужно либо создать дочерний элемент ModuleSpriteдля этой конкретной функции ( ModuleSpriteSnake), либо добавить новый модуль в целом ( ModuleSnake). К счастью, они всего лишь указатели, но я видел код, где GameObjectбуквально все, что мог сделать объект.
knight666