High-level architectural framework for Unity Engine
Фреймворк, который я сделал для личного пользования. Используется на проекте Dungeon Hero
Позволяет разбивать игровую логику на "модули". Каждый модуль содержит в себе все необходимые зависимости. Причем как зависимости на уровне кода, так и в виде каких-либо ассетов (для подгрузки используются Addressables).
Также облегчает работу с ecs-фреймворком Morpeh, поскольку инкапсулирует в себе логику регистрации и порядка вызова всех ecs-систем.
При этом не диктует определенного подхода по работе с самим собой. Модули могут быть любого размера. Можно выстраивать иерархии из модулей. Нет привязки к сценам в unity, соответственно, можно выстраивать работу со сценами исходя из надобностей конкретного проекта.
-
ECS-фреймворк Morpeh
-
Unity Addressables (добавятся автоматически)
-
DI-контейнер VContainer
Ссылки для быстрого добавления через Package Manager:
-
https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer
-
https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask
-
Установить все необходимые зависимости
-
Добавить ModuleFramework как git-зависимость через Package Manager в Unity:
https://github.com/artUSUN/ModuleFramework.git
Намеренно помещаю этот раздел повыше, чтобы показать, что он существует. При ознакомлении с возможностями фреймворка его можно смело пропустить, но важно вернуться к нему при интеграции.
Чтобы все заработало, нужно сделать два важных пункта:
- Написать реализацию для интерфейса
IEcsSystemsOrderRepository
- Зарегистрировать в DI-контейнере класс
EcsSystemsOrderResolver
и реализацию интерфейсаIEcsSystemsOrderRepository
из первого пункта
В текущем репозитории в папке Framework.Examples
можно найти два класса: EcsSystemsOrderRepository
и ModuleFrameworkInstaller
.
Первый класс - это предлагаемая мною реализация для интерфейса IEcsSystemsOrderRepository
. Можно скопировать ее в свой код и использовать, добавляя в словарь Order
системы из всей игры, которым необходим переопределенный порядок выполнения.
Второй класс - это пример Installer’a для регистрации обоих вышеупомянутых классов. Можно скопировать себе весь класс и вызвать метод Install
из корневого модуля.
При желании можно сделать собственную реализацию IEcsSystemsOrderRepository
и/или регистрировать его и EcsSystemsOrderRepository
в разных модулях. Это может быть полезно, например, если вы хотите разбить каждый модуль на отдельную asmdef сборку. Или если хотите держать порядок выполнения системы поближе к каждому отдельному модулю.
Я обычно держу этот список в одном месте, поскольку:
- Удобно видеть и менять в одном месте порядок выполнения для всех ecs-систем в игре в одном месте.
- Систем, для которых действительно будет важен порядок выполнения, скорее всего будет не более 10-20% от общего кол-ва, а значит словарь не разрастется до совсем космических размеров.
Warning
Для каждой системы с заданным порядком выполнения должен быть свой уникальный порядок. Одинаковый порядок для двух разных систем использовать нельзя, ровно как использовать индексы с 0 до IEcsSystemsOrderRepository.GetLastDefaultOrder();
. Если реализуете множество IEcsSystemsOrderRepository
, то возвращайте в методе GetLastDefaultOrder()
общую для всех константу.
Модуль создается путем наследования от класса Module
или ModuleWithSettings<TSettings>
. Разница заключается в том, что необходимы ли какие-либо зависимости в виде ассетов (настройки) для этого модуля или нет.
- Наследование от
Module
, когда настройки не нужны. - От
ModuleWithSettings<TSettings>
, когда нужны. По-сутиModuleWithSettings<TSettings>
сам наследуется отModule
и инкапсулирует в себе работу с абстрактной SO-шкой TSettings, которая будет грузиться из Addressable.
Обязательных методов для реализации у обоих вариантов нет, переопредляйте только то, что будет нужно:
-
OnLoad()
,OnActivate()
,OnDeactivate()
,OnUnload()
- вызываются на соответствующее действие с модулем. Возвращаемый тип во всех случаяхUniTask
, что позволяет выполнять внутри асинхронную логику и не блокировать основной поток долгой операцией. -
UniTask BeforeScopeCreate()
- асинхронный метод, который вызывается перед сборкой DI-контейнера. Нужен, если хочется выполнить какую-то логику и затем зарегистрировать результат в контейнере. По такому принципу и работаетModuleWithSettings<TSettings>
. В случае, если метод вызывается при наследовании отModuleWithSettings<TSettings>
- необходимо обязательно вызвать базовый класс. -
void InstallDependenciesToModule(IContainerBuilder builder)
- метод для регистрации классов и инстансов в DI-контейнере. При наследовании отModuleWithSettings<TSettings>
обязательно вызывать базовый метод. Либо можно переопределить методInstallDependencies(IContainerBuilder builder)
, который работает абсолютно аналогично, но без необходимости вызывать базовый метод.
Основные способы регистрации в DI-контейнере:
builder.Register<T>(Lifetime.Singleton);
builder.Register<ISystem, TSystem>(Lifetime.Singleton);
builder.RegisterInstance(TInstance);
Другие способы и прочие подробности см. в документации VContainer
- Для загрузки модуля необходимо вызвать статический метод:
UniTask<TModule> Module.Load<TModule>(Module parent, bool activateAfterLoad = false)
Параметр parent
может быть null
. Тогда модуль будет являться корневым. На загрузке модуля собирается DI-контейнер и загружаются необходимые ресурсы.
- Активация, деактивация и выгрузка модуля происходит путем вызова соответствующих методов у конкретного экземпляра класса
Module
. При вызове деактивации/выгрузке родителей - все активные/загруженные модули-наследники будут выключены и/или выгружены.
При включении модуля начинают работать ecs системы. При выключении, соответственно, перестают.
Для всех классов, зарегистрированных в модуле, можно определить дополнительную логику путем реализации соответствующего интерфейса (IModuleLoadListener
, IModuleUnloadListener
, IModuleActivationListener
, IModuleDeactivationListener
)
Здесь нет четких рамок или правил. Опишу, как я обычно его использую:
- Создаю один корневой General-модуль. Он загружается и запускается один раз на старте и никогда не выключается/выгружается. Такой global-контекст на всю игру.
- Далее выделяю два модуля: Core и Meta. В них при необходимости будут запускаться другие модули.
- Пишу игровую логику, деля ее по фичам. Модуль инпута, модуль нанесения урона, модуль подключения к серверу, модуль боевого пропуска, модуль инвентаря и т.д.
- Модули фичей не обязательно запускать на загрузке модуля-родителя. Например можно не грузить боевой пропуск, пока игрок не нажал на кнопку боевого пропуска. Или не грузить модуль езды на драконе, если игрок еще не получил дракона.
- Сцена у меня обычно вообще является пустой, все необходимое динамически создается. При этом смена сцены присутствует, чтобы не менеджерить удаление объектов. Например по окончанию сессии в core и переходу в meta. То же самое касается и World’а в морпехе. Его обычно тоже пересоздаю при необходимости.