Введение
Разработка игр в 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, внедрение зависимостей) - Паттерн проектирования, который решает описанные проблемы.
Он делает:
Инверсия управления (IoC): Класс не создаёт свои зависимости самостоятельно. Вместо этого, он объявляет, что они ему нужны (через конструктор, публичные поля или методы с атрибутами).
Внешнее предоставление: Ответственность за создание экземпляров зависимостей и их "впрыскивание" (injection) в нужные классы берёт на себя внешний компонент - Контейнер.
Слабая связанность: Классы знают только об интерфейсах или абстрактных типах своих зависимостей, а не о конкретных реализациях. Это позволяет легко заменять реализации.
Zenject
Zenject (позже Extenject) - Фреймворк с открытым исходным кодом, специально разработанный для интеграции паттерна DI в среду Unity. Он предоставляет способ управления зависимостями, жизненным циклом объектов и архитектурой приложения.
Ключевые компоненты и концепции:
-
Сердце системы это глобальный или сценарный реестр, который знает:
Что (какой интерфейс, абстрактный класс или конкретный тип) нужно предоставить.
Как создать экземпляр этого "чего-то" (самостоятельно / через фабрику / из префаба).
Кому и когда это нужно предоставить.
-
Привязка (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")
и т.д.).
-
Внедрение (Injection):
-
Конструктор: Самый предпочтительный способ. Zenject автоматический разрешает параметры конструктора.
-
Поля (с атрибутом
[Inject]
): -
Методы (с атрибутом
[Inject]
): Вызываются после создания / инъекции полей Свойства (аналогично полям,
[Inject]
).
-
-
Установщики (Installers):
Классы, наследующие от
MonoInstaller
(для привязок в сцене) илиInstaller
(для не-MonoBehaviour привязок).В методе
InstallBindings()
описываются все привязки для определённого контекста (проекта, сцены, подсистемы
).-
Иерархия и модульность:
Проектные (ProjectContext): Установщик, прикреплённый к префабу
ProjectContext
(создаётся автоматический при первом запуске). Привязки здесь доступны во всех сценах. Идеально для глобальных сервисов (сохранение, аудио, настройки, сеть).Сценарные (SceneContext): Установщик, прикреплённый к объекту
SceneContext
в сцене. Привязки здесь доступны только в текущей сцене. Обычно ссылается на ProjectContext черезContainer.Inherit = true
.Подконтексты (SubContainers): Позволяют создавать изолированные области зависимостей внутри сцены (например, для UI-окна).
-
Пример простого установщика:
-
Время жизни (Scope) и синглтоны:
AsTransient()
: Новый экземпляр при каждом запросе (по умолчанию).AsSingle()
: Один экземпляр на контейнер (и его дочерние контейнеры, если не указано иное).AsCached()
: АналогAsSingle()
, но для фабрик и пулов (один экземпляр на вызов фабрики / пула).FromResolve()
/FromResolveGetter()
: Связывает зависимость с другой зависимостью, уже зарегистрированной в контейнере.NonLazy()
: Заставляет контейнер создать экземпляр немедленно при старте, а не при первом запросе.
-
Фабрики (Factories):
Проблема: Создание объектов, требующих параметров при создании (например, враг с определённым уровнем и позицией).
-
Решение: Zenject генерирует классы фабрик автоматический.
Memory Pools (
FromPoolableMemoryPool
): Расширение фабрик для пулинга объектов (переиспользования), критически важное для производительности.
-
Сигналы (Signals):
Проблема: Глобальные события через
Action
или UnityEvents приводят к жёстким связям и сложности откладки.-
Решение: Система событий, основанная на DI. Публикуются и подписываются через контейнер.
Преимущества: Типобезопасность, централизация, возможность передачи данных в сигнале, автоматическая отписка при уничтожении подписчика.
Практическая интеграция в Unity проект
Установка: Через Package Manager (Git URL: https://github.com/modesttree/Zenject.git?path=UnityProject/Assets/Plugins/Zenject) или через Asset Store (Extenject).
-
Создание ProjectContext:
Создайте префаб
ProjectContext
(обычно в папкеResources
).Добавьте компонент
ProjectContext
.Добавьте ваши глобальные установщики в список
Installers
наProjectContext
.
-
Создание SceneContext:
На каждую сцену добавьте объект
SceneContext
.В его списке
Installers
добавьте установщики, специфичные для этой сцены.Убедитесь, что
Parent Container
ссылается наProjectContext
(обычно настроено по умолчанию).
-
Написание установщиков (
Installers
):Создайте классы-установщики для разных областей ответственности.
В
InstallBindings()
используйте методыBind<>().To<>().As...()
для регистрации зависимостей.
-
Рефакторинг классов:
Уберите ручное создание зависимостей (
new
,GetComponent
,FindObjectOfType
).Объявите зависимости через конструктор, поля или методы с
[Inject]
.Опирайтесь на интерфейсы!
Zenject vs Ручное управление
Ручное управление:

Жёсткая связь.
Сложно тестировать.
Изменение
SaveLoadManager
требует правкиPlayerController
.Код создания размазан по логике.
Zenject:

PlayerController
ничего не знает о созданииInventorySystem
или его внутренних зависимостях.Зависимость предоставляется контейнером автоматически.
Легко заменить
FileSystemService
наCloudeSaveService
только в установщике.Легко протестировать
PlayerController
, подсунув мокIInventorySystem
.
Словарик терминов
Внедрение зависимостей (Dependency Injection - DI) - Паттерн проектирования, при котором объект получает свои зависимости извне (через конструктор, поля, методы), а не создаёт их сам или ищет явно.
Инверсия управления (Inversion of Control - IoC) - Принцип, при котором управление созданием объектов и потоком выполнения передаётся внешнему фреймворку (контейнеру DI), а не остаётся внутри самих объектов.
Контейнер (DiContainer) - Объект, который знает какие зависимости зарегистрированы (
Bind
); как их создавать (из префаба / черезnew
/ из существующего экземпляра и т.д); их время жизни (Scope); и автоматически разрешает зависимости при создании объектов, которыми управляет.Привязка (Binding) - Процесс регистрации типа (интерфейса / класса) и его конкретной реализации или способа создания в контейнере. Делается в установщиках (
Installers
).Установщик (Installer): Класс (обычно наследующий от
MonoInstaller
), в котором выполняется конфигурация контейнера через методInstallBindings()
. Содержит вызовBind()
. Могут быть проектные (ProjectContext
) и сценарные (SceneContext
).ProjectContext - Специальный префаб (обычно в папке
Resources
), создаваемый при старте игры. Содержит глобальные контейнеры и установщики, чьи привязки доступны во всех сценах. Хранит глобальные синглтоны (AsSingle()
).SceneContext - Компонент, добавляемый в каждую сцену. Содержит сценарный контейнер и установщики, специфичные для данной сцены. Обычно наследует привязки от
ProjectContext
(Container.Inherit = true
).Внедрение (Injection) - Процесс, при котором контейнер автоматически передаёт экземпляры зависимостей в объект.
Время жизни / Скоуп (Scope) - Определяет, как долго живёт экземпляр, созданный контейнером, и как часто он создаётся заново.
AsTransient()
: Создается новый экземпляр каждый раз, когда запрашивается зависимость (по умолчанию). Короткая жизнь.AsSingle()
: Создается один экземпляр на контейнер (и его дочерние контейнеры). Классический "синглтон" в рамках своего скоупа (проект, сцена, подконтейнер). Долгая жизнь.AsCached()
: Создается один экземпляр, но привязанный к конкретному "источнику" (например, к конкретному вызову фабрики). Используется в фабриках и пулах.Фабрика (Factory) - Объект, отвечающий за создание других объектов, особенно когда для создания нужны параметры. Zenject автоматически генерирует фабрики на основе интерфейса
PlaceholderFactory<Т, Парам1, Парам2, ...>
. Позволяет создавать объекты с зависимостями, передавая параметры.Пул (Pool) - Механизм для переиспользования объектов (часто дорогих в создании). Расширяет концепцию фабрики (
FromPoolableMemoryPool
). Уменьшает нагрузку на сборщик мусора (GC). Критично для производительности.Сигналы (Signals) - Система событий, основанная на DI. Альтернативна глобальным
Action
илиUnityEvent
. Позволяет объявлять типизированные сообщения (struct PlayerDiedSignal
), публиковать их черезSignalBus.Fire()
и подписываться на них черезSignalBus.Subscribe()
. Типобезопасна, централизована, упрощает отписку.SignalBus - Центральный объект, через который происходит публикация (
Fire
) и подписка на сигналы.Подконтекст (Sub-Container) - Изолированный "дочерний" контейнер внутри основного сценарного контейнера. Позволяет создавать области с собственным набором зависимостей. Управляется через
SubContainerCreator
.Резолюция (Resolving) - Процесс, в ходе которого контейнер находит или создаёт экземпляр зависимости, запрошенной классом.
Граф объектов (Object Graph): Иерархия объектов, созданных контейнером, где каждый объект получает свои зависимости, разрешенные контейнером. Zenject автоматически строит этот граф при старте сцены или при создании корневого объекта.
Lazy опция привязки - (По умолчанию) Объект создаётся только при первом запросе его как зависимости.
NonLaze опция привязки - Объект создаётся немедленно при инициализации контейнера (старте сцены/проекта), даже если на него ещё не ссылок. Полезно для сервисов, которые должны быть готовы сразу.
Id (Идентификатор) - Позволяет различать несколько привязок одного и того же типа. Используется с
Bind<Тип>().WithId("имя").To...
и[Inject(Id = "имя")]
.Интерфейс (Interface) - Ключевая абстракция в DI. Классы должны зависеть от интерфейсов (
IInventory, IAudioService
), а не от конкретных реализаций (InventorySystem, UnityAudioService
). Это основа слабой связанности и возможности замены реализаций.
Если понравилась статья - рекомендую подписаться на телеграм‑канал NetIntel. Там вы сможете найти множество полезных материалов по IT и разработке!
Jijiki
спасибо, теперь понял зачем это нужно, тоесть цепочка связи по-сути иерархия 1 настройкой цепочки, посмотрел он и в java есть как раз то что нужно )