🎮 Что за модули в Unreal Engine и почему я должен о них париться?
Рассказываем о концепции модулей в Unreal Engine, зависимостях между ними, а также о том, как реализовывать модули, собирать их, загружать и использовать.
Основные причины разделения кода на модули:
- Переиспользование. Модули можно безболезненно переносить в другие проекты и применять там.
- Инкапсуляция. Модули позволяют скрыть подробности реализации какой-либо функциональности от внешнего кода.
- В готовую игру попадут только те модули, которые реально используются, а значит лишние мегабайты обойдут билд стороной.
- С помощью модулей можно устанавливать момент времени, в который будет загружен код этих самых модулей.
Код модулей
Перед тем как заниматься кодом модулей, необходимо рассказать, где он может быть расположен. Это зависит от предназначения модуля. Список потенциальных мест представлен ниже, где <PluginName>
– имя плагина, <ModuleName>
– имя модуля, <Game>
– директория игры, <Groups>
– опциональные директории для группировки модулей или плагинов:
- Если модуль является частью движка:
Engine/Source/<Groups>/<ModuleName>
. - Если модуль является частью плагина движка:
Engine/Plugins/<Groups>/<PluginName>/Source/<Groups>/<ModuleName>
. - Если модуль является частью игрового плагина:
<Game>/Plugins/<Groups>/<PluginName>/Source/<Groups>/<ModuleName>
. - Если модуль является частью игры:
<Game>/Source/<Groups>/<ModuleName>
.
Каждая из таких директорий может содержать две поддиректории: Public
и Private
. В Public
должны находиться все хедеры, которые предназначены для использования другими модулями. В Private
– всё остальное (файлы реализации и скрытые от внешнего кода хедеры). Чем меньше хедеров находится в Public
, тем быстрее будет проходить процесс компиляции и линковки. Используйте также forward declaration [1], где это возможно.
На изображении ниже приведена структура одного из модулей (ActorPickerMode
) движка:
Public
/Private
можно обнаружить директорию Classes
. Когда-то она являлась единственным местом, где могли быть объявлены классы-наследники UObject
, но теперь она deprecated и не несёт никакой дополнительной функциональности.В одной из таких директорий (обычно в Private
) должен находиться файл с реализацией модуля, по всеобщему соглашению называющийся <ModuleName>Module.cpp
, где <ModuleName>
– имя модуля. Его минимальное содержимое таково:
- Соответствующий хедер (
<ModuleName>Module.h
) может быть пустым. - Первым аргументом макроса
IMPLEMENT_MODULE
является имя класса, реализующего модуль. Это может быть ваш собственный класс (об этом варианте мы поговорим чуть позже) или “заглушка”, такая какFDefaultModuleImpl
[2]. - Второй аргумент – имя модуля. Обратите внимание, что макрос
IMPLEMENT_MODULE
должен быть расположен после всех объявлений в данном файле. - Если модуль является игровым, тогда вместо
IMPLEMENT_MODULE
необходимо использоватьIMPLEMENT_GAME_MODULE
. Если мы говорим о главном игровом модуле (primary game module), т.е. модуле, в котором движок располагает основной код игры, то необходимо использовать особый макросIMPLEMENT_PRIMARY_GAME_MODULE
.
Основной тип модулей, который вам, вероятно, придется создавать – это модули для плагинов, и они как раз используют IMPLEMENT_MODULE
.
Public
/Private
. Однако иногда от главного игрового модуля зависит модуль, содержащий тесты для него – в таком случае разделение имеет смысл.При добавлении класса C++ через редактор движка пользователю будет предложено выбрать, в какой модуль добавить данный класс:
Сразу после загрузки модуля движком создаётся объект класса, реализующего модуль (того самого класса, который указывается в IMPLEMENT_MODULE
), а после выгрузки данный объект уничтожается. Реализующий модуль класс должен быть унаследован от IModuleInterface
[3], который содержит много полезных методов. Мы остановимся на двух самых важных:
void StartupModule()
вызывается сразу после того, как DLL модуля была загружена и объект модуля был создан;void ShutdownModule()
вызывается перед тем, как DLL будет выгружена. После выполнения этого метода модуль больше недоступен к использованию.
Разумеется, модуль не должен быть ограничен только одним (главным) классом. Этот класс лишь служит точкой доступа к модулю, а также позволяет реализовать некоторую особую функциональность, связанную с движком. Вся же бизнес-логика должна быть вынесена в отдельные классы.
Чтобы получить доступ к модулю из любого другого модуля, необходимо вызвать следующий метод (не забудьте включить хедер “Modules/ModuleManager.h”
[4]):
Этот метод возвращает модуль с именем <ModuleName>
, если он загружен, а если нет – предварительно загружает. О других способах загружать модули мы поговорим далее в статье.
По умолчанию классы и методы из вашего модуля недоступны для использования внешним кодом, даже если хедеры с ними расположены в Public
. Чтобы это исправить, существует несколько путей:
- Отметить необходимые методы с помощью макроса
<MODULENAME>_API
, как в коде ниже:
- Отметить класс с помощью макроса
<MODULENAME>_API
, в таком случае все методы класса становятся доступными для внешнего кода:
- Использовать спецификатор
MinimalAPI
. Помеченные таким спецификатором классы можно наследовать, использовать при приведении типов, например,Cast<T>
[5]. Также на объектах таких классов можно вызывать inline-методы.
Если не сделать ничего из этого, то при попытке внешнего кода использовать методы/классы из вашего модуля возникнут ошибки линковки.
Сборка модулей
.Build.cs
- [7] и .Target.cs
- [8]. Файл SLN нужен лишь для удобства пользователя при работе с кодом через поддерживающие этот формат IDE (например, Microsoft Visual Studio или JetBrains Rider for Unreal Engine).К каждому модулю прилагается <ModuleName>.Build.cs
– файл, в котором описываются особенности модуля, вроде зависимостей от других модулей. Любой модуль зависит от Core
[9], так как использованный нами ранее ModuleManager.h
находится именно там.
Зависимости позволяют модулям сообщать системе сборки UE о том, что для работы им требуются какие-либо другие модули.
Зависимости бывают двух видов:
- Приватные: добавляются через список
PrivateDependencyModuleNames
. - Публичные: добавляются через список
PublicDependencyModuleNames
.
Пример файла .Build.cs
:
Добавление в любой из этих списков позволит вашему модулю использовать публичные хедеры из зависимого модуля (хедеры, включенные в список PublicIncludePaths
), а также сообщит линкеру, что файлы .cpp
зависимого модуля должны линковаться с вашим модулем.
Разница же между приватными и публичными зависимостями проявляется, когда в цепочке зависимостей становится больше двух модулей:
Таким образом, при приватной зависимости модуля B
от модуля C
модуль A
, зависящий от B
(не важно как – приватно или публично), не будет видеть публичные хедеры C
, в то время как при публичной – будет. Однако линковка A
с C
не произойдёт в любом случае.
Неправильно указанные зависимости выльются в ошибки компиляции и/или линковки.
Обратите внимание, что будут скомпилированы только те модули, которые находятся в дереве зависимостей. Если же от какого-то из модулей (например, от главного игрового модуля) ничего не зависит, но его необходимо скомпилировать, такой модуль нужно добавить в файл .Target.cs
в список ExtraModuleNames
:
Как использовать модули
Прежде чем использовать функциональность какого-то модуля, его необходимо загрузить. Об одном из способов загрузки модулей мы уже говорили выше – посмотрим на три других:
- Указать модуль в разделе
Modules
в файле.uproject
. - Указать модуль в разделе
Modules
в файле.uplugin
. - Указать плагин в разделе
Plugins
в файле.uproject
или.uplugin
.
Синтаксис для первых двух вариантов идентичен:
Поле Name
предназначено для имени модуля. Type
описывает, в каких билдах модуль будет загружен: например, Runtime
– модуль будет загружен всегда, за исключением случая, когда сборка – программа (Program Target); Editor
– только при сборке с редактором и т.д. Полный список возможных типов доступен в документации.
Developer
помечен как deprecated, вместо него необходимо использовать UncookedOnly
.LoadingPhase
описывает, когда во время загрузки движка будет загружен модуль: например, Default
– в момент инициализации движка, PostEngineInit
– после завершения инициализации. Полный список возможных фаз в документации.
Тип модуля и фаза загрузки не единственные свойства модулей, которые можно задать. Обратитесь к документации за полным списком.
При использовании третьего способа загрузки для каждого модуля плагина будет использован соответствующий дескриптор из файла .uplugin
. Заметьте, что модули всех игровых плагинов, если соответствующие плагины не были отключены в настройках, будут загружены, даже если не указаны в .uproject
.
Вывод
Весь Unreal Engine, как и любая игра на нём, состоит из модулей, каждый из которых представляет собой группу классов. Основные причины использования модулей – переиспользование и инкапсуляция. Сборка модуля управляется через соответствующий файл .Build.cs
. Модули могут зависеть друг от друга и загружать друг друга. В следующей статье мы более подробно расскажем о создании и использовании плагинов.
Источники
- https://en.cppreference.com/w/cpp/language/class
- https://docs.unrealengine.com/4.26/en-US/API/Runtime/Core/Modules/FDefaultModuleImpl/
- https://docs.unrealengine.com/4.26/en-US/ProgrammingAndScripting/GameplayArchitecture/Gameplay/
- https://docs.unrealengine.com/4.26/en-US/API/Runtime/Core/Modules/IModuleInterface/
- https://docs.unrealengine.com/4.26/en-US/API/Runtime/Core/Modules/FModuleManager/
- https://docs.unrealengine.com/4.27/en-US/API/Runtime/CoreUObject/Templates/Cast/
- https://docs.unrealengine.com/4.26/en-US/ProductionPipelines/BuildTools/UnrealBuildTool/ModuleFiles/
- https://docs.unrealengine.com/4.26/en-US/ProductionPipelines/BuildTools/UnrealBuildTool/TargetFiles/
- https://docs.unrealengine.com/4.26/en-US/API/Runtime/Core/
- https://docs.unrealengine.com/4.27/en-US/API/Runtime/Projects/EHostType__Type/
- https://docs.unrealengine.com/4.27/en-US/API/Runtime/Projects/ELoadingPhase__Type/
- https://docs.unrealengine.com/4.27/en-US/API/Runtime/Projects/FModuleDescriptor/