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’а в морпехе. Его обычно тоже пересоздаю при необходимости.