Dependency Injection (Zenject)
Эта страница описывает внутренний код движка, доступный исключительно лицензиатам р едакций Source и Studio.
Zenject связывает через эти границы модулей, и только в одном направлении: установщик в Core никогда ничего не разрешает из Online.
MCE использует Zenject (через форк Extenject) для внедрения зависимостей. Эта страница охватывает соглашения, паттерны и все установщики движка, чтобы вы могли расширить граф DI своими собственными системами.
Почему Zenject?
MCE — это большой модульный движок, где такие системы, как битвы, монстры, сохранения, аудио и UI, должны ссылаться друг на друга без жёсткой связанности. Zenject предоставляет:
- Внедрение через конструктор для MonoBehaviour (через
[Inject]вConstruct()) без использованияFindObjectOfTypeили синглтонов. - Установщики на основе ScriptableObject, которые можно настраивать в Inspector и заменять между проектами.
- Паттерн Factory для создания экземпляров префабов во время выполнения с автоматическим внедрением.
- Контейнеры с областью видимости (ProjectContext vs SceneContext) для чистой совместной работы глобальных и посистемных систем.
ProjectContext
Глобальные привязки MCE находятся в префабе ProjectContext по пути Assets/Resources/ProjectContext.prefab. Zenject автоматически загружает этот префаб перед запуском любой сцены. Все активы ScriptableObjectInstaller, указанные в этом префабе, устанавливаются один раз и сохраняются на всё время работы приложения.
Здесь привязываются основные системы движка: база данных монстров, система сохранения, аудио, ввод, рендеринг, запускатель битв, менеджер эволюции, локализация и многое другое.
Паттерн Installer
MCE использует почти исключительно подклассы ScriptableObjectInstaller. Каждый установщик — это актив ScriptableObject (создаваемый через CreateAssetMenu), который хранит сериализованные ссылки на устанавливаемые им системы.
Базовая конвенция: LazySingletonScriptableInstaller
Большинство установщиков MCE расширяют удобный базовый класс:
public class LazySingletonScriptableInstaller<T> : ScriptableObjectInstaller
where T : ScriptableObject
{
[SerializeField]
protected T Reference;
public override void InstallBindings()
{
Container.QueueForInject(Reference);
Container.Bind<T>().FromInstance(Reference).AsSingle().Lazy();
}
}
Это делает три вещи:
- Ставит ScriptableObject в очередь на внедрение, чтобы его собственные поля
[Inject]были разрешены. - Привязывает его как синглтон, чтобы любой класс, объявляющий
[Inject] private T myRef;, получал тот же экземпляр. - Помечает привязку как
.Lazy(), чтобы объект не разрешался сразу при построении контейнера.
Существует также вариант LazySingletonMonoInstaller<T> для систем на основе MonoBehaviour.
Конвенция .AsSingle().Lazy()
Каждая привязка MCE использует .AsSingle().Lazy(). Вот почему:
.AsSingle()— Гарантирует, что на контейнер существует ровно один экземпляр. Это предотвращает случайное дублирование менеджеров..Lazy()— Откладывает разрешение до первого использования. Поскольку MCE имеет более 20 установщиков, немедленное разрешение вызвало бы проблемы с порядком инициализации, когда система пытается использовать другую систему, которая ещё не завершила настройку.
Все установщики MCE
Вот полный список установщиков в движке:
Установщики основного рантайма
| Установщик | Расположение | Что привязывает |
|---|---|---|
GameFlowInstaller | Runtime/GameFlow/ | NewGameInitializer, PlayerTeleporter, TimeManager |
MonsterDatabaseInstaller | Runtime/MonsterDatabase/DependencyInjection/ | MonsterDatabaseInstance, ExperienceLookupTable |
PlayerDataInstaller | Runtime/Player/ | Roster, Bag, MonsterStorage, Dex, CharacterData, GlobalGameData, PlayerSettings, QuestManager |
BattleLauncherInstaller | Runtime/Battle/ | IBattleLauncher (через BattleLauncher) |
SinglePlayerPlayerControlManagerInstaller | Runtime/Battle/PlayerControl/ | Менеджер управления игроком в битве |
SavegameInstaller | Runtime/Saves/ | SavegameManager, ISerializer<string> (через MCESavesSerializer) |
DialogManagerInstaller | Runtime/UI/Dialogs/ | DialogManager.Factory |
PlayerCharacterInstaller | Runtime/Characters/ | PlayerCharacter.Factory |
EvolutionManagerInstaller | Runtime/Monster/Evolution/ | EvolutionManager |
HatchingManagerInstaller | Runtime/Monster/Breeding/ | HatchingManager |
TradeManagerInstaller | Runtime/Monster/Trade/ | TradeManager |
InputManagerInstaller | Runtime/Input/ | IInputManager |
ConfigurationManagerInstaller | Runtime/Configuration/ | IConfigurationManager |
AudioSettingsInstaller | Runtime/Audio/ | Менеджер настроек аудио |
RenderingInstaller | Runtime/Rendering/ | RenderingManager |
UIElementsInstaller | Runtime/UI/ | Ссылки на элементы UI |
MapSceneLauncherInstaller | Runtime/UI/Map/ | Запускатель сцены карты |
GlobalGridManagerInstaller | Runtime/World/ | GlobalGridManager |
GridInstaller | Runtime/World/ | GridController |
SceneInfoInstaller | Runtime/World/ | SceneInfo |
TileDataInstaller | Runtime/World/ | TileData |
WorldDatabaseInstaller | Runtime/World/ | WorldDatabase |
SDKInstaller | Runtime/SDK/ | IMonsterCaptureEngine (публичная точка входа SDK) |
Онлайн-слой добавляет ещё один, OnlineBattleInstaller (MCE.Online.Runtime, в Online/Runtime/Battle/). Папка Events/ в Foundation (ScriptableObject EventChannel) не нуждается в установщике.
Установщики Foundation
| Установщик | Расположение | Что привязывает |
|---|---|---|
AudioInstaller | Foundation/Runtime/Audio/ | IAudioManager |
LocalizationInstaller | Foundation/Runtime/Localization/ | ILocalizer |
SceneManagerInstaller | Foundation/Runtime/SceneManagement/ | ISceneManager |
ConfigurationManagerInstaller | Foundation/Runtime/Configuration/ | Конфигурация уровня Foundation |
Паттерны внедрения
Паттерн 1: [Inject] в Construct() (MonoBehaviours)
Это предпочтительный паттерн для MonoBehaviour. Zenject автоматически вызывает Construct() после создания объекта:
public class MCEInitialization : OpenMonBehaviour<MCEInitialization>
{
[Inject]
public void Construct(DialogManager.Factory dialogManagerFactory,
RenderingManager renderingManager,
MonsterDatabaseInstance monsterDatabase,
WorldDatabase worldDatabase,
ILocalizer localizer) =>
StartCoroutine(Initialize(dialogManagerFactory,
renderingManager,
monsterDatabase,
worldDatabase,
localizer));
}
Почему Construct() вместо внедрения через поля:
- Явные зависимости — Всё, что нужно классу, видно в одной сигнатуре метода.
- Неизменяемость — Внедрённые ссылки хранятся в
private readonlyполях; они не могут быть случайно перезаписаны. - Тестируемость — Вы можете вызвать
Construct()напрямую в юнит-тестах с mock-объектами.
Паттерн 2: Внедрение через поле [Inject] (ScriptableObjects)
ScriptableObjects, поставленные в очере дь через QueueForInject, используют внедрение через поля, поскольку у них нет жизненного цикла Construct():
public class EvolutionManager : OpenMonScriptable<EvolutionManager>
{
[Inject] private ILocalizer localizer;
[Inject] private ISceneManager sceneManager;
[Inject] private IInputManager inputManager;
[Inject] private GlobalGridManager globalGridManager;
[Inject] private TimeManager timeManager;
[Inject] private MCESettings settings;
[Inject] private MonsterDatabaseInstance database;
}
Чтобы это работало, установщик должен вызвать Container.QueueForInject(reference) перед привязкой.