Данная статья является попыткой переварить и упростить 2-часовое видео, материала созданного ребятами из “k-syndicate.school” и предоставленного (за что им огромное спасибо) в свободном доступе: K-Syndicate.

ZENJECT (позже EXTENJECT) – Фреймворк с открытым исходным кодом, специально разработанный для интеграции паттерна Dependency Injection в среду Unity. Он предоставляет способ управления зависимостями, жизненным циклом объектов и архитектурой приложения.

Установка ZENJECT (EXTENJECT)

Создадим новый тестовый проект Unity. Для установки ZENJECT можно использовать один из двух методов:

  1. Качаем с гита и устанавливаем в проект последнюю версию плагина github Zenject На данный момент текущая версия 9.2.0.
  2. Устанавливаем ZENJECT из Asset Store AssetStore EXTENJECT

Настройка ZENJECT (EXTENJECT)

  1. Создание ProjectContext:
    • Создайте префаб ProjectContext (обычно в папке Resources).
    • Добавьте компонент ProjectContext.
    • Глобальные установщики (менеджеры аналитики, рекламы)будем добавлять в список Installers на ProjectContext.
  2. Создание SceneContext:
    • На каждую сцену из контекстного меню добавим объект SceneContext.
    • В его списке Installers будем добавлять установщики специфичные для этой сцены.

По сути, SceneContext нужен, чтобы инициализировать ( забутстрапить ) весь ZENJECT и происходит это следующим образом:

когда стартует SceneContext он ищет ProjectContext, если его нет, он создаётся и помечается как DontDestroyOnLoad; затем выполняются все регистрации в ProjectContext и лишь потом делает свои регистрации. (работает это благодаря “Script Execution Order”)

Созданный ProjectContext имеет несколько инсталлеров.

Инсталлер может регистрировать зависимости в контейнере.
Лучше убрать галочку с “Parent New Object Under Context”, так как мы не хотим чтоб все объекты которые мы будем инстанцировать через контейнер, были дочерними для ProjectContext.

Добавление инсталеров

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

  • Классы, наследующие от MonoInstaller (для привязок в сцене) или Installer (для не-MonoBehaviour привязок).
  • В методе InstallBindings() описываются все привязки для определённого контекста (проекта, сцены, подсистемы).
  • Иерархия и модульность:
    • Проектные (ProjectContex): Установщик, прикреплённый к префабу ProjectContext (создаётся автоматический при первом запуске). Привязки здесь доступны во всех сценах. Идеально для глобальных сервисов (сохранение, аудио, настройки, сеть).
    • Сценарные (SceneContext): Установщик, прикреплённый к объекту SceneContext в сцене. Привязки здесь доступны только в текущей сцене. Обычно ссылается на ProjectContext через Container.Inherit = true.
    • Подконтексты (SubContainers): Позволяют создавать изолированные области зависимостей внутри сцены (например, для UI-окна).

В “Assets” создадим папку “Infrastructure” в которой создадим класс BootstrapInstaller он будет наследоваться от MonoInstaller. Внутри переопределим InstallBindings() в котором будем регистрировать сервисы игры.

using Zenject;
public class BootstrapInstaller: MonoInstaller
{
    public override void InstallBindings()
    {
        
    }
}

BootstrapInstaller добавляем как компонент в ProjectContext и уже созданный компонент
перетягиваем в список “Mono Installers”.

Аналогично создаём LocationInstaller и вешаем его на SceneContext.
Итого мы получили два инсталера, один работает локально в сцене, другой действует во всем проекте.

Эксперимент-1. Создание героя из префаба и изменение героя врагом

Подготовка героя и врага

Создадим префаб кубика, который будет нашим условным героем и назовем его “Hero” Добавим кубику класс HeroController и добавим метод который будет увеличивать размер кубика в два раза.

public class HeroController : MonoBehaviour
{
    public void ChangeLocalScale()
    {
        gameObject.transform.localScale = new Vector3(2, 2, 2);
    }
}


В сцене создадим пустой объект “StartPoint”, это будет точка, в которой мы будем инстанцировать героя. В LocationInstaller создадим два поля “public Transform StartPoint;” и public “GameObject HeroPrefab;” и перетянем на них “Hero” и “StartPoint”,

Создадим Врага. Чтоб не усложнять статью создадим на сцене простой шар назовем его “enemy” и создадим для него класс EnemyController (пока пустой).

Создание обьекта из префаба с помощью ZENJECT

Теперь у нас все готово, чтоб инстанцировать героя через контейнер.
В класс LocationInstaller добавляем строчку
Container.InstantiatePrefabForComponent<HeroController>(HeroPrefab, StartPoint.position, Quaternion.identity, null);

using Zenject;
public class LocationInstaller : MonoInstaller
{
    public Transform StartPoint;
    public GameObject HeroPrefab;
    public override void InstallBindings()
    {
        HeroController heroController = Container
            .InstantiatePrefabForComponent<HeroController>(HeroPrefab, StartPoint.position, Quaternion.identity, null);
    }

Сейчас при запуске сцены наш кубик будет создаваться на сцене из префаба.

HeroController heroController = Container
.InstantiatePrefabForComponent(HeroPrefab, StartPoint.position, Quaternion.identity, null);

означает, что мы создаем объект из префаба и берем его компонент “HeroController“.

Binding

Далее будем “биндить” компонент куба “HeroController“.

Привязка (Binding): Процесс регистрации типов и их зависимостей в контейнере.
После регистрации контейнер может отдать “по типу” эту реализацию сущностям которым она понадобится.
Для сервисов типичной является ситуация когда регистрация осуществляется по интерфейсу, ибо интерфейсы позволяют создавать нужные реализации под конкретные задачи ( например инпут под различные платформы)

Полный формат команды bind выглядит следующим образом. Обратите внимание, что в большинстве случаев вы не будете использовать все эти методы, и что у всех них есть логические значения по умолчанию, если они не указаны.

Container.Bind<ContractType>()
    .WithId(Identifier)
    .To<ResultType>()
    .FromConstructionMethod()
    .AsScope()
    .WithArguments(Arguments)
    .OnInstantiated(InstantiatedCallback)
    .When(Condition)
    .(Copy|Move)Into(All|Direct)SubContainers()
    .NonLazy()
    .IfNotBound();
  • ContractType = Тип, для которого вы создаете привязку.
    • Это значение будет соответствовать типу вводимого поля/параметра.
  • ResultType = Тип для привязки.
    • По умолчанию: ContractType
    • Этот тип должен быть равен ContractType или быть производным от ContractType . Если не указано иное, предполагается ToSelf(), что означает, что ResultType будет таким же, как ContractType . Это значение будет использоваться любым значением, указанным в качестве ConstructionMethod , для получения экземпляра этого типа
  • Identifier = значение, используемое для уникальной идентификации привязки. В большинстве случаев это можно игнорировать, но может быть весьма полезно, когда нужно различать несколько привязок с одним и тем же типом контракта. Подробнее см. здесь .
  • ConstructionMethod = Метод создания/извлечения экземпляра ResultType . Подробнее о различных методах построения см. в этом разделе .
    • Bind<ContractType>().FromNew — создание с помощью оператора new в C#. Создает новый экземпляр каждый раз, когда запрашивается.
    • Bind<ContractType>().FromGetter
    • Bind<ContractType>().FromMethod — создать с помощью пользовательского метода
    • Bind<ContractType>().FromResolve — получение экземпляра путём повторного поиска по контейнеру (то есть вызова DiContainer.Resolve()). Обратите внимание, что для этого ResultType должен быть привязан в отдельном операторе привязки.
    • Bind<ContractType>().FromComponentInNewPrefab — Создать экземпляр из префаба
    • Bind<ContractType>().FromSubContainerResolve — Получить экземпляр, выполнив поиск по подконтейнеру. Обратите внимание, что для этого у подконтейнера должна быть привязка к ResultType.
    • Bind<ContractType>().FromInstance — Использовать существующий заранее созданный экземпляр.
    • Bind<ContractType>().FromComponentInHierarchy(); – Найти существующий компонент типа ContractType на сцене.
    • etc.
  • Scope (Область действия) = Это значение определяет, как часто (и используется ли вообще) сгенерированный экземпляр повторно в нескольких инъекциях.
    • Значение по умолчанию: AsTransient. Обратите внимание, что не все привязки имеют значение по умолчанию, поэтому, если оно не указано, будет сгенерировано исключение. Привязки, не требующие явного указания области действия, — это любые привязки с методом конструирования, который представляет собой поиск, а не создание нового объекта с нуля (например, FromMethod, FromComponentX, FromResolve и т. д.).
    • Это может быть одно из следующего::
      1. AsTransient – экземпляр не будет использоваться повторно. При каждом запросе ContractType DiContainer будет повторно выполнять заданный метод конструирования.
      2. AsCached – будет повторно использовать один и тот же экземпляр ResultType каждый раз при запросе ContractType , который будет лениво сгенерирован при первом использовании.
      3. AsSingle – то же самое, что и AsCached, за исключением того, что иногда возникают исключения, если привязка для ResultType уже существует . Это просто способ гарантировать уникальность данного ResultType в контейнере. Однако следует отметить, что это гарантирует только наличие одного экземпляра в данном контейнере, а это означает, что использование AsSingle с той же привязкой в подконтейнере может сгенерировать второй экземпляр.
    • В большинстве случаев вы, скорее всего, захотите использовать AsSingle, однако AsTransient и AsCached также имеют свое применение.
  • Arguments = Список объектов, используемых при создании нового экземпляра типа ResultType . Это может быть полезно в качестве альтернативы добавлению других привязок для аргументов в форме Container.BindInstance(arg).WhenInjectedInto<ResultType>()
  • InstantiatedCallback = В некоторых случаях полезно иметь возможность настраивать объект после его создания. В частности, при использовании сторонней библиотеки может потребоваться изменить несколько полей одного из его типов. В таких случаях можно передать метод OnInstantiated, который настроит вновь созданный экземпляр.

В функцию InstallBindings() добавляем строку:
Container.Bind<HeroController>().FromInstance(heroController).AsSingle();

using UnityEngine;
using Zenject;
namespace Infrastructure
{
    public class LocationInstaller : MonoInstaller
    {
        public GameObject HeroPrefab;

        public override void InstallBindings()
        {
            HeroController heroController = Container
                .InstantiatePrefabForComponent<HeroController>(HeroPrefab,
                    StartPoint.position, Quaternion.identity,
                    null);

            Container
                .Bind<HeroController>()
                .FromInstance(heroController)
                .AsSingle();
        }
    }
}

тут возможны варианты
.AsSingle() – как и .AsCached() но как и синглтон бросает экзепшен если будет попытка создать инстанс по такому же типу.
.AsCached() – сохраняет до уничтожения контейнера.
.AsTransient() – по умолчанию. при каждом запросе будет вызываться код, создающий экземпляр (запросили – создали – использовали – уничтожили).
.NonLazy()– заставляет контейнер создать экземпляр немедленно при старте, а не при первом запросе. это полезно для менеджеров типа аналитики, рекламы и другим, которые начинают работать до основного старта геймплея.
( Container.Bind().FromInstance(heroController).AsSingle() .NonLazy() )

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

На данный момент мы зарегистрировали HeroController в контейнере. теперь мы можем запросить его из кода врага.
Лучше всего инжектить в конструктор. Если инжектим в MonoBehaviour то
создаем метод с атрибутом [Inject]

Создаем инжекцию в классе EnemyController

using UnityEngine;
using Zenject;

public class EnemyController : MonoBehaviour
{
    [Inject]
    private void Construct(HeroController heroController)
    {
        heroController.ChangeLocalScale();
    }
} 

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

Эксперимент-2. Binding объекта сцены и Injection в создаваемый из префаба объект

Предположим на сцене есть объект который отображает данные героя (например инвентарь) и созданному из префаба герою нужно что-то туда записывать

Создадим на сцене UI панель и добавим на неё три текстовых поля. UI панели добавим класс InventoryDisplay

using UnityEngine;
using TMPro;
public class InventoryDisplay : MonoBehaviour
{
    public TMP_Text[] HotbarSlots;
}

В редакторе перетянем на TMP_Text[] три текстовых поля.

биндим InventoryDisplay в LocationInstaller

первым делом в LocationInstaller создадим переменную которой назначим InventoryDisplay UI панели

 public InventoryDisplay inventoryDisplay;
            Container
                .Bind<InventoryDisplay>()
                .FromInstance(inventoryDisplay)
                .AsSingle();

полный LocationInstaller имеет вид

using UnityEngine;
using Zenject;
namespace Infrastructure
{
    public class LocationInstaller : MonoInstaller
    {
        public Transform StartPoint;
        public GameObject HeroPrefab;

        public InventoryDisplay inventoryDisplay;

        public override void InstallBindings()
        {
            BindInventoryDisplay();
            BindHero();
        }

        private void BindInventoryDisplay()
        {
            Container
                .Bind<InventoryDisplay>()
                .FromInstance(inventoryDisplay)
                .AsSingle();
        }

        private void BindHero()
        {
            HeroController heroController = Container
                .InstantiatePrefabForComponent<HeroController>(HeroPrefab, StartPoint.position, Quaternion.identity,
                    null);

            Container
                .Bind<HeroController>()
                .FromInstance(heroController)
                .AsSingle();
        }
    }
}

теперь мы можем инжектить в класс героя HeroController, но мы не будем создавать там кашу и для героя создадим специальный класс который будет отвечать за инвентарь CharacterInventory

using Zenject;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

namespace Infrastructure
{
    public class CharacterInventory : MonoBehaviour
    {
        TMP_Text[] HotbarDisplayHolders = new TMP_Text[3];

        [Inject]
        public void Construct(InventoryDisplay inventoryDisplay)
        {
            HotbarDisplayHolders = inventoryDisplay.HotbarSlots;
        }

        void Start()
        {
            HotbarDisplayHolders[1].text = "герой изменил текст";
        }

    }
}

Запустим проект и наблюдаем как герой внес изменение в текстовое поле.

Эксперимент-3. Создание сервиса в ProjectContext

Пришло время сотворить что-нибудь используя ProjectContext.
Как уже говорилось ранее ProjectContext нужен для создания сервисов которые будут работать во всем приложении. Первое что приходит на ум, так это какой-нибудь InputService отвечающий за ввод. Построим сервис на интерфейсе чтоб иметь возможность переключать ввод с клавиатуры на что-то другое. Сервис будет содержать пару event Action на которые будет подписываться герой. Усложним себе задачу тем что сделаем сервис НЕ MonoBehaviour

Первым делом создадим интерфейс IInputService с двумя event Action ClickOne и ClickTwo

using System;

public interface IInputService
{
    event Action ClickOne;
    event Action ClickTwo;
}

Создадим класс наследник KeyboardInput. Поскольку мы усложнили себе жизнь, решив сделать все без MonoBehaviour, возникает большая проблема. В Unity событие нажатия клавиатуры (Input.GetKey, Input.GetKeyDown, Input.GetKeyUp) обрабатывается только в игровом цикле — то есть внутри методов, которые Unity сама вызывает (Update, LateUpdate, OnGUI и т.д.) в MonoBehaviour.
Чтоб решить эту проблему, пойдем на хитрость. В классе KeyboardInput мы реализуем метод Tick() интерфейса ITickable. Tick() – аналог Update(). Благодаря этому мы сможем вызывать Action ClickOne и ClickTwo при нажатии клавиш “А” и “B”

using System;
using System.Linq;
using UnityEngine;
using UnityEngine.LowLevel;
public class KeyboardInput : IInputServiсe, ITickable
{
    public event Action ClickOne;
    public event Action ClickTwo;

    public void Tick()
    {
        if (Input.GetKey(KeyCode.A))
        {
            ClickOne?.Invoke();
        }
        if (Input.GetKey(KeyCode.B))
        {
            ClickTwo?.Invoke();
        }
    }
}

Биндим KeyboardInput в BootstrapInstaller предварительно создав обьект класса KeyboardInput биндим с .NonLazy(), так как хотим чтоб контейнер создался немедленно. Не забываем забиндить интерфейс ITickable в keyboardInput , если этого не сделать Tick() работать не будет. Нажатие клавиш при отсутствии подписантов не вызовет проблем так как в KeyboardInput используется конструция вида хххххх?.Invoke().

using Zenject;
public class BootstrapInstaller : MonoInstaller
{
    KeyboardInput keyboardInput;
    public override void InstallBindings()
    {
        keyboardInput = new KeyboardInput();

        Container
            .Bind<IInputServiсe>()
            .FromInstance(keyboardInput)
            .AsSingle()
            .NonLazy();
            
        Container
            .Bind<ITickable>()
            .FromInstance(keyboardInput)
            .AsSingle()
            .NonLazy();
    }
}

Инжектим контейнер в HeroController и подписываемся на события.
При событии ClickOne будем уменьшать героя. При ClickTwo – увеличиваем героя,

using UnityEngine;
using Zenject;
public class HeroController : MonoBehaviour
{
    IInputServiсe _inputServiсe;

    [Inject]
    public void Construct(IInputServiсe inputServiсe)
    {
        _inputServiсe = inputServiсe;
        _inputServiсe.ClickOne += ClickOne;
        _inputServiсe.ClickTwo += ClickTwo;
    }

    private void ClickOne()
    {
        gameObject.transform.localScale = new Vector3(1, 1, 1);
    }

    private void ClickTwo() 
    { 
        gameObject.transform.localScale = new Vector3(3, 3, 3); 
    }


    public void ChangeLocalScale()
    {
        gameObject.transform.localScale = new Vector3(2, 2, 2);
    }

    void OnDestroy()
    {
        _inputServiсe.ClickOne -= ClickOne;
        _inputServiсe.ClickTwo -= ClickTwo;
    }
}

запускаем проект и наблюдаем уменьшение и увеличивание героя при нажатии “А” и “В”.

Благодаря использованию интерфейса мы можем менять тип ввода внося изменения лиш в один класс – BootstrapInstaller.

Эксперимент-4. Фабрика

Переименуем сферу-врага в EnemyMelee. Создадим копию переместим в другую случайную позицию и назовем EnemyRange.
Создадим enum c врагами.

public enum EnemyType
{
    Melee = 0,
    ranged = 1
}

Создадим из врагов префабы и удалим их со сцены. На сцене будут располагаться специальные маркеры врагов.
Создадим два объекта-пустышки EnemyMarker и EnemyMarker1. Расположим их в местах появления врагов и повесим на них новый класс EnemyMarker. Он будет отрисовывать в Gizmos красную сферу обозначающую врага.

using UnityEngine;

public class EnemyMarker : MonoBehaviour
{
    public EnemyType enemyType;
    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawSphere(transform.position, 1);
        Gizmos.color = Color.white;
    }
}

Создадим фабрику Enemys

В ZENJECT есть свои фабрики, но мы создадим свою.
создаем клас EmenyFactory Он должен уметь загружать префабы врагов и создавать врагов из префабов. EmenyFactory будет наследоватся от интерфейса IEmenyFactory.

using UnityEngine;

public interface IEnemyFactory
{
    void Create(EnemyType enemyType, Vector3 at);
    void Load();
}


public class EnemyFactory : IEnemyFactory
{
    public Object _meleeEnemyPrefab;
    public Object _rangeEnemyPrefab;

    public EnemyFactory(DiContainer diContainer)
    {
        _diContainer = diContainer;
    }

    public void Load() {}
    public void Create(EnemyType enemyType, Vector3 at) { }
}

Сейчас отложим фабрику в сторону и вернемся к LocationInstaller.
Дело в том, что фабрику нужно запустить в какой-то момент времени. На первый взгляд кажется, что сделать это можно в InstallBindings(), но это будет ошибкой.

InstallBindings() — это момент конфигурации контейнера, а не выполнения логики игры.


В нём:

  • Регистрируются зависимости (Bind, FromComponentInHierarchy, To<>() и т.д.)
  • Но игровой мир ещё не загружен до конца: сцена может не иметь всех объектов, некоторые MonoBehaviour ещё не проинициализированы.

рискуем:

  • Создать врагов до того, как сцена полностью загрузилась
  • Столкнуться с тем, что фабрика или её зависимости ещё не собраны
  • Поймать NullReferenceException

Чтобы решить эту проблему, в LocationInstaller добавим интерфуйс IInitializable.

IInitializable — это интерфейс, у которого один метод, void Initialize().

Zenject автоматически вызывает Initialize() у всех зарегистрированных IInitializable после того, как:

  • Все InstallBindings() во всех установщиках завершены
  • Все зависимости внедрены
  • Сцена и контейнер полностью собраны

В момент Initialize():

  • Контейнер уже готов, все EnemyMarker в сцене уже есть.
  • Фабрика enemyFactory уже полностью сконфигурирована.
  • Можно безопасно пройтись по всем EnemyMarker и создать врагов.

По сути, это инициализация сцены после загрузки.

Теперь зная немного теории, забиндим фабрику в методе InstallBindings()

 Container.Bind<IEnemyFactory>().To<EnemyFactory>().AsSingle();

как говорилось ранее:
В момент калбека Initialize() контейнер уже готов, все EnemyMarker в сцене уже есть. Фабрика enemyFactory уже полностью сконфигурирована.

В Initialize() заресолвим фабрику. Загрузим префабы врагов в фабрику. Пройдемся по Маркерам создавая Enemy.

  public void Initialize()
    {
        // DiContainer.Resolve — возвращает экземпляр, соответствующий заданному типу
        // этот кодвыполнится так как в конструкторе забиндили фабрику
        var enemyFactory = Container.Resolve<IEnemyFactory>();
        enemyFactory.Load();

        foreach (EnemyMarker marker in EnemyMarkers)
        {
            enemyFactory.Create(marker.enemyType, marker.transform.position);
        }
    }

В метод InstallBindings() класса LocationInstaller добавим код, в котором забиндим сам LocationInstaller (это понадобится в фабрике)

 Container.BindInterfacesTo<LocationInstaller>().FromInstance(this).AsSingle();

Теперь, когда мы все подготовили в LocationInstaller возвращаемся в EnemyFactory.
добавим загрузку префабов.

 public void Load()
    {
        _meleeEnemyPrefab = Resources.Load(PathEnemyMelee);
        _rangeEnemyPrefab = Resources.Load(PathEnemyRange);
    }

Инстанцировать Enemy из префаба будем с помощью ранее забинденого LocationInstaller.
Для этого в конструкторе EnemyFactory получаем зависимостью сам контейнер

  public EnemyFactory(DiContainer diContainer)
    {
        _diContainer = diContainer;
    }

Благодаря этому ХАКу мы можем инстанцировать врагов в фабрике с помощъю diContainer.

public void Create(EnemyType enemyType, Vector3 at)
    {
        switch (enemyType)
        {
            case EnemyType.Melee:
                _diContainer.InstantiatePrefab(_meleeEnemyPrefab, at, Quaternion.identity, null);
                break;
            case EnemyType.Ranged:
                _diContainer.InstantiatePrefab(_rangeEnemyPrefab, at, Quaternion.identity, null);
                break;
        }

    }

Полный код класса IEnemyFactory:

using UnityEngine;
using Zenject;

public class EnemyFactory : IEnemyFactory
{
    private const string PathEnemyMelee = "EnemyMelee";
    private const string PathEnemyRange = "EnemyRange";
    private readonly DiContainer _diContainer;
    public Object _meleeEnemyPrefab;
    public Object _rangeEnemyPrefab;

    public EnemyFactory(DiContainer diContainer)
    {
        _diContainer = diContainer;
    }

    public void Load()
    {
        _meleeEnemyPrefab = Resources.Load(PathEnemyMelee);
        _rangeEnemyPrefab = Resources.Load(PathEnemyRange);

    }
    public void Create(EnemyType enemyType, Vector3 at)
    {
        switch (enemyType)
        {
            case EnemyType.Melee:
                _diContainer.InstantiatePrefab(_meleeEnemyPrefab, at, Quaternion.identity, null);
                break;
            case EnemyType.Ranged:
                _diContainer.InstantiatePrefab(_rangeEnemyPrefab, at, Quaternion.identity, null);
                break;
        }

    }
}

Полный код класса LocationInstaller


using UnityEngine;
using Zenject;
public class LocationInstaller : MonoInstaller, IInitializable
{
    public Transform StartPoint;
    public GameObject HeroPrefab;
    public EnemyMarker[] EnemyMarkers;

    public InventoryDisplay inventoryDisplay;

    public override void InstallBindings()
    {
        BindInstallerInterfaces(); // LocationInstaller биндит сам себя
        BindInventoryDisplay();
        BindHero();
        BindEmenyFactory();
    }

    private void BindEmenyFactory()
    {
        Container
            .Bind<IEnemyFactory>()
            .To<EnemyFactory>()
            .AsSingle();
    }

    private void BindInstallerInterfaces()
    {
        Container.BindInterfacesTo<LocationInstaller>().FromInstance(this).AsSingle();
    }

    private void BindInventoryDisplay()
    {
        Container
            .Bind<InventoryDisplay>()
            .FromInstance(inventoryDisplay)
            .AsSingle();
    }

    private void BindHero()
    {
        HeroController heroController = Container
            .InstantiatePrefabForComponent<HeroController>(HeroPrefab, StartPoint.position, Quaternion.identity,
                null);

        Container
            .Bind<HeroController>()
            .FromInstance(heroController)
            .AsSingle();
    }
    // инициализацию объекта вне конструктора можем делать тут, удобно для фабрик и биндинга после резолва
    public void Initialize()
    {
        // DiContainer.Resolve — возвращает экземпляр, соответствующий заданному типу
        // этот кодвыполнится так как в конструкторе забиндили фабрику
        var enemyFactory = Container.Resolve<IEnemyFactory>();
        enemyFactory.Load();

        foreach (EnemyMarker marker in EnemyMarkers)
        {
            enemyFactory.Create(marker.enemyType, marker.transform.position);
        }
    }
}

После того как в редакторе в поля LocationInstaller перетянем маркеры врагов,можно запустить игру и убедится враги (шары) появились в местах расположения маркеров

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *