Файл: ИСТОРИЯ ВОЗНИКНОВЕНИЯ И РАЗВИТИЯ ЯЗЫКА С, С++ и Java (ЯЗЫКИ ПРОГРАММИРОВАНИЯ C, C++, Java).pdf
Добавлен: 30.03.2023
Просмотров: 147
Скачиваний: 1
Среда разработки движка UE4 предоставляет разработчику высокоуровневые библиотеки и API на языке C++, кроме того, позволяет использовать встроенный язык визуального программирования Blueprints Visual Scripting, более удобный для использования специалистами, работающими в проекте по другим задачам, например 3D художникам или дизайнерам уровней, которые не владеют навыками программирования на языке C++, а также в случае быстрого прототипирования или просто удобства использования для конкретной задачи. Такое разделение позволяет максимально эффективно распределить ресурсы, уменьшить порог вхождения для всех специалистов, участвующих в разработке и ускорить создание продукта в реальных условиях.
Для меня, как для одного из программистов команды разработки, стояла задача реализации системы разрушений объектов в реальном времени. Она должна чётко показывать переходы между состояниями объекта в зависимости от показателя его здоровья, которое выражается в условных единицах HealthPoints. Это не только красивые визуальные эффекты, но и одна из ключевых геймплей особенностей, поэтому очень важно видеть чётко данный переход, и отличать одно состояние здоровья объекта от другого. Графическое отображение объектов на сцене является индикатором, по которому игрок оценивает состояние юнитов и зданий в его управлении, и на основании этой информации принимает тактические и стратегические решения.
В документации, которая являлась основным источником информации и одновременно техническим заданием для программиста, были выделены основные требования реализации задачи:
- У каждого объекта указывается количество состояний от 0 до 5-ти включительно.
- У разных объектов могут быть разные точки перехода между состояниями.
- Для оптимизации достаточно использовать процент от HealthPoints в целочисленных значениях (integer).
- Юнит может быть не только поврежден, но и отремонтирован, т.е. может поменять свое состояние в лучшую сторону.
- У дизайнера должен быть удобный инструмент для редактирования состояний, выбора их количества и назначения атрибутов для этих состояний в редакторе Unreal Studio.
- В случае изменения состояния объекта, для визуализации будут использованы специально подготовленные 3D модель нового состояния, анимация, материал и текстуры.
- Смена состояния также сопровождается звуковыми эффектами, которые отличаются, в зависимости от улучшения или ухудшения состояния юнита.
- В случае повреждения объекта и перехода его в худшее состояние, проигрываются визуальные эффекты (ParticleEffects), которые скрывают момент подмены 3D модели и добавляют эффектности визуализации.
При первом же подходе к решению данной задачи, встал вопрос об оптимальном варианте хранения и редактировании данных, которые будут участвовать в процессе визуализации состояния объектов.
Так как одним из пунктов в документации была указана возможность редактирования данных из редактора Unreal Studio не техническими специалистами, необходимо было реализовать базу данных с интерфейсом, доступным из редактора, без необходимости открывать исходный код для внесения изменений. Поэтому мне необходимо было выбрать один из вариантов встроенных классов для хранения данных (UDataTable, UDataAsset), с готовым интерфейсом, понятным для 3D специалистов.
На данном этапе все численные характеристики объектов хранились в отдельной таблице, представленной в виде встроенного UE4 класса UDataTable.[13] Этот формат был выбран как удобный вариант для экспорта данных из таблиц MS Excel, в которых гейм-дизайнер проекта производил расчеты. Я рассматривал один из вариантов создания базы для системы разрушений в той же таблице, с целью централизованного хранения данных. Однако, после обсуждения с 3D художниками, было принято решение о разделении численных данных и данных, содержащих атрибуты визуализации для отображения разрушений, т.к. они имеют принципиально разное назначение, и разделение представляется логичным и позволяет максимально ускорить поиск, и удобство редактирования в процессе дальнейшей работы с компонентом.
Исходя из задачи, необходимо было реализовать функциональность разрушения для всех юнитов и зданий, созданных в игре. В связи с чем встал выбор между реализацией данного функционала с помощью наследования или с использованием композиции. Изучив данный вопрос, я решил спроектировать данный модуль с использованием второго варианта, как наиболее подходящего по ряду причин.
Во-первых, композиция позволяет построить более гибкую систему, которая будет готова к изменениям в процессе исполнения программы. Во-вторых, она позволит инкапсулировать объект, т.е. скрыть реализацию и предоставить только необходимый интерфейс для пользователей компонента (black-box reuse).[14] В-третьих, архитектура UE4 позволяет использовать компонентно-ориентированный подход без специальной реализации связей в объектах.[15]
Для начала, в рамках системного анализа компонента, я построил его BlackBox диаграмму для определения входных и выходных данных:
В данном представлении мы можем определить, что на входе компоненту достаточно получить от объекта информацию о HealthPoints в процентном выражении, и на основании этих данных изменить текущее состояние объекта посредством подмены 3d модели и набора визуальных и звуковых эффектов, если это соответствует правилам, заданным дизайнером в редакторе. Компонент также должен иметь доступ к атрибутам состояний объекта, назначенных в редакторе.
Информацию о текущем состоянии будет хранить в локальной переменной, в самом компоненте, и не имеет смысла хранить ее в объекте.
Все объекты содержащие DestroyingComponent будут иметь набор состояний от 100% до 0%. При этом, для каждого состояния будет определен набор атрибутов, согласно документации, достаточный для реализации всех аудио и визуальных эффектов на сцене.
На следующем этапе работы я решил описать схематически, каким образом будет работать логика внутри нашего компонента:
Далее необходимо было принять решение, какими средствами будет реализован сам компонент. Я решил максимально скрыть содержимое объекта, описать всю логику работы компонента на С++, основу структуры данных также реализовать на C++ и предоставить пользователям интерфейс, через который было бы возможно редактирование атрибутов в Unreal Studio. Такой подход также был связан с большим количеством предполагаемых вызовов операций над объектами в реальном времени, и нагрузкой на процессор и GPU, где использование C++ предпочтительнее Blueprint по параметрам быстродействия.
При проработке задачи необходимо было учитывать то, что UE4 имеет свою систему заголовочных файлов, которые формируются самим фреймворком и участвуют в “сборке мусора” встроенной системой. Данная система реализует автоматическую работу с памятью и позволяет программисту сосредоточиться на других задачах.
Поэтому я создавал все С++ файлы с помощью средств редактора, которые позволяет автоматически сформировать все необходимые связи в проекте и включить новый компонент в систему “сборки мусора”.[16]
В своей работе я придерживался общих рекомендаций по практическому программированию [17], а также стандартов написания кода Unreal Coding Standard. [18]
Пошаговый план разработки компонента системы:
2.2. Реализация структуры данных
На данном этапе я завершил планирование разработки и приступил непосредственно к реализации. Я сравнил два варианта хранилища, которые предоставляет нам UE4 и сделал выбор в пользу DataAsset, по нескольким параметрам:
- Атрибуты состояния объекта хранят в основном ассеты, назначаемые в Blueprints, и DataAsset предоставляет более удобный интерфейс для пользователя компонента, в сравнении с DataTable, в котором все хранится в виде таблицы.
- В случае перемещения DataAsset в папках проекта, все связи обновляются автоматически, и нет необходимости менять ссылку на него в коде.
- Возможность хранить сложные типы данных, в данном случае структуры, позволяет удобно организовать хранение и уменьшить вероятность случайных ошибок.
Для создания DataAsset необходимо в первую очередь добавить базовый класс С++, в котором будем описана структура данных. Добавляем класс через редактор, для корректной работы UnrealBuildTool [19] и UnrealHeaderTool [20], и задаем базовый класс DataAsset:
Внутри класса UnitAsset необходимо создать структуру данных, которая будет хранить в себе ссылку на объект и соответствующую ему структуру с набором состояний для каждого уровня HealthPoints. Так как объекты уникальны, и не используются вставки и сортировки в runtime, то логичным представляется выбор контейнера типа Map. Таким образом, идентификатор класса будет являться ключом, а список его состояний - значением этого ключа. В библиотеке UE4 предоставляются свои классы для реализации контейнеров, поэтому я буду использовать шаблонный класс TMap< >. [21]
Для набора состояний объекта также будет удобно использовать этот контейнер, но только уже с уровнем HealthPoint в виде ключа и структурой с набором атрибутов в виде значения.
Чтобы для каждого последующего элемента системы иметь конкретные именования переменных, структур и классов, я начал с создания структуры для хранения атрибутов состояния:
Здесь используется макрос USTRUCT() при объявлении структуры, и GENERATED_BODY() для корректной “сборки мусора”, также в теле структуры нам необходимо установить параметры в макросе UPROPERTY(), для отображения и возможности присваивания значения атрибутам в редакторе. [22]
Все указатели в структуре инициализируются значением nullptr, в целях предотвращения ошибок обращения к случайным участкам памяти. [23]
Необходимо также помнить о включении нужных заголовков для всех используемых классов:
В процессе разработки выяснилось, что UE4 имеет ограничение на создание вложенных контейнеров напрямую в шаблонных классах. Я пришел к решению создать структуру-обертку для набора состояний, и уже внутри этой структуры создать контейнер для хранения состояний, в котором ключом будет являться % от HealthPoints:
Это позволит вложить структуру в контейнер на следующем уровне.
Так как все объекты в UE4 наследуются от базового класса UObject, то используется такая возможность C++, как создание указателя на родительский класс, что позволит использовать данный указатель для всех объектов:
Далее, необходимо реализовать функцию получения данных из базы для конкретного объекта. Эта функция принимает на входе указатель на объект, и возвращает структуру с соответствующим списком состояний и набором атрибутов для каждого состояния. Объявляем public функцию в файле UnitAsset.h:
Здесь используется макрос UFUNCTION() с параметром BlueprintCallable, для возможности вызова этой функции из Blueprint. Для реализации этой функции я создал вспомогательную функцию, которая будет сравнивать объекты по идентификатору класса. Объявим эту функцию в разделе private:
В процессе создания объектов на сцене, идентификаторам класса добавляется суффикс вида _NN, где NN представляет собой номер ID назначенный UE4 для каждой новой сущности, поэтому реализация функции сравнения идентификаторов выглядит следующим образом:
Закомментированные строки 19 и 21 были использованы для тестирования корректности работы функции при разработке. Теперь мы можем реализовать функцию GetUnitHealthStates:
В реализации этой функции я использовал синтактические конструкции современного стандарта С++, такие как цикл перебора коллекции for ( : ) для каждого объекта в базе и идентификатор auto, для сокращения записи типов. [25] Поиск и инициализация ссылки на структуру будет происходить в компоненте DestroyingComponent. Также, все функции в базе объявляются константными, что гарантирует безопасность и защиту от каких-либо изменений в структуре. В случае попытки внесения изменений, компилятор выдаст сообщение об этом на этапе компиляции файла.
На данном этапе структура данных для компонента готова, теперь необходимо создать на ее основе DataAsset в редакторе, и протестировать корректность отображения данных в редакторе. Создаем DataAsset:
Проверяем корректность отображения данных в редакторе и заполняем тестовый набор данных для юнита:
2.3. Реализация структуры данных
После того как структура данных успешно реализована, и я убедился в том, что все данные отображаются корректно, а также, что все значения в таблице могут быть заполнены в редакторе, можно перейти к реализации самого компонента. По аналогии с UnitAsset, создается класс компонента из редактора, и наследуется от класса ActorComponent, для того чтобы использовать функционал компонентной системы UE4.