У меня возник вопрос об игровой архитектуре: как лучше связать друг с другом разные компоненты?
Я действительно извиняюсь, если этот вопрос уже задавался миллион раз, но я не могу найти ничего с той информацией, которую я ищу.
Я пытался создать игру с нуля (C ++, если это имеет значение) и наблюдал за некоторым вдохновляющим игровым программным обеспечением с открытым исходным кодом (Super Maryo Chronicles, OpenTTD и другие). Я заметил, что во многих игровых проектах повсеместно используются глобальные экземпляры и / или синглтоны (для таких вещей, как очереди рендеринга, менеджеры сущностей, менеджеры видео и т. Д.). Я пытаюсь избегать глобальных экземпляров и синглетонов и создавать движок, который настолько слабо связан, насколько возможно, но я сталкиваюсь с некоторыми препятствиями, которые связаны с моей неопытностью в эффективном дизайне. (Часть мотивации для этого проекта состоит в том, чтобы заняться этим :))
Я построил дизайн, в котором у меня есть один главный GameCore
объект, члены которого аналогичны глобальным экземплярам, которые я вижу в других проектах (т. Е. У него есть менеджер ввода, видео менеджер, GameStage
объект, который контролирует все сущности и игру). за какой этап загружен в данный момент и т. д.). Проблема в том, что, поскольку в GameCore
объекте все централизовано, у разных компонентов нет простого способа взаимодействия друг с другом.
Например, если взглянуть на Super Maryo Chronicles, всякий раз, когда компонент игры должен обмениваться данными с другим компонентом (т. Е. Вражеский объект хочет добавить себя в очередь рендеринга, которая будет отрисовываться на этапе рендеринга), он просто обращается к глобальный экземпляр.
Мне нужно, чтобы мои игровые объекты передавали соответствующую информацию обратно GameCore
объекту, чтобы GameCore
объект мог передавать эту информацию другим компонентам системы, которые в ней нуждаются (т. Е. Для вышеуказанной ситуации, каждому вражескому объекту). будет передавать свою информацию рендеринга обратно GameStage
объекту, который соберет все это и передаст его обратно GameCore
, что, в свою очередь, передаст ее диспетчеру видео для рендеринга). Это похоже на действительно ужасный дизайн как таковой, и я пытался придумать решение этой проблемы. Мои мысли о возможных конструкциях:
- Глобальные экземпляры (дизайн Super Maryo Chronicles, OpenTTD и т. Д.)
- Наличие
GameCore
объекта в качестве посредника, через которого все объекты взаимодействуют (текущий дизайн описан выше) - Присвойте указатели компонентов всем другим компонентам, с которыми им нужно будет общаться (т. Е. В приведенном выше примере с Maryo вражеский класс будет иметь указатель на видеообъект, с которым ему нужно общаться)
- Разбейте игру на подсистемы - например, в
GameCore
объекте есть управляющие объекты, которые управляют связью между объектами в их подсистеме. - (Другие опции? ....)
Я полагаю, что вариант 4, представленный выше, является лучшим решением, но у меня возникли некоторые проблемы при его разработке ... возможно, потому, что я думал о проектах, которые я видел, которые используют глобальные переменные. Такое ощущение, что я беру ту же проблему, которая существует в моем текущем проекте, и копирую ее в каждой подсистеме, только в меньшем масштабе. Например, GameStage
объект, описанный выше, является своего рода попыткой сделать это, но GameCore
объект все еще участвует в процессе.
Кто-нибудь может предложить какие-либо советы по дизайну здесь?
Благодарность!
источник
Ответы:
В наших играх для организации глобальных данных мы используем шаблон проектирования ServiceLocator . Преимущество этого шаблона по сравнению с шаблоном Singleton состоит в том, что реализация ваших глобальных данных может измениться во время выполнения приложения. Кроме того, ваши глобальные объекты также могут быть изменены во время выполнения. Другое преимущество состоит в том, что легче управлять порядком инициализации ваших глобальных объектов, что очень важно, особенно в C ++.
например (код C #, который можно легко перевести на C ++ или Java)
Допустим, у вас есть интерфейс рендеринга, который имеет некоторые общие операции для рендеринга.
И что у вас есть реализация по умолчанию рендеринга Backend
В некоторых проектах кажется законным иметь возможность доступа к бэкэнду рендеринга глобально. В шаблоне Singleton это означает, что каждая реализация IRenderBackend должна быть реализована как уникальный глобальный экземпляр. Но использование шаблона ServiceLocator не требует этого.
Вот как:
Чтобы получить доступ к вашему глобальному объекту, вам нужно сначала его инициализировать.
Просто чтобы продемонстрировать, как реализации могут меняться во время выполнения, допустим, что в вашей игре есть мини-игра, в которой рендеринг является изометрическим, а вы реализуете IsometricRenderBackend .
При переходе из текущего состояния в состояние мини-игры вам просто нужно изменить глобальный сервер рендеринга, предоставляемый локатором сервиса.
Еще одним преимуществом является то, что вы также можете использовать нулевые сервисы. Например, если у нас была служба ISoundManager и пользователь хотел отключить звук, мы могли бы просто реализовать NullSoundManager, который ничего не делает при вызове его методов, поэтому, установив объект службы ServiceLocator в объект NullSoundManager, мы могли бы достичь этот результат почти без труда.
Подводя итог, иногда бывает невозможно исключить глобальные данные, но это не значит, что вы не можете организовать их должным образом и объектно-ориентированным способом.
источник
std::unique_ptr<ISomeService>
.Есть много способов создать игровой движок, и все сводится к предпочтениям.
Чтобы уйти от основ, некоторые разработчики предпочитают проектировать их как пирамиду, где есть некоторый верхний базовый класс, часто называемый классом ядра, ядра или платформы, который создает, владеет и инициализирует ряд подсистем, таких как как аудио, графика, сеть, физика, ИИ, а также задачи, сущности и управление ресурсами. Как правило, эти подсистемы предоставляются вам этим базовым классом, и обычно вы передаете этот базовый класс своим собственным классам в качестве аргумента конструктора, где это уместно.
Я считаю, что вы на правильном пути, думая о варианте № 4.
Имейте в виду, когда речь идет о самой коммуникации, это не всегда подразумевает прямой вызов функции. Существует много косвенных способов общения, будь то с помощью какого-либо косвенного метода использования
Signal and Slots
или использованияMessages
.Иногда в играх важно разрешить асинхронное выполнение действий, чтобы игровой цикл двигался как можно быстрее, чтобы частота кадров была невооруженным глазом. Игрокам не нравятся медленные и изменчивые сцены, и поэтому мы должны найти способы, чтобы все было так, как нужно, но логика должна идти, но под контролем и в порядке. Хотя асинхронные операции имеют свое место, они не являются ответом и для каждой операции.
Просто знайте, что у вас будет сочетание как синхронной, так и асинхронной связи. Выберите подходящее, но знайте, что вам необходимо поддерживать оба стиля в своих подсистемах. Разработка поддержки для обоих будет хорошо служить вам в будущем.
источник
Вы просто должны убедиться, что нет обратных или циклических зависимостей. Например, если у вас есть класс
Core
, и уCore
него естьLevel
, и уLevel
него есть списокEntity
, дерево зависимостей должно выглядеть так:Итак, учитывая это начальное дерево зависимостей, вы никогда не должны
Entity
зависеть отLevel
илиCore
иLevel
никогда не должны зависеть отCore
. ЕслиLevel
илиEntity
нужно иметь доступ к данным, которые находятся выше в дереве зависимостей, они должны быть переданы в качестве параметра по ссылке.Рассмотрим следующий код (C ++):
Используя эту технику, вы можете увидеть , что каждый
Entity
имеет доступ кLevel
, иLevel
имеет доступ кCore
. Обратите внимание, что каждыйEntity
хранит ссылку на один и тот жеLevel
потерянную память. Заметив это, вы должны спросить, действительно ли каждомуEntity
нужен доступ кLevel
.По моему опыту, есть либо А) Действительно очевидное решение, позволяющее избежать обратных зависимостей, либо Б) Невозможно избежать глобальных экземпляров и синглетонов.
источник
Итак, в принципе, вы хотите избежать глобального изменчивого состояния ? Вы можете сделать его либо локальным, неизменным, либо вообще не государственным. Последние наиболее эффективны и гибки, имо. Это известно как скрытие iplementation.
источник
Вопрос на самом деле, кажется, заключается в том, как уменьшить связь без ущерба для производительности. Все глобальные объекты (сервисы) обычно образуют своего рода контекст, который может изменяться во время выполнения игры. В этом смысле шаблон локатора службы распределяет разные части контекста по разным частям приложения, что может быть или не быть тем, что вы хотите. Другой реальный подход состоит в том, чтобы объявить такую структуру:
И передать его как необработанный необработанный указатель
sEnvironment*
. Здесь указатели указывают на интерфейсы, поэтому связывание уменьшается аналогичным образом по сравнению с локатором службы. Тем не менее, все услуги находятся в одном месте (что может или не может быть хорошо). Это просто другой подход.источник