Введение

Разработка игр в Unity, особенно средних и крупных проектов, быстро приводит к сложному переплетению классов и компонентов. Представьте:

  • PlayerController зависит от InventorySystem.

  • InventorySystem зависит от ItemDatabase и SaveLoadManager.

  • SaveLoadManager зависит от FileSystemService и EncryptionService.

  • EncryptionService использует настройки из GameSettings.

Создавая PlayerController вручную (new PlayerController()), вы вынуждены также создавать InventorySystem, потом создавать ItemDatabase и SaveLoadSystem, потом создавать FileSystemService, EncryptionService и получить доступ к GameSettings... Короче, это превращается в "спагетти-код", где классы жёстко связаны. Изменение одной зависимости вызывает волну правок по всему коду. Тестирование становиться невозможным, так как не возможно изолировать класс, не создавая всю гору его зависимостей.

Но выход есть!

Dependency Injection

Dependency Injection (DI, внедрение зависимостей) - Паттерн проектирования, который решает описанные проблемы.

Он делает:

  1. Инверсия управления (IoC): Класс не создаёт свои зависимости самостоятельно. Вместо этого, он объявляет, что они ему нужны (через конструктор, публичные поля или методы с атрибутами).

  2. Внешнее предоставление: Ответственность за создание экземпляров зависимостей и их "впрыскивание" (injection) в нужные классы берёт на себя внешний компонент - Контейнер.

  3. Слабая связанность: Классы знают только об интерфейсах или абстрактных типах своих зависимостей, а не о конкретных реализациях. Это позволяет легко заменять реализации.

Zenject

Zenject (позже Extenject) - Фреймворк с открытым исходным кодом, специально разработанный для интеграции паттерна DI в среду Unity. Он предоставляет способ управления зависимостями, жизненным циклом объектов и архитектурой приложения.

Ключевые компоненты и концепции:

  1. Сердце системы это глобальный или сценарный реестр, который знает:

    • Что (какой интерфейс, абстрактный класс или конкретный тип) нужно предоставить.

    • Как создать экземпляр этого "чего-то" (самостоятельно / через фабрику / из префаба).

    • Кому и когда это нужно предоставить.

  2. Привязка (Binding):

    • Процесс регистрации типов и их зависимостей в контейнере.

    • Основные методы:

      • Bind<ТипИнтерфейса>().To<КонкретнаяРеализация>().AsSingle(); - Привязка интерфейса к конкретному классу.
        AsSingle() - "Один экземпляр - один контейнер",
        To<>() - Привязка интерфейса к реализации. Интерфейс в Bind<>() привязывается к реализации в To<>().

      • Bind<Тип>().FromNew(); - Создавать новый экземпляр каждый раз, когда запрашивается.

      • Bind<Тип>().FromInstance(myExistingObject); - Использовать существующий заранее созданный экземпляр.

      • Bind<Тип>().FromComponentInNewPrefab(playerPrefab); - Создавать экземпляр из префаба (для MonoBehaviour).

      • Bind<Тип>().FromComponentInHierarchy(); - Найти существующий компонент типа Тип на сцене.

      • Bind<Тип>().To<Реализация>().AsTransient(); - Создавать новый экземпляр при каждом запросе (по умолчанию).

      • Bind<Тип>().WithId("SpecialID").To ... - Привязка с идентификатором для различения зависимостей одного типа.

    • Цепочки: Методы можно цепочкой добавлять для настройки (AsSingle(), NonLaze(), WhenInjectedInto<SomeType>(), WithArguments(42, "hello") и т.д.).

  3. Внедрение (Injection):

    • Конструктор: Самый предпочтительный способ. Zenject автоматический разрешает параметры конструктора.

    • Поля (с атрибутом [Inject]):

    • Методы (с атрибутом [Inject]): Вызываются после создания / инъекции полей

    • Свойства (аналогично полям, [Inject]).

  4. Установщики (Installers):

    • Классы, наследующие от MonoInstaller (для привязок в сцене) или Installer (для не-MonoBehaviour привязок).

    • В методе InstallBindings() описываются все привязки для определённого контекста (проекта, сцены, подсистемы).

    • Иерархия и модульность:

      • Проектные (ProjectContext): Установщик, прикреплённый к префабу ProjectContext (создаётся автоматический при первом запуске). Привязки здесь доступны во всех сценах. Идеально для глобальных сервисов (сохранение, аудио, настройки, сеть).

      • Сценарные (SceneContext): Установщик, прикреплённый к объекту SceneContext в сцене. Привязки здесь доступны только в текущей сцене. Обычно ссылается на ProjectContext через Container.Inherit = true.

      • Подконтексты (SubContainers): Позволяют создавать изолированные области зависимостей внутри сцены (например, для UI-окна).

    • Пример простого установщика:

  5. Время жизни (Scope) и синглтоны:

    • AsTransient(): Новый экземпляр при каждом запросе (по умолчанию).

    • AsSingle(): Один экземпляр на контейнер (и его дочерние контейнеры, если не указано иное).

    • AsCached(): Аналог AsSingle(), но для фабрик и пулов (один экземпляр на вызов фабрики / пула).

    • FromResolve() / FromResolveGetter(): Связывает зависимость с другой зависимостью, уже зарегистрированной в контейнере.

    • NonLazy(): Заставляет контейнер создать экземпляр немедленно при старте, а не при первом запросе.

  6. Фабрики (Factories):

    • Проблема: Создание объектов, требующих параметров при создании (например, враг с определённым уровнем и позицией).

    • Решение: Zenject генерирует классы фабрик автоматический.

    • Memory Pools (FromPoolableMemoryPool): Расширение фабрик для пулинга объектов (переиспользования), критически важное для производительности.

  7. Сигналы (Signals):

    • Проблема: Глобальные события через Action или UnityEvents приводят к жёстким связям и сложности откладки.

    • Решение: Система событий, основанная на DI. Публикуются и подписываются через контейнер.

    • Преимущества: Типобезопасность, централизация, возможность передачи данных в сигнале, автоматическая отписка при уничтожении подписчика.

Практическая интеграция в Unity проект

  1. Установка: Через Package Manager (Git URL: https://github.com/modesttree/Zenject.git?path=UnityProject/Assets/Plugins/Zenject) или через Asset Store (Extenject).

  2. Создание ProjectContext:

    • Создайте префаб ProjectContext (обычно в папке Resources).

    • Добавьте компонент ProjectContext.

    • Добавьте ваши глобальные установщики в список Installers на ProjectContext.

  3. Создание SceneContext:

    • На каждую сцену добавьте объект SceneContext.

    • В его списке Installers добавьте установщики, специфичные для этой сцены.

    • Убедитесь, что Parent Container ссылается на ProjectContext (обычно настроено по умолчанию).

  4. Написание установщиков (Installers):

    • Создайте классы-установщики для разных областей ответственности.

    • В InstallBindings() используйте методы Bind<>().To<>().As...() для регистрации зависимостей.

  5. Рефакторинг классов:

    • Уберите ручное создание зависимостей (new, GetComponent, FindObjectOfType).

    • Объявите зависимости через конструктор, поля или методы с [Inject].

    • Опирайтесь на интерфейсы!

Zenject vs Ручное управление

Ручное управление:

  • Жёсткая связь.

  • Сложно тестировать.

  • Изменение SaveLoadManager требует правки PlayerController.

  • Код создания размазан по логике.

Zenject:

  • PlayerController ничего не знает о создании InventorySystem или его внутренних зависимостях.

  • Зависимость предоставляется контейнером автоматически.

  • Легко заменить FileSystemService на CloudeSaveService только в установщике.

  • Легко протестировать PlayerController, подсунув мок IInventorySystem.

Словарик терминов

  1. Внедрение зависимостей (Dependency Injection - DI) - Паттерн проектирования, при котором объект получает свои зависимости извне (через конструктор, поля, методы), а не создаёт их сам или ищет явно.

  2. Инверсия управления (Inversion of Control - IoC) - Принцип, при котором управление созданием объектов и потоком выполнения передаётся внешнему фреймворку (контейнеру DI), а не остаётся внутри самих объектов.

  3. Контейнер (DiContainer) - Объект, который знает какие зависимости зарегистрированы (Bind); как их создавать (из префаба / через new / из существующего экземпляра и т.д); их время жизни (Scope); и автоматически разрешает зависимости при создании объектов, которыми управляет.

  4. Привязка (Binding) - Процесс регистрации типа (интерфейса / класса) и его конкретной реализации или способа создания в контейнере. Делается в установщиках (Installers).

  5. Установщик (Installer): Класс (обычно наследующий от MonoInstaller), в котором выполняется конфигурация контейнера через метод InstallBindings(). Содержит вызов Bind(). Могут быть проектные (ProjectContext) и сценарные (SceneContext).

  6. ProjectContext - Специальный префаб (обычно в папке Resources), создаваемый при старте игры. Содержит глобальные контейнеры и установщики, чьи привязки доступны во всех сценах. Хранит глобальные синглтоны (AsSingle()).

  7. SceneContext - Компонент, добавляемый в каждую сцену. Содержит сценарный контейнер и установщики, специфичные для данной сцены. Обычно наследует привязки от ProjectContext (Container.Inherit = true).

  8. Внедрение (Injection) - Процесс, при котором контейнер автоматически передаёт экземпляры зависимостей в объект.

  9. Время жизни / Скоуп (Scope) - Определяет, как долго живёт экземпляр, созданный контейнером, и как часто он создаётся заново.
    AsTransient(): Создается новый экземпляр каждый раз, когда запрашивается зависимость (по умолчанию). Короткая жизнь.
    AsSingle(): Создается один экземпляр на контейнер (и его дочерние контейнеры). Классический "синглтон" в рамках своего скоупа (проект, сцена, подконтейнер). Долгая жизнь.
    AsCached(): Создается один экземпляр, но привязанный к конкретному "источнику" (например, к конкретному вызову фабрики). Используется в фабриках и пулах.

  10. Фабрика (Factory) - Объект, отвечающий за создание других объектов, особенно когда для создания нужны параметры. Zenject автоматически генерирует фабрики на основе интерфейса PlaceholderFactory<Т, Парам1, Парам2, ...>. Позволяет создавать объекты с зависимостями, передавая параметры.

  11. Пул (Pool) - Механизм для переиспользования объектов (часто дорогих в создании). Расширяет концепцию фабрики (FromPoolableMemoryPool). Уменьшает нагрузку на сборщик мусора (GC). Критично для производительности.

  12. Сигналы (Signals) - Система событий, основанная на DI. Альтернативна глобальным Action или UnityEvent. Позволяет объявлять типизированные сообщения (struct PlayerDiedSignal), публиковать их через SignalBus.Fire() и подписываться на них через SignalBus.Subscribe(). Типобезопасна, централизована, упрощает отписку.

  13. SignalBus - Центральный объект, через который происходит публикация (Fire) и подписка на сигналы.

  14. Подконтекст (Sub-Container) - Изолированный "дочерний" контейнер внутри основного сценарного контейнера. Позволяет создавать области с собственным набором зависимостей. Управляется через SubContainerCreator.

  15. Резолюция (Resolving) - Процесс, в ходе которого контейнер находит или создаёт экземпляр зависимости, запрошенной классом.

  16. Граф объектов (Object Graph): Иерархия объектов, созданных контейнером, где каждый объект получает свои зависимости, разрешенные контейнером. Zenject автоматически строит этот граф при старте сцены или при создании корневого объекта.

  17. Lazy опция привязки - (По умолчанию) Объект создаётся только при первом запросе его как зависимости.

  18. NonLaze опция привязки - Объект создаётся немедленно при инициализации контейнера (старте сцены/проекта), даже если на него ещё не ссылок. Полезно для сервисов, которые должны быть готовы сразу.

  19. Id (Идентификатор) - Позволяет различать несколько привязок одного и того же типа. Используется с Bind<Тип>().WithId("имя").To... и [Inject(Id = "имя")].

  20. Интерфейс (Interface) - Ключевая абстракция в DI. Классы должны зависеть от интерфейсов (IInventory, IAudioService), а не от конкретных реализаций (InventorySystem, UnityAudioService). Это основа слабой связанности и возможности замены реализаций.


Если понравилась статья - рекомендую подписаться на телеграм‑канал NetIntel. Там вы сможете найти множество полезных материалов по IT и разработке!

Комментарии (2)


  1. Jijiki
    19.06.2025 10:07

    спасибо, теперь понял зачем это нужно, тоесть цепочка связи по-сути иерархия 1 настройкой цепочки, посмотрел он и в java есть как раз то что нужно )


  1. CloudlyNosound
    19.06.2025 10:07

    Одна из немногих публикаций, где есть список терминов.