Zenject unity что это

Инструменты при разработке моей первой выпущенной игры

Здравствуйте! Мне всегда интересно какими инструментами пользовались те или иные разработчики, в связи с выпуском моей игры решил написать статью об инструментах, которые использовались при разработке с небольшими комментариями.

Это мобильная 2d игра на внимательность и скорость, комбинация hidden object и игры spot it. Главный персонаж в игре кот Бэйзил, который убегает от зомби. В игре есть набор локаций, которые содержат в себе различные уровни. Также в игре есть довольно стандартный для мобильной игры магазин и одежда для главного персонажа.

В команде я один, моя основная специализация — программист.

Unity — довольно стандартный выбор для мобильных игр. Каких-то явных проблем не было. Мне как программисту очень нравится статически типизированный C# и также scriptable objects. В целом unity меня полностью устроил.

Git — без контроля версий очень сложно работать хоть со сколько-нибудь большим проектом. Для моего проекта гит меня полностью устроил по всем критериям.

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

Trello (+ плагин для scrum) — очень простой и удобный в использовании инструмент для управлением задачами. Есть бесплатный вариант с ограничениями, для небольшой команды его вполне хватает. Плагин scrumfortrello предоставляет возможность использовать так называемые scrum очки. Я положительно отношусь к scrum и несмотря на то что в команде я один, я частично использовал его и в этом помогал плагин.

Rider — платная IDE от jetbrains для C#, которая имеет очень хорошую интеграцию с Unity.

Google Drive — использую как базу знаний и обменник данными. Для небольшого проекта достаточно бесплатного кол-ва гигабайт.

Inkscape — open source редактор векторной графики. Почти вся графика в игре векторная, большая часть это купленные ассеты, часть я рисовал сам. Для все этого использовал Inkscape.

OBS studio — open source видеорекордер. Использовал для записи видео геймплея с ПК.

Blender — open source мультимедиа редактор. Несмотря на то, что blender известен как редактор для 3d моделирования и анимирования, также у него есть ряд других функций. Я его использовал исключительно для монтирования видео.

Fungus — плагин визуального программирования. Бесплатный и очень простой в использовании инструмент, активно использовался на этапе прототипирования, но имеет ряд недостатков, из-за которых в релизе не использовался.

Doozy UI — плагин для создания UI. Данное решение не является полноценным и имеет ряд недостатков, но позволяет сократить время разработки. Есть хорошие туториалы и документация. Как минимум рекомендую ознакомиться с данным плагином.

Zenject — фреймворк для dependency injection (DI). Я сторонник того что синглтонов в проекте быть не должно (либо почти не должно). Даже если не использовать zenject крайне рекомендую ознакомиться с концепцией DI. В своей группе в контакте написал заметку по этому поводу.

Localization — плагин от unity для локализации. Еще находится в состоянии превью. Я его использовал только для локализации текста. Явных проблем не обнаружил.

AdMob — плагин от Google для интеграции рекламы.

Firebase — аналитика от Google.

Asset store — в основном тут брал только музыку, при этом часть получил через Humble Bundle со скидкой.

Humble Bundle — магазин, в котором периодически продают наборы различных ассетов и курсов по очень выгодной цене.

Freepik.com — сервис с большим количество арта. Часть ресурсов можно скачать бесплатно для некоммерческого использования. Сервис больше подходит для дизайна чем разработки игр, но для стиля моей игры он подошел.

Pinterest — сервис для поиска арта с продвинутой системой рекомендаций. Очень полезный инструмент для поиска идей и референсов.

vk.com — создал группу по разработке. Стараюсь иногда писать небольшие статьи. В целом как инструмент маркетинга для меня не работает, больше как личный блог.

Instagram — помимо арта из игры делаю посты с загадками и фотографиями с персонажем из игры. Посты стараюсь делать стабильно, несколько раз в неделю. За довольно короткое время набралось ощутимое количество подписчиков. Думаю, в качестве маркетинга для меня работает неплохо.

Twitter — в основном делаю только screenshot saturday посты. Несмотря на то, что подписчиков почти нету просмотров у постов получается почти столько же сколько и в инстаграмме.

Facebook — стараюсь делать посты вместе с инстаграмом, толку от этой страницы очень мало.

Pinterest — создаю пины со всеми постами из инстаграма. Сейчас количество человек просматривающих пины не очень большое, но в отличии от социальных сетей, старые пины могут быть найдены позже внутри сервиса или через Google.

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

Unity learn и Youtube — думаю это самый удобный способ изучать различные аспекты Unity. Очень полезные материалы бывают на Unite Now.

Надеюсь этот список будет полезен.

Буду очень рад если скачаете и оцените.

Спасибо за внимание!

Если какая тема окажется очень интересна, готов раскрыть более подробно в отдельной статье.

Если какая тема окажется очень интересна, готов раскрыть более подробно в отдельной статье.

Лично мне было бы очень интересно почитать про применение Zenject и DI в целом, с практическими примерами. Заметку в вк посмотрел, но хочется больше)

В заметке описывается только передача объектов между сценами, но применение DI, конечно, намного шире.
Вообще планировал написать более подробно про DI в различных аспектах, но довольно сложная тема, постараюсь написать)

Низкий поклон! Спасибо большое за статью и материалы! Желаю большой удачи!

Большое спасибо за отзыв!

А какие результаты то с игрой получились? Вроде речь идет о завершенном проекте, но без цифр скачек/заработка/ретеншенов/конверсии/вотэва сложно судить о результате.

На данный момент игра находится на этапе софт ланча в России, так что говорить о каких-либо показателях, думаю, рано.

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

Как основной инструмент продвижения сейчас я использую google ads.

А как заманиваешь народ в инстаграм и вк?

В основном только с помощью #screenshotsaturday, стараюсь делать посты в различных группах, социальных сетях и на DTF
Для вк видимо это плохо работает, хотя думаю зависит от игры сильно, а в инстаграм часто приходят люди по интересам, такие же инди-разработчики

Это настолько очевидная статья с настолько типичным подходом ко всему, что можно испытать некоторую боль от её прочтения. Тут явно нужны инсайты, глубина описания отдельных пунктов и причины некоторых выборов.

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

Тут скорее слишком много интригующих анонсов и выборов, но по каждому коротенькая зарисовка и её мало для понимания. Если придёт новичок сюда, он, скорее всего, таким же новичком и выйдет из статьи, что обидно.

Не могу сравнивать VSCode в связке с Unity, т.к. не использовал такой вариант.

Выбор Rider обусловлен большим и очень положительным опытом работы с IDE от Jetbrains (основная специализация программист). Там есть мощная работа с самим языком (продвинутые поиски и рефакторинги), отличный git клиент и др.

Плагин с Unity позволяет искать использование скриптов как в коде так и в сценах и префабах, часто это очень полезно. Также есть большое количество подсказок в коде связанных со спецификой Unity.

Отмечу, что в данном проекте кода не очень много, так что выбор IDE не очень важен. Думаю, если нет какой-то специфичной ситуации, то по умолчанию подобный проект стоит начинать с VSCode.

Источник

Расширяемая и удобная в сопровождении архитектура игр на Unity

А пока предлагаем прочитать перевод полезной статьи.

Введение

За годы работы над множеством проектов я выработал четкий подход к структурированию игровых проектов в Unity, который зарекомендовал себя в особой степени расширяемым и удобным в сопровождении.

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

Эта статья является обновленной версией моего выступления на GDC в 2017 году (“Data Binding Architectures for Rapid UI Creation in Unity”).

Дисклеймер: вы должны понимать, что это лишь наработанные мной практические рекомендации, которые отражают мой опыт и взгляд на разработку, а не универсальное решение всех проблем и определенно не единственно правильный подход для каждого проекта или команды.

Второй дисклеймер: после того, как эта статья была опубликована, читатели обратили мое внимание, что я не одинок в данном подходе, поскольку Kolibri Games также практикует нечто подобное: их статья

Архитектура

Основными целями этой архитектуры являются:

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

Инверсия управления (inversion of control)

Интерфейс передачи сообщений (MPI)

Модель / представление / контроллер (MVC)

Модульное тестирование (Unit testing)

Инверсия управления

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

Внедрение зависимостей (DI — Dependency Injection) — это подход к реализации инверсии управления. На следующем рисунке показан предыдущий пример с использованием внедрения зависимостей:

Для реализации этого паттерна мы остановились на Zenject/Extenject. Он основан на рефлексии. Используя функцию запекания рефлексий (reflection-baking), мы можем избавиться от негативного влияния рефлексии на производительность.

Модель-Представление-Контроллер

Суть этой архитектуры — разбиение кода на отдельные уровни. Паттерн Модель-Представление-Контроллер (Model-View-Controller — MVC), перенесенный на Unity, выглядит следующим образом:

Monobehaviour-ы Unity обитают на уровне представления (View), что, как предполагается, защищает остальную часть архитектуры от затрудняющих модульное тестирование элементов Unity. Этот уровень имеет доступ только к уровню контроллера. Представление создает инстансы префабов и использует [SerializeField] для использования типичных drag’n’drop компонентов Unity. Здесь не должно быть никакой игровой логики, только чистая визуализация данных.

Уровень контроллера содержит бизнес-логику и выполняет всю тяжелую работу. Этот код должен быть тестируемым, он не зависит от специфики уровня представления Unity. Но все же этот уровень не определяет способ хранения данных на уровне модели, он только контролирует изменения на нем.

Модель содержит фактические данные, они могут быть эфемерными, хранимыми на диске или в каком-либо бэкенде. Обычно модели — это старые добрые, хорошо нам известные типы данных.

Поскольку представление не должно запрашивать информацию об изменении данных, для его уведомления мы используем передачу сообщений (Message Passing). Так мы можем сохранять слои обособленными друг от друга и при этом сохранять производительность.

Решение о том, считывает ли представление данные прямо из модели или через контроллер, не является каким-нибудь догматом. Единственное правило: изменения происходят только через уровень контроллера. Считывание значений может происходить прямо из модели.

Передача сообщений

Вышеупомянутая архитектура полагается на соответствующих уведомлениях (notification messages), чтобы уровень представления мог подписаться и реагировать на изменения/события (events):

Следующий код является примером его использования:

Важно отметить, что сигналы (Signals) должны быть легковесными и не содержать данных — для этого мы используем остальные уровни MVC. Сигналы — это инструмент чисто для уведомления, распространения событий и уменьшения связанности кода.

Альтернативой этому подходу является использование инструментов для наблюдения за изменениями данных в модели, таких как UniRx, но я предпочитаю иметь более строгий контроль над тем, когда мы хотим уведомлять об изменениях, вместо того, чтобы позволять представлению видеть каждое отдельное изменение значения. Решение о том, когда уведомлять, следует принимать на уровне контроллера, и поэтому сигналы сюда подходят лучше.

Модульное тестирование

Благодаря всем вышеперечисленным ограничениям и механизмам мы теперь можем покрыть модульными тестами почти всю нашу игровую (бизнес) логику.

Для реализации технической части написания этих тестов мы используем стандартный фреймворк Unity NUnit и NSubstitute в качестве решения для создания моков.

Давайте посмотрим на один из наших тестов:

Вышеупомянутый тест проверяет правильность поведения контроллера при загрузке дефолтных данных. Вы можете увидеть, как мы используем NSubstitute , чтобы мокать зависимости и даже утверждать, что для них были вызваны определенные методы.

Давайте посмотрим на более интересный пример билдинга чего-либо на слоте 0:

Теперь мы проверяем, что наш GetCurrentBuildCount возвращает правильное количество новых билдов после успешного билда на слоте 0. Мы также ожидаем, что на шину будет отправлен правильный сигнал — таким образом, соответствующее представление сможет обновиться.

«Погодите-ка, нельзя мокать то, что имеет корни в Zenject?» (что очень метко сказано моим хорошим другом Питером)

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

Заключение

Это было всего лишь взгляд с высоты птичьего полета на эту тему. Но подведем итоги:

Мы хотим иметь возможность писать тестируемый код, поэтому мы максимально отделяем Unity от нашей бизнес-логики, общаемся с Unity посредством сообщений, и у нас есть четкий интерфейс от Unity для доступа к данным. При этом у нас есть небольшая область того, что специфично для Unity и не может быть протестировано (игнорируя playmode тесты).

В будущих статьях мы напишем конкретный пример игры, чтобы применить все это на практике, и, кроме того, посмотрим, как объединить эту архитектуру с:

практическим примером применения этих подходов,

мокингом сцены для тестирования пользовательского интерфейса

фейковыми бэкендами и сторонними SDK

промисами для поддерживаемого асинхронного кода

Источник

Zenject unity что это

Dependency Injection Framework for Unity3D

If you are looking for the older documentation for Zenject 3.X click here.

Many hours have gone into the creation of this framework and many more will go to continue maintaining it. If you or your team have found it useful, consider buying me a coffee! Every donation makes me significantly more likely to find time for it.

Zenject unity что это. btn donate SM. Zenject unity что это фото. Zenject unity что это-btn donate SM. картинка Zenject unity что это. картинка btn donate SM

Also, if you like Zenject, you may also be interested in Projeny (our other open source project)

Zenject is a lightweight dependency injection framework built specifically to target Unity 3D. It can be used to turn your Unity 3D application into a collection of loosely-coupled parts with highly segmented responsibilities. Zenject can then glue the parts together in many different configurations to allow you to easily write, re-use, refactor and test your code in a scalable and extremely flexible way.

Tested in Unity 3D on the following platforms:

This project is open source. You can find the official repository here.

For general troubleshooting / support, please use the zenject subreddit or the zenject google group. If you have found a bug, you are also welcome to create an issue on the github page, or a pull request if you have a fix / extension. You can also follow @Zenject on twitter for updates. Finally, you can also email me directly at sfvermeulen@gmail.com

You can install Zenject using any of the following methods

From Releases Page. Here you can choose between the following:

Unity is a fantastic game engine, however the approach that new developers are encouraged to take does not lend itself well to writing large, flexible, or scalable code bases. In particular, the default way that Unity manages dependencies between different game components can often be awkward and error prone.

This project was started after reading a series of great articles by Sebastiano Mandalà outlining the problem. Sebastiano even wrote a proof of concept and open sourced it, which became the basis for this library. Zenject also takes a lot of inspiration from Ninject (as implied by the name).

Finally, I will just say that if you don’t have experience with DI frameworks, and are writing object oriented code, then trust me, you will thank me later! Once you learn how to write properly loosely coupled code using DI, there is simply no going back.

The Zenject documentation is split up into the following sections. It is split up into two parts (Introduction and Advanced) so that you can get up and running as quickly as possible. I would recommend at least reading the Introduction section, but then feel free to jump around in the advanced section as necessary

You might also benefit from playing with the provided sample projects (which you can find by opening Zenject/OptionalExtras/SampleGame1 or Zenject/OptionalExtras/SampleGame2 ).

You may also find the cheatsheet at the bottom of this page helpful in understanding some typical usage scenarios.

The tests may also be helpful to show usage for each specific feature (which you can find at Zenject/OptionalExtras/UnitTests and Zenject/OptionalExtras/IntegrationTests )

What follows is a general overview of Dependency Injection from my perspective. However, it is kept light, so I highly recommend seeking other resources for more information on the subject, as there are many other people (often with better writing ability) that have written about the theory behind it.

Also see here for a video that serves as a nice introduction to the theory.

When writing an individual class to achieve some functionality, it will likely need to interact with other classes in the system to achieve its goals. One way to do this is to have the class itself create its dependencies, by calling concrete constructors:

This works fine for small projects, but as your project grows it starts to get unwieldy. The class Foo is tightly coupled to class ‘SomeService’. If we decide later that we want to use a different concrete implementation then we have to go back into the Foo class to change it.

After thinking about this, often you come to the realization that ultimately, Foo shouldn’t bother itself with the details of choosing the specific implementation of the service. All Foo should care about is fulfilling its own specific responsibilities. As long as the service fulfills the abstract interface required by Foo, Foo is happy. Our class then becomes:

This is better, but now whatever class is creating Foo (let’s call it Bar) has the problem of filling in Foo’s extra dependencies:

And class Bar probably also doesn’t really care about what specific implementation of SomeService Foo uses. Therefore we push the dependency up again:

So we find that it is useful to push the responsibility of deciding which specific implementations of which classes to use further and further up in the ‘object graph’ of the application. Taking this to an extreme, we arrive at the entry point of the application, at which point all dependencies must be satisfied before things start. The dependency injection lingo for this part of the application is called the ‘composition root’. It would normally look like this:

DI frameworks such as Zenject simply help automate this process of creating and handing out all these concrete dependencies, so that you don’t need to explicitly do it like in the above code.

There are many misconceptions about DI, due to the fact that it can be tricky to fully wrap your head around at first. It will take time and experience before it fully ‘clicks’.

As shown in the above example, DI can be used to easily swap different implementations of a given interface (in the example this was ISomeService). However, this is only one of many benefits that DI offers.

More important than that is the fact that using a dependency injection framework like Zenject allows you to more easily follow the ‘Single Responsibility Principle’. By letting Zenject worry about wiring up the classes, the classes themselves can just focus on fulfilling their specific responsibilities.

Another common mistake that people new to DI make is that they extract interfaces from every class, and use those interfaces everywhere instead of using the class directly. The goal is to make code more loosely coupled, so it’s reasonable to think that being bound to an interface is better than being bound to a concrete class. However, in most cases the various responsibilities of an application have single, specific classes implementing them, so using interfaces in these cases just adds unnecessary maintenance overhead. Also, concrete classes already have an interface defined by their public members. A good rule of thumb instead is to only create interfaces when the class has more than one implementation. This is known, by the way, as the Reused Abstraction Principle)

Other benefits include:

Also see here for further justification for using a DI framework.

Hello World Example

You can run this example by doing the following:

There are many different ways of binding types on the container, which are documented in the next section. There are also several ways of having these dependencies injected into your classes. These are:

Field injection occurs immediately after the constructor is called. All fields that are marked with the [Inject] attribute are looked up in the container and given a value. Note that these fields can be private or public and injection will still occur.

Property injection works the same as field injection except is applied to C# properties. Just like fields, the setter can be private or public in this case.

Method Inject injection works very similarly to constructor injection.

Note that these methods are called after all other injection types. It is designed this way so that these methods can be used to execute initialization logic which might make use of one of these dependencies. Note also that you can leave the parameter list empty if you just want to do some initialization logic only.

Note that there can be any number of inject methods. In this case, they are called in the order of Base class to Derived class. This can be useful to avoid the need to forward many dependencies from derived classes to the base class via constructor parameters, while also guaranteeing that the base class inject methods complete first, just like how constructors work.

Note that the dependencies that you receive via inject methods should themselves have already been injected. This can be important if you use inject methods to perform some basic initialization, since you may need the given dependencies to themselves be initialized via their Inject methods.

Using [Inject] methods to inject dependencies is the recommended approach for MonoBehaviours, since MonoBehaviours cannot have constructors.

Recommendations

Every dependency injection framework is ultimately just a framework to bind types to instances.

In Zenject, dependency mapping is done by adding bindings to something called a container. The container should then ‘know’ how to create all the object instances in your application, by recursively resolving all dependencies for a given object.

When the container is asked to construct an instance of a given type, it uses C# reflection to find the list of constructor arguments, and all fields/properties that are marked with an [Inject] attribute. It then attempts to resolve each of these required dependencies, which it uses to call the constructor and create the new instance.

Each Zenject application therefore must tell the container how to resolve each of these dependencies, which is done via Bind commands. For example, given the following class:

You can wire up the dependencies for this class with the following:

This tells Zenject that every class that requires a dependency of type Foo should use the same instance, which it will automatically create when needed. And similarly, any class that requires the IBar interface (like Foo) will be given the same instance of type Bar.

The full format for the bind command is the following. Note that in most cases you will not use all of these methods and that they all have logical defaults when unspecified

ContractType = The type that you are creating a binding for.

ResultType = The type to bind to.

ConstructionMethod = The method by which an instance of ResultType is created/retrieved. See this section for more details on the various construction methods.

WithGameObjectName = The name to give the new Game Object associated with this binding.

UnderGameObjectGroup = The name of the game object group to place the new game object under. This is especially useful for factories, which can be used to create many copies of a prefab for example.

Scope = This value determines how often (or if at all) the generated instance is re-used across multiple injections.

It can be one of the following:

In most cases, you will likely want to just use AsSingle, however AsTransient and AsCached have their uses too.

To illustrate the difference between the different scope types, see the following example:

Arguments = A list of objects to use when constructing the new instance of type ResultType. This can be useful as an alternative to Container.BindInstance(arg).WhenInjectedInto ()

Condition = The condition that must be true for this binding to be chosen. See here for more details.

InheritInSubContainers = If supplied, then this binding will automatically be inherited from any subcontainers that are created from it. In other words, the result will be equivalent to copying and pasting the Container.Bind statement into the installer for every sub-container.

NonLazy = Normally, the ResultType is only ever instantiated when the binding is first used (aka «lazily»). However, when NonLazy is used, ResultType will immediately by created on startup.

Often, there is some collections of related bindings for each sub-system and so it makes sense to group those bindings into a re-usable object. In Zenject this re-usable object is called an ‘installer’. You can define a new installer as follows:

You add bindings by overriding the InstallBindings method, which is called by whatever Context the installer has been added to (usually this is SceneContext ). MonoInstaller is a MonoBehaviour so you can add FooInstaller by attaching it to a GameObject. Since it is a GameObject you can also add public members to it to configure your installer from the Unity inspector. This allows you to add references within the scene, references to assets, or simply tuning data (see here for more information on tuning data).

Note that in order for your installer to be triggered it must be attached to the Installers property of the SceneContext object. The installers are executed in the order given to SceneContext however this order should not usually matter (since nothing should be instantiated during the install process)

In many cases you want to have your installer derive from MonoInstaller, so that you can have inspector settings. There is also another base class called simply Installer which you can use in cases where you do not need it to be a MonoBehaviour.

You can also call an installer from another installer. For example:

One of the main reasons we use installers as opposed to just having all our bindings declared all at once for each scene, is to make them re-usable. This is not a problem for installers of type Installer because you can simply call FooInstaller.Install as described above for every scene you wish to use it in, but then how would we re-use a MonoInstaller in multiple scenes?

There are three ways to do this.

Prefab instances within the scene. After attaching your MonoInstaller to a gameobject in your scene, you can then create a prefab out of it. This is nice because it allows you to share any configuration that you’ve done in the inspector on the MonoInstaller across scenes (and also have per-scene overrides if you want). After adding it in your scene you can then drag and drop it on to the Installers property of a Context

Prefabs. You can also directly drag your installer prefab from the Project tab into the InstallerPrefabs property of SceneContext. Note that in this case you cannot have per-scene overrides like you can when having the prefab in your scene, but can be nice to avoid clutter in the scene.

Prefabs within Resources folder. You can also place your installer prefabs underneath a Resoures folder and install them directly from code by using the Resources path. For details on usage see here.

When calling installers from other installers it is common to want to pass parameters into it. See here for details on how that is done.

In many cases it is preferable to avoid the extra weight of MonoBehaviours in favour of just normal C# classes. Zenject allows you to do this much more easily by providing interfaces that mirror functionality that you would normally need to use a MonoBehaviour for.

For example, if you have code that needs to run per frame, then you can implement the ITickable interface:

Then it’s just a matter of including the following in one of your installers:

Or if you don’t want to have to remember which interfaces Ship implements:

Note that the order that Tick() is called on all ITickables is also configurable, as outlined here.

Also note that there are interfaces ILateTickable and IFixedTickable which work similarly for the other unity update methods.

If you have some initialization that needs to occur on a given object, you could include this code in the constructor. However, this means that the initialization logic would occur in the middle of the object graph being constructed, so it may not be ideal.

Note that the constructors for the initial object graph are called during Unity’s Awake event, and that the IInitializable.Initialize methods are called immediately on Unity’s Start event. Using IInitializable as opposed to a constructor is therefore more in line with Unity’s own recommendations, which suggest that the Awake phase be used to set up object references, and the Start phase should be used for more involved initialization logic.

IInitializable works well for start-up initialization, but what about for objects that are created dynamically via factories? (see this section for what I’m referring to here). For these cases you will most likely want to use an [Inject] method:

If you have external resources that you want to clean up when the app closes, the scene changes, or for whatever reason the context object is destroyed, you can simply declare your class as IDisposable like below:

Then in your installer you can include:

Or you can use the following shortcut:

This works because when the scene changes or your unity application is closed, the unity event OnDestroy() is called on all MonoBehaviours, including the SceneContext class, which then triggers Dispose() on all objects that are bound to IDisposable

Note that this example may or may not be a good idea (for example, the file will be left open if your app crashes), but illustrates the point 🙂

Using the Unity Inspector To Configure Settings

One implication of writing most of your code as normal C# classes instead of MonoBehaviour’s is that you lose the ability to configure data on them using the inspector. You can however still take advantage of this in Zenject by using the following pattern, as seen in the sample project:

Note that if you follow this method, you will have to make sure to always include the [Serializable] attribute on your settings wrappers, otherwise they won’t show up in the Unity inspector.

Another way to do this is to use ScriptableObjectInstaller to store settings, which have the added advantage that you can change your settings at runtime and have those changes automatically persist when play mode is stopped. See here for details.

Object Graph Validation

The usual workflow when setting up bindings using a DI framework is something like this:

This works ok for small projects, but as the complexity of your project grows it is often a tedious process. The problem gets worse if the startup time of your application is particularly bad, or when the exceptions only occur from factories at various points at runtime. What would be great is some tool to analyze your object graph and tell you exactly where all the missing bindings are, without requiring the cost of firing up your whole app.

In many cases, you have a number of MonoBehaviour’s that have been added to the scene within the Unity editor (ie. at editor time not runtime) and you want to also have these MonoBehaviour’s added to the Zenject Container so that they can be injected into other classes.

The usual way this is done is to add public references to these objects within your installer like this:

This works fine however in some cases this can get cumbersome. For example, if you want to allow an artist to add any number of Enemy objects to the scene, and you also want all those Enemy objects added to the Zenject Container. In this case, you would have to manually drag each one to the inspector of one of your installers. This is very error prone since its easy to forget one, or to delete the Enemy game object but forget to delete the null reference in the inspector for your installer, etc.

So another way to do this is to use the ZenjectBinding component. You can do this by adding a ZenjectBinding MonoBehaviour to the same game object that you want to be automatically added to the Zenject container.

For example, if I have a MonoBehaviour of type Foo in my scene, I can just add ZenjectBinding alongside it, and then drag the Foo component into the Component property of the ZenjectBinding component.

Zenject unity что это. AutoBind1. Zenject unity что это фото. Zenject unity что это-AutoBind1. картинка Zenject unity что это. картинка AutoBind1

Then our installer becomes:

When using ZenjectBinding this way, by default it will bind Foo using the Self method, so it is equivalent to the first example where we did this:

Which is also the same as this:

So if we duplicate this game object to have multiple game objects with Foo on them (and its ZenjectBinding ), they will all be bound to the Container this way. So after doing this, we would have to change GameRunner above to take a List otherwise we would get Zenject exceptions (see here for info on list bindings).

Also note that the ZenjectBinding component contains a Bind Type property in its inspector. By default this simply binds the instance as shown above but it can also be set to the following:

This bind type is equivalent to the following:

Note however, in this case, that GameRunner must ask for type IFoo in its constructor. If we left GameRunner asking for type Foo then Zenject would throw exceptions, since the BindAllInterfaces method only binds the interfaces, not the concrete type. If you want the concrete type as well then you can use:

This bind type is equivalent to the following:

This is the same as AllInterfaces except we can directly access Foo using type Foo instead of needing an interface.

The final property you will notice on the ZenjectBinding component is the «Context». This is completely optional and in most cases should be left unset. However, if you are using GameObjectContext in places, then the other value might be useful to you (see section on sub-containers for details)

«Context» will determine what container the component gets added to. If left unset, it will use whatever context the GameObject is in. In most cases this will be SceneContext, but if it’s inside a GameObjectContext it will be bound into that container instead. One important use case here is to drag the SceneContext into this field for one of the MonoBehaviour’s inside a GameObjectContext. This allows you to treat this MonoBehaviour as a Facade for the entire sub-container given by the GameObjectContext.

General Guidelines / Recommendations / Gotchas / Tips and Tricks

Do not use GameObject.Instantiate if you want your objects to have their dependencies injected

Best practice with DI is to only reference the container in the composition root «layer»

Do not use IInitializable, ITickable and IDisposable for dynamically created objects

Using multiple constructors

Prefer [Inject] methods to Start/Awake methods for dynamically created MonoBehaviours

Using Zenject outside of Unity

Lazily instantiated objects and the object graph

The order that things occur in is wrong, like injection is occurring too late, or Initialize() event is not called at the right time, etc.

Transient is the default scope

Please feel free to submit any other sources of confusion to sfvermeulen@gmail.com and I will add it here.

You can declare some dependencies as optional as follows:

If an optional dependency is not bound in any installers, then it will be injected as null.

If the dependency is a primitive type (eg. int, float, struct) then it will be injected with its default value (eg. 0 for ints).

You may also assign an explicit default using the standard C# way such as:

Note also that the [InjectOptional] is not necessary in this case, since it’s already implied by the default value.

Alternatively, you can define the primitive parameter as nullable, and perform logic depending on whether it is supplied or not, such as:

In many cases you will want to restrict where a given dependency is injected. You can do this using the following syntax:

Note that WhenInjectedInto is simple shorthand for the following, which uses the more general When() method:

The InjectContext class (which is passed as the context parameter above) contains the following information that you can use in your conditional:

When Zenject finds multiple bindings for the same type, it interprets that to be a list. So, in the example code below, Bar would get a list containing a new instance of Foo1, Foo2, and Foo3:

Also worth noting is that if you try and declare a single dependency of IFoo (like Bar below) and there are multiple bindings for it, then Zenject will throw an exception, since Zenject doesn’t know which instance of IFoo to use.

Also, if the empty list is valid, then you should mark your List constructor parameter (or [Inject] field) as optional (see here for details).

This all works great for each individual scene, but what if you have dependencies that you wish to persist permanently across all scenes? In Zenject you can do this by adding installers to a ProjectContext object.

Note also that this only occurs once. If you load another scene from the first scene, your ProjectContext will not be called again and the bindings that it added previously will persist into the new scene. You can declare ITickable / IInitializable / IDisposable objects in your global installers in the same way you do for your scene installers with the result being that IInitializable.Initialize is called only once across each play session and IDisposable.Dispose is only called once the application is fully stopped.

This works because the container defined for each scene is nested inside the global container that your global installers bind into. For more information on nested containers see here.

Note also that you can do the same thing for constructor/inject-method arguments as well:

In many cases, the ID is created as a string, however you can actually use any type you like for this. For example, it’s sometimes useful to use an enum instead:

You can also use custom types, as long as they implement the Equals operator.

Non Generic bindings

In some cases you may not know the exact type you want to bind at compile time. In these cases you can use the overload of the Bind method which takes a System.Type value instead of a generic parameter.

Note also that when using non generic bindings, you can pass multiple arguments:

The same goes for the To method:

You can also do both:

This can be especially useful when you have a class that implements multiple interfaces:

Though in this particular example there is already a built-in shortcut method for this:

Convention Based Binding

Convention based binding can come in handy in any of the following scenarios:

Using «convention over configuration» can allow you to define a framework that other programmers can use to quickly and easily get things done, instead of having to explicitly add every binding within installers. This is the philosophy that is followed by frameworks like Ruby on Rails, ASP.NET MVC, etc. Of course, there are both advantages and disadvantages to this approach.

They are specified in a similar way to Non Generic bindings, except instead of giving a list of types to the Bind() and To() methods, you describe the convention using a Fluent API. For example, to bind IFoo to every class that implements it in the entire codebase:

Note that you can use the same Fluent API in the Bind() method as well, and you can also use it in both Bind() and To() at the same time.

For more examples see the examples section below. The full format is as follows:

InitialList = The initial list of types to use for our binding. This list will be filtered by the given Conditionals. It can be one of the following (fairly self explanatory) methods:

Conditional = The filter to apply to the list of types given by InitialList. Note that you can chain as many of these together as you want, and they will all be applied to the initial list in sequence. It can be one of the following:

AssemblySources = The list of assemblies to search for types when populating InitialList. It can be one of the following:

Note that you can chain together any combination of the below conditionals in the same binding. Also note that since we aren’t specifying an assembly here, Zenject will search within all loaded assemblies.

Bind IFoo to every class that implements it in the entire codebase:

Note that this will also have the same result:

Bind an interface to all classes implementing it within a given namespace

Auto-bind IController every class that has the suffix «Controller» (as is done in ASP.NET MVC):

You could also do this using MatchingRegex :

Bind all types with the prefix «Widget» and inject into Foo

Auto-bind the interfaces that are used by every type in a given namespace

This is equivalent to calling Container.BindAllInterfaces ().To () for every type in the namespace «MyGame.Things». This works because, as touched on above, Zenject will skip any bindings in which the concrete type does not actually derive from the base type. So even though we are using AllInterfaces which matches every single interface in every single loaded assembly, this is ok because it will not try and bind an interface to a type that doesn’t implement this interface.

It is also possible to remove or replace bindings that were added in another bind statement. This is especially useful when used in combination with Scene Decorators

In addition to normal identifiers, you can also assign an identifer to a given singleton.

This allows you to force Zenject to create multiple singletons instead of just one, since otherwise the singleton is uniquely identified based on the type given as generic argument to the To<> method. So for example:

In the above code, both IFoo and IBar will be bound to the same instance. Only one instance of Foo will be created.

In this case however, two instances will be created.

Another use case for this is to allow creating multiple singletons from the same prefab. For example, Given the following:

Now two instances of the prefab will be created.

Scriptable Object Installer

Another alternative to deriving from MonoInstaller or Installer when implementing your own installers, is to derive from the ScriptableObjectInstaller class. This is most commonly used to store game settings. This approach has the following advantages:

Runtime Parameters For Installers

Often when calling installers from other installers it is desirable to be able to pass parameters. You can do this by adding generic arguments to whichever installer base class you are using with the types for the runtime parameters. For example, when using a non-MonoBehaviour Installer:

Or when using a MonoInstaller prefab:

Or, by using a ScriptableObjectInstaller:

Commands And Signals

Creating Objects Dynamically Using Factories

Update / Initialization Order

In Zenject, by default, ITickables and IInitializables are updated in the order that they are added, however for cases where the update or initialization order does matter, there is a much better way: By specifying their priorities explicitly in the installer. For example, in the sample project you can find this code in the scene installer:

This way, you won’t hit a wall at the end of the project due to some unforeseen order-dependency.

Note here that the value given to BindExecutionOrder will apply to ITickable / IInitializable and IDisposable (with the order reversed for IDisposable’s).

You can also assign priorities for each specific interface separately like this:

Any ITickables, IInitializables, or IDisposable ‘s that are not assigned a priority are automatically given the priority of zero. This allows you to have classes with explicit priorities executed either before or after the unspecified classes. For example, the above code would result in ‘Foo.Initialize’ being called before ‘Bar.Initialize’.

Zenject Order Of Operations

A Zenject driven application is executed by the following steps:

Injecting data across scenes

In some cases it’s useful to pass arguments from one scene to another. The way Unity allows us to do this by default is fairly awkward. Your options are to create a persistent GameObject and call DontDestroyOnLoad() to keep it alive when changing scenes, or use global static classes to temporarily store the data.

Let’s pretend you want to specify a ‘level’ string to the next scene. You have the following class that requires the input:

You can load the scene containing LevelHandler and specify a particular level by using the following syntax:

The bindings that we add here inside the lambda will be added to the container as if they were inside an installer in the new scene.

Note that you can still run the scene directly, in which case it will default to using «default_level». This is possible because we are using the InjectOptional flag.

An alternative and arguably cleaner way to do this would be to customize the installer itself rather than the LevelHandler class. In this case we can write our LevelHandler class like this (without the [InjectOptional] flag).

Then, in the installer for our scene we can include the following:

Then, instead of injecting directly into the LevelHandler we can inject into the installer instead.

The ZenjectSceneLoader class also allows for more complex scenarios, such as loading a scene as a «child» of the current scene, which would cause the new scene to inherit all the dependencies in the current scene.

Scene Decorators can be thought of a more advanced way doing the process described above. That is, they can be used to add behaviour to another scene without actually changing the installers in that scene.

Usually, when you want to customize different behaviour for a given scene depending on some conditions, you would use boolean or enum properties on MonoInstallers, which would then be used to add different bindings depending on the values set. However, the scene decorator approach can be cleaner sometimes because it doesn’t involve changing the main scene.

For example, let’s say we want to add some special keyboard shortcuts to our main production scene for testing purposes. In order to do this using decorators, you can do the following:

If you run your scene it should now behave exactly like the scene you entered in ‘Scene Name’ except with the added functionality in your decorator installer.

NOTE: If the scene fails to load, it might be because the scene that you’re decoratoring has not been added to the list of levels in build settings.

For a full example see the asteroids project that comes with Zenject (open ‘AsteroidsDecoratorExample’ scene). NOTE: If installing from asset store version, you need to add the ‘Asteroids’ scene to your build settings so that the scene decorator can find it.

Note also that Zenject validate (using CTRL+SHIFT+V or the menu item via Edit->Zenject->Validate Current Scene) also works with decorator scenes.

Note also that you can add a decorator scene for another decorator scene, and this should work fine.

Sub-Containers And Facades

Auto-Mocking using Moq

Frequently Asked Questions

Isn’t this overkill? I mean, is using statically accessible singletons really that bad?

For small enough projects, I would agree with you that using a global singleton might be easier and less complicated. But as your project grows in size, using global singletons will make your code unwieldy. Good code is basically synonymous with loosely coupled code, and to write loosely coupled code you need to (A) actually be aware of the dependencies between classes and (B) code to interfaces (however I don’t literally mean to use interfaces everywhere, as explained here)

In terms of (A), using global singletons, it’s not obvious at all what depends on what, and over time your code will become really convoluted, as everything will tend towards depending on everything. There could always be some method somewhere deep in a call stack that does some hail mary request to some other class anywhere in your code base. In terms of (B), you can’t really code to interfaces with global singletons because you’re always referring to a concrete class

With a DI framework, in terms of (A), it’s a bit more work to declare the dependencies you need up-front in your constructor, but this can be a good thing too because it forces you to be aware of the dependencies between classes.

Then the result will be more loosely coupled code, which will make it 100x easier to refactor, maintain, test, understand, re-use, etc.

Does this work on AOT platforms such as iOS and WebGL?

Yes. However, there are a few things that you should be aware. One of the things that Unity’s IL2CPP compiler does is strip out any code that is not used. It calculates what code is used by statically analyzing the code to find usage. This is great, except that this will miss any methods/types that are not used explicitly. In particular, any classes that are created solely through Zenject will have their constructors ignored by the IL2CPP compiler. In order to address this, the [Inject] attribute that is sometimes applied to constructors also serves to automatically mark the constructor to IL2CPP to not strip out. In other words, to fix this issue all you have to do is mark every constructor that you create through Zenject with an [Inject] attribute when compiling for WebGL / iOS.

How is performance?

DI can affect start-up time when it builds the initial object graph. However it can also affect performance any time you instantiate new objects at run time.

Zenject uses C# reflection which is typically slow, but in Zenject this work is cached so any performance hits only occur once for each class type. In other words, Zenject avoids costly reflection operations by making a trade-off between performance and memory to ensure good performance.

For some benchmarks on Zenject versus other DI frameworks, see here.

Zenject should also produce zero per-frame heap allocations.

How do I use Unity style Coroutines in normal C# classes?

One solution here is to use a dedicated class and just call StartCoroutine on that instead. For example:

If you need more control than this, another option is to use a coroutine library that implements similar functionality to what Unity provides. This is what we do. See here for the library that we use for this.

How do I use Zenject with pools to minimize memory allocations?

Currently, Zenject does not support memory pooling. When you bind something to transient or use a factory, Zenject will always create a brand new instance from scratch. We realize that this can be inefficient in cases where you are creating many objects (especially on mobile) so it is something we want to address in future versions.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *