Перейти к основному содержимому

Dependency Injection (Zenject)

Только для редакций Source и Studio

Эта страница описывает внутренний код движка, доступный исключительно лицензиатам редакций 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();
}
}

Это делает три вещи:

  1. Ставит ScriptableObject в очередь на внедрение, чтобы его собственные поля [Inject] были разрешены.
  2. Привязывает его как синглтон, чтобы любой класс, объявляющий [Inject] private T myRef;, получал тот же экземпляр.
  3. Помечает привязку как .Lazy(), чтобы объект не разрешался сразу при построении контейнера.

Существует также вариант LazySingletonMonoInstaller<T> для систем на основе MonoBehaviour.

Конвенция .AsSingle().Lazy()​

Каждая привязка MCE использует .AsSingle().Lazy(). Вот почему:

  • .AsSingle() — Гарантирует, что на контейнер существует ровно один экземпляр. Это предотвращает случайное дублирование менеджеров.
  • .Lazy() — Откладывает разрешение до первого использования. Поскольку MCE имеет более 20 установщиков, немедленное разрешение вызвало бы проблемы с порядком инициализации, когда система пытается использовать другую систему, которая ещё не завершила настройку.

Все установщики MCE​

Вот полный список установщиков в движке:

Установщики основного рантайма​

УстановщикРасположениеЧто привязывает
GameFlowInstallerRuntime/GameFlow/NewGameInitializer, PlayerTeleporter, TimeManager
MonsterDatabaseInstallerRuntime/MonsterDatabase/DependencyInjection/MonsterDatabaseInstance, ExperienceLookupTable
PlayerDataInstallerRuntime/Player/Roster, Bag, MonsterStorage, Dex, CharacterData, GlobalGameData, PlayerSettings, QuestManager
BattleLauncherInstallerRuntime/Battle/IBattleLauncher (через BattleLauncher)
SinglePlayerPlayerControlManagerInstallerRuntime/Battle/PlayerControl/Менеджер управления игроком в битве
SavegameInstallerRuntime/Saves/SavegameManager, ISerializer<string> (через MCESavesSerializer)
DialogManagerInstallerRuntime/UI/Dialogs/DialogManager.Factory
PlayerCharacterInstallerRuntime/Characters/PlayerCharacter.Factory
EvolutionManagerInstallerRuntime/Monster/Evolution/EvolutionManager
HatchingManagerInstallerRuntime/Monster/Breeding/HatchingManager
TradeManagerInstallerRuntime/Monster/Trade/TradeManager
InputManagerInstallerRuntime/Input/IInputManager
ConfigurationManagerInstallerRuntime/Configuration/IConfigurationManager
AudioSettingsInstallerRuntime/Audio/Менеджер настроек аудио
RenderingInstallerRuntime/Rendering/RenderingManager
UIElementsInstallerRuntime/UI/Ссылки на элементы UI
MapSceneLauncherInstallerRuntime/UI/Map/Запускатель сцены карты
GlobalGridManagerInstallerRuntime/World/GlobalGridManager
GridInstallerRuntime/World/GridController
SceneInfoInstallerRuntime/World/SceneInfo
TileDataInstallerRuntime/World/TileData
WorldDatabaseInstallerRuntime/World/WorldDatabase
SDKInstallerRuntime/SDK/IMonsterCaptureEngine (публичная точка входа SDK)

Онлайн-слой добавляет ещё один, OnlineBattleInstaller (MCE.Online.Runtime, в Online/Runtime/Battle/). Папка Events/ в Foundation (ScriptableObject EventChannel) не нуждается в установщике.

Установщики Foundation​

УстановщикРасположениеЧто привязывает
AudioInstallerFoundation/Runtime/Audio/IAudioManager
LocalizationInstallerFoundation/Runtime/Localization/ILocalizer
SceneManagerInstallerFoundation/Runtime/SceneManagement/ISceneManager
ConfigurationManagerInstallerFoundation/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) перед привязкой.

Паттерн 3: Паттерн Factory (Создание во время выполнения)​

Для префабов, которые необходимо создавать во время выполнения с полным DI, MCE использует PlaceholderFactory от Zenject:

// В DialogManagerInstaller:
Container.BindFactory<DialogManager, DialogManager.Factory>()
.FromComponentInNewPrefab(Prefab);

// В PlayerCharacterInstaller:
Container.BindFactory<PlayerCharacter, PlayerCharacter.Factory>()
.FromComponentInNewPrefab(Prefab)
.AsSingle()
.Lazy();

Любой класс может затем внедрить фабрику и создавать экземпляры:

[Inject]
public void Construct(DialogManager.Factory dialogManagerFactory)
{
DialogManager dialog = dialogManagerFactory.Create();
// Все поля [Inject] в 'dialog' разрешены автоматически
}

Паттерн 4: Условное внедрение с WhenInjectedInto​

PlayerDataInstaller ограничивает привязки данных игрока классами, реализующими IPlayerDataReceiver:

Container.Bind<Roster>()
.FromInstance(PlayerRoster)
.AsSingle()
.WhenInjectedInto<IPlayerDataReceiver>()
.Lazy();

Это предотвращает случайное внедрение данных, специфичных для игрока, в системы, которые не должны от них зависеть.

Паттерн 5: Привязка через интерфейс​

Когда системы имеют уровень абстракции, установщик привязывает интерфейс к конкретному экземпляру:

// BattleLauncherInstaller
Container.Bind<IBattleLauncher>().FromInstance(Reference).AsSingle().Lazy();

Потребители внедряют интерфейс, а не конкретный класс, что позволяет заменять реализацию для тестирования или онлайн-игры.

Создание собственного установщика​

Шаг 1: Создание установщика ScriptableObject​

using UnityEngine;
using OpenMon.Foundation.DependencyInjection;

[CreateAssetMenu(menuName = "MCE/Dependency Injection/MySystemInstaller",
fileName = "MySystemInstaller")]
public class MySystemInstaller : LazySingletonScriptableInstaller<MySystem>
{
// Если вашей системе нужны дополнительные привязки, переопределите InstallBindings:
public override void InstallBindings()
{
base.InstallBindings(); // Привязывает MySystem как синглтон

// Добавьте дополнительные привязки здесь
Container.Bind<IMyService>().To<MyServiceImpl>().AsSingle().Lazy();
}
}

Шаг 2: Создание актива установщика​

Щёлкните правой кнопкой мыши в окне Project и используйте свой путь CreateAssetMenu для создания актива. Назначьте ссылки на ScriptableObject в Inspector.

Шаг 3: Регистрация в ProjectContext​

  1. Откройте Assets/Resources/ProjectContext.prefab.
  2. Выберите компонент ProjectContext.
  3. Добавьте ваш актив установщика в список ScriptableObjectInstallers.

Шаг 4: Внедрение в ваши классы​

public class MyConsumer : OpenMonBehaviour<MyConsumer>
{
[Inject]
public void Construct(MySystem mySystem, IMyService myService)
{
// Готово к использованию
}
}

Распространённые проблемы​

ПроблемаПричинаРешение
ZenjectException: Unable to resolveОтсутствует привязка или установщик не в ProjectContextПроверьте, что установщик добавлен в ProjectContext и тип точно совпадает
NullReferenceException на поле [Inject]QueueForInject не вызван для ScriptableObjectДобавьте Container.QueueForInject(reference) перед вызовом .Bind()
Объект внедрён слишком раноСистема использует внедрённую зависимость в Awake()Перенесите логику в Construct() или Start(); Zenject внедряет после Awake()
Ошибка дублирования привязкиОдин и тот же тип привязан в двух установщикахИспользуйте .WhenInjectedInto<>() для ограничения области или привяжите к интерфейсу
Фабрика создаёт объекты без внедренияИспользован Instantiate() напрямуюИспользуйте паттерн фабрики Zenject (BindFactory + Factory.Create())

Тестирование с mock-контейнерами​

Для юнит-тестов, которым нужен DI, создайте DiContainer вручную:

[Test]
public void MySystem_ShouldDoSomething()
{
var container = new DiContainer();
var mockLocalizer = Substitute.For<ILocalizer>();

container.Bind<ILocalizer>().FromInstance(mockLocalizer);
container.Bind<MySystem>().AsSingle();

var system = container.Resolve<MySystem>();
// Assert behavior
}

Для интеграционных тестов, которым нужен полный контейнер MCE, используйте [UnityTest] со сценой, где настроен ProjectContext.