Найти в Дзене
Джулис разраб

80/20 в шаблонах разработки. Путь к ультра эффективности на примере "HARVEST"

Оглавление

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

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

1. Паттерн «Наблюдатель» (Observer): Держим всех в курсе (без хаоса)

Представьте себе оживленную систему квестов в игре. Когда квест принимается, обновляется (например, «Собрано 5/10 трав») или завершается, различные части игры должны об этом узнать: пользовательский интерфейс должен обновить журнал квестов, может проиграться звуковой сигнал, или другая игровая система может разблокировать новую область.

Паттерн «Наблюдатель» предлагает чистое решение. Он определяет зависимость «один ко многим» между объектами таким образом, что когда один объект («Субъект» или «Наблюдаемый») изменяет свое состояние, все его зависимые объекты («Наблюдатели») автоматически уведомляются и обновляются.

В коде нашей системы квестов UQuestSubsystem выступает в роли Субъекта, а различные элементы пользовательского интерфейса или другие игровые системы могут выступать в роли Наблюдателей. Мы ясно видим это благодаря использованию делегатов в QuestSubsystem.h:

-2

А Наблюдатель, такой как AQuestObserver, будет привязываться к этим делегатам:

-3

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

2. Доступ в стиле «Одиночки» (Singleton): Подсистема GameInstance (GameInstance Subsystem)

Хотя классический паттерн «Одиночка» (класс, который гарантирует существование только одного своего экземпляра и предоставляет глобальную точку доступа к нему) иногда может вызывать споры из-за проблем с тестируемостью и сильной связанностью, его цель – предоставление единого, доступного менеджера для определенной задачи – часто оправдана.

В Unreal Engine UGameInstanceSubsystem предоставляет управляемый способ достижения аналогичной глобальной доступности для сервисов, которые должны сохраняться между загрузками уровней на протяжении всего игрового сеанса. Наш UQuestSubsystem является одним из таких примеров:

-4

Практическое применение: Это делает UQuestSubsystem легкодоступным из различных частей игрового кода (Actors, Components и т.д.) без необходимости постоянно передавать ссылки или прибегать к более рискованным глобальным статическим экземплярам. Unreal Engine управляет его жизненным циклом.

3. Проектирование, управляемое данными (Data-Driven Design): Отделение данных от логики

Хотя это и не паттерн из «Банды Четырех», проектирование, управляемое данными, является важнейшей архитектурной практикой. Вместо жесткого кодирования деталей квеста (названий, целей, наград) непосредственно в классах C++, эти детали часто хранятся во внешних ресурсах данных, таких как таблицы данных (Data Tables) Unreal Engine.

Код явно на это намекает:

-5

Преимущества: Дизайнеры могут создавать, изменять и балансировать квесты, редактируя таблицу данных, без необходимости изменять код C++ и перекомпилировать. Это значительно ускоряет итерацию и создание контента. Это также упрощает локализацию.

4. Паттерн «Фабричный метод» (Factory Method) (или его зародышевая форма): Создание объектов из данных

Паттерн «Фабричный метод» определяет интерфейс для создания объекта, но позволяет подклассам решать, какой класс инстанцировать. Более общая концепция заключается в наличии централизованного механизма создания, который может производить различные типы объектов на основе входных данных.

В AQuestTriggerBox::InitializeQuestInstances() создание QuestInstance с использованием NewObject<UQuestBase>(this, QuestBaseClass, ...) с последующим SetupFromQuestData предполагает движение в этом направлении. Если бы QuestBaseClass можно было динамически определять из QuestData (например, если бы структура FQuestData содержала TSubclassOf<UQuestBase>), это больше походило бы на Фабрику.

-6

Практическое применение: Это позволяет системе создавать различные типы квестов (например, UCollectItemsQuest, UGoToThePointQuest, UEscortQuest) из одного и того же механизма запуска, управляемого данными, просто указав желаемый класс квеста в таблице данных. Файл QuestBase.cpp действительно определяет UGoToThePointQuest и UCollectItemsQuest, наследующиеся от UQuestBase, что делает этот паттерн весьма применимым.

В QuestBase.cpp мы видим логику для UCollectItemsQuest:

-7

Эта логика подтверждает идею о том, что разные типы квестов (инстанцированные фабрикоподобным механизмом) будут иметь собственные реализации HandleRequirements (полиморфизм).

-8

Ценность паттернов

Паттерны проектирования – это не слепое применение решения. Это понимание проблемы и выбор признанной, эффективной структуры, которая ее решает. В предоставленном коде системы квестов:

  • Паттерн «Наблюдатель» (через делегаты) обеспечивает слабосвязанное взаимодействие для изменений состояния квеста.
  • Подсистемы GameInstance предлагают управляемую точку доступа в стиле «Одиночки» для глобальных систем, таких как управление квестами.
  • Проектирование, управляемое данными, делает систему гибкой и удобной для дизайнеров.
  • Паттерн «Фабричный метод» (или аналогичная логика создания) может использоваться для инстанцирования различных типов квестов на основе данных.

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

-9