Dependency Injection через Extenject
Зачем это нужно
Dependency Injection через Extenject нужен, чтобы:
- разделять игровую логику и Unity-обвязку
- уменьшать количество жёстких ссылок между компонентами
- легче тестировать код
- проще собирать разные режимы проекта: игра, сервисный режим, призовой контур, аппаратный слой
Для Danro Jump это особенно полезно, потому что проект со временем будет сочетать:
- аркадный геймплей
- работу с
COM-портами - сервисные экраны для персонала
- призовую витрину
- сценарии выдачи через
QR-коды
Без DI такие подсистемы быстро начинают зависеть друг от друга напрямую и усложняют сопровождение.
Когда Extenject действительно полезен
Extenject особенно хорошо подходит, если в проекте есть:
- несколько отдельных подсистем
- сервисы с долгим жизненным циклом
- необходимость подменять реализации
- разные режимы запуска
- желание держать доменную логику вне
MonoBehaviour
Для Danro Jump типичные кандидаты на DI:
IGameSessionServiceIScoreServiceIPrizeServiceIPrizeShowcaseGatewayIQrRewardServiceIComPortGatewayIServiceModeAccessPolicyIGameBalanceProvider
Базовая модель Extenject
В Extenject важно понимать три уровня:
ProjectContext— глобальные зависимости на весь runtimeSceneContext— зависимости внутри конкретной сценыGameObjectContext— локальный контейнер для prefab или отдельного объекта
Практическое правило:
- в
ProjectContextскладывай только по-настоящему глобальные вещи - в
SceneContextскладывай зависимости конкретного режима или сцены GameObjectContextиспользуй только там, где действительно нужен локальный composition root
Рекомендуемая стратегия для Danro Jump
ProjectContext
В ProjectContext обычно стоит держать:
- конфигурацию приложения
- логирование
- фабрики системного уровня
- глобальные настройки локализации
- сервисы, которые переживают смену сцен
Примеры:
Container.BindInterfacesAndSelfTo<AppLifetime>().AsSingle();
Container.BindInterfacesAndSelfTo<GameSettingsProvider>().AsSingle();
Container.Bind<IComPortGateway>().To<ComPortGateway>().AsSingle();
Container.Bind<IQrRewardService>().To<QrRewardService>().AsSingle();
SceneContext
В SceneContext конкретной сцены лучше держать:
- локальный gameplay flow
- сценовые контроллеры
- scene-specific presenters
- экранные сервисы
Пример для игровой сцены:
public sealed class GameplayInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.BindInterfacesAndSelfTo<GameSessionService>().AsSingle();
Container.BindInterfacesAndSelfTo<PlatformSpawner>().AsSingle();
Container.BindInterfacesAndSelfTo<PlayerDeathHandler>().AsSingle();
Container.BindInterfacesAndSelfTo<GameHudPresenter>().AsSingle();
}
}
Отдельный сервисный режим
Сервисный режим для персонала лучше оформлять отдельной сценой или отдельным сценовым контекстом, а не примешивать к gameplay-сцене напрямую.
Тогда там можно поднимать:
- настройки стоимости игры
- настройки сложности
- настройки призов
- диагностику
COM-оборудования - ручные тесты витрины и выдачи
Composition Root
Главное правило Extenject:
- контейнер знает, как собрать объект
- сам объект не должен знать, где искать свои зависимости
Это значит:
- не использовать
FindObjectOfType - не использовать singleton-поиск как основной способ сборки
- не прокидывать контейнер в доменную логику как сервис-локатор
Хорошо:
public sealed class PrizeService : IPrizeService
{
private readonly IPrizeRepository _repository;
private readonly IPrizeShowcaseGateway _showcaseGateway;
public PrizeService(
IPrizeRepository repository,
IPrizeShowcaseGateway showcaseGateway)
{
_repository = repository;
_showcaseGateway = showcaseGateway;
}
}
Плохо:
public sealed class PrizeService
{
public PrizeService()
{
var gateway = ProjectContext.Instance.Container.Resolve<IPrizeShowcaseGateway>();
}
}
Что именно инжектить
Обычно через Extenject лучше инжектить:
- интерфейсы
- чистые C# сервисы
- конфигурационные объекты
- фабрики
- адаптеры к внешним системам
С осторожностью:
MonoBehaviourScriptableObject- runtime-created prefab-объекты
Если зависимость является Unity-компонентом сцены, лучше явно понимать её жизненный цикл.
Типичный набор ролей в проекте
Для Danro Jump удобно делить классы так:
Domain / Core
- правила игры
- подсчёт наград
- проверка eligibility для выдачи призов
- политика сложности
Application / Services
- orchestration игровых сценариев
- координация сервисного режима
- работа с
QR - orchestration аппаратных операций
Infrastructure
COM-порт- Unity scene loading
- persistence
- конкретная работа с UI и устройствами
Presentation
- HUD presenters
- menu presenters
- service panel presenters
Extenject особенно хорошо работает, когда связывает эти уровни без прямой жёсткой сцепки.
Рекомендуемые binding-паттерны
BindInterfacesAndSelfTo
Удобен для сервисов с несколькими ролями:
Container.BindInterfacesAndSelfTo<GameSessionService>().AsSingle();
FromInstance
Хорошо подходит для конфигурации:
Container.Bind<GameBalanceConfig>().FromInstance(_balanceConfig).AsSingle();
FromComponentInHierarchy
Подходит только для сценовых объектов, которые действительно уже существуют:
Container.Bind<GameplayHudView>().FromComponentInHierarchy().AsSingle();
Factory
Подходит для runtime-создания:
- врагов
- платформ
- popup-элементов
- наградных визуальных объектов
Жизненные циклы
Для сервисов в Extenject чаще всего пригодятся:
IInitializableITickableIDisposable
Практика:
IInitializableиспользуй для явного стартаITickableтолько там, где реально нужен цикл обновленияIDisposableобязательно для внешних ресурсов, особенноCOM-соединений и подписок
Пример:
public sealed class ComPortGateway : IComPortGateway, IInitializable, IDisposable
{
public void Initialize()
{
// Открытие и подготовка порта
}
public void Dispose()
{
// Корректное закрытие порта
}
}
Связка с управлением сценами
Для сцен в Extenject важны два сценария:
- передача зависимостей через
ProjectContext - передача параметров при переходах
Если нужно передавать параметры между сценами, удобнее не использовать статические поля, а оформлять отдельный переходный сервис или ZenjectSceneLoader.
Это особенно важно для переходов вида:
- из attract/menu режима в игровую сессию
- из игры в экран награды
- из игры в сервисный режим
Чего лучше избегать
- делать
ProjectContextсвалкой всех сервисов - инжектить всё подряд в
MonoBehaviour - дублировать bindings в разных installers без правил
- смешивать доменную логику и прямую работу с Unity API
- решать все проблемы через один огромный installer
Практическая схема для этого проекта
На старте проекта удобно держать такой набор installers:
ProjectInstallerGameplayInstallerMenuInstallerServiceModeInstallerPrizeFlowInstallerHardwareInstaller
Пример разбиения ответственности:
ProjectInstaller— глобальные настройки и shared-сервисыGameplayInstaller— игровой цикл и gameplay-сервисыServiceModeInstaller— настройки персоналаHardwareInstaller—COM, диагностика, витрина
Как внедрять DI поэтапно
Не нужно переводить весь проект на Extenject за один раз.
Лучше идти по шагам:
- Вынести 1-2 ключевых сервиса в чистые C# классы.
- Подключить их через
SceneContext. - Отделить UI от прикладной логики.
- Вынести аппаратные зависимости за интерфейсы.
- Только потом строить более широкую сетку installers и фабрик.
Для каких задач это даст максимальную пользу в Danro Jump
- для сервисного режима
- для аппаратной интеграции
- для призовой логики
- для разделения игровых режимов
- для тестируемой бизнес-логики без прямой привязки к
MonoBehaviour
Официальные и полезные источники
- https://github.com/Mathijs-Bakker/Extenject
- https://github.com/Mathijs-Bakker/Extenject/releases
GUIDES/packages/zenject/README.md