

Автор статьи: Игорь Гулькин
Senior Unity Developer
Всем привет! ????
Меня зовут Игорь Гулькин, и я Unity разработчик. За свои 5 лет накопилось много опыта, поэтому в этой статье хотел бы поделиться принципами и подходами, с помощью которых можно реализовать архитектуру игры просто и гибко без фреймворка. Цель доклада, дать не просто готовое решение, а показать ход мыслей и паттерны, с помощью которых ее можно выстроить. Если вы не читали первую и вторую части, то рекомендую начать с них :).
Давайте посмотрим, как у нас выглядит архитектура в конце второй части:

Выглядит не очень понятно, давайте рефакторить с помощью шаблонов GRASP с целью упрощения понимания…
Рефакторинг архитектуры
Первым делом мы можем использовать шаблон Pure Fabrication и объединить логику классов GameMachine
и GameLocator
в некий Фасад, который назовем GameContext
:
public sealed class GameContext : MonoBehaviour, IGameLocator, IGameMachine
{
public GameState GameState
{
get { return this.gameMachine.GameState; }
}
private readonly GameMachine gameMachine = new();
private readonly GameLocator serviceLocator = new();
public GameContext()
{
this.serviceLocator.AddService(this.gameMachine);
}
[ContextMenu("Start Game")]
public void StartGame()
{
this.gameMachine.StartGame();
}
[ContextMenu("Pause Game")]
public void PauseGame()
{
this.gameMachine.PauseGame();
}
[ContextMenu("Resume Game")]
public void ResumeGame()
{
this.gameMachine.ResumeGame();
}
[ContextMenu("Finish Game")]
public void FinishGame()
{
this.gameMachine.FinishGame();
}
public void AddListener(object listener)
{
this.gameMachine.AddListener(listener);
}
public void RemoveListener(object listener)
{
this.gameMachine.RemoveListener(listener);
}
public void AddService(object service)
{
this.serviceLocator.AddService(service);
}
public void RemoveService(object service)
{
this.serviceLocator.RemoveService(service);
}
public T GetService<T>()
{
return this.serviceLocator.GetService<T>();
}
}
Применив шаблон GRASP, получаем один монобех, который содержит всю логику для работы с сервисами и состоянием игры. При этом GameContext
реализует интерфейсы IGameMachine
и IGameLocator
, но бизнес-логику делегирует теперь уже обычным классам GameMachine
и GameLocator
.
Добавим GameContext
на сцену:

Теперь давайте глянем на инсталлеры в схеме:

С инсталлерами все немного интереснее, тут уже придется применить несколько шаблонов GRASP, но самый ключевой из них будет Indirection. В качестве посредника между GameContext
и компонентами системы будет некий GameContextInstaller
.

Теперь GameContextInstaller
будет регистрировать все сервисы и листенеры и заниматься внедрением зависимостей. А вместо классов GameObservableInstaller
, GameServiceInstaller
и GameAssembler
будут интерфейсы IGameServiceProvider
, IGameListenerProvider
, IGameConstructor
. Вот как это будет выглядеть в коде:
public sealed class GameContextInstaller : MonoBehaviour
{
[SerializeField]
private GameContext gameContext;
[SerializeField]
private MonoBehaviour[] installers;
private void Awake()
{
foreach (var installer in this.installers)
{
if (installer is IGameServiceProvider serviceProvider)
{
this.gameContext.AddServices(serviceProvider.GetServices());
}
if (installer is IGameListenerProvider listenerProvider)
{
this.gameContext.AddListeners(listenerProvider.GetListeners());
}
}
}
private void Start()
{
foreach (var installer in this.installers)
{
if (installer is IGameConstructor constructor)
{
constructor.ConstructGame(this.gameContext);
}
}
}
}
В методе GameContextInstaller.Awake()
происходит регистрация сервисов и слушателей в GameContext
, а в методе GameContextInstaller.Start()
происходит внедрение зависимости.
Теперь пару слов про новые интерфейсы:
IGameListenerProvider предоставляет слушателей для регистрации в GameContext
.
public interface IGameListenerProvider
{
IEnumerable<object> GetListeners();
}
IGameServiceProvider предоставляет сервисы для регистрации в GameContext
.
public interface IGameServiceProvider
{
IEnumerable<object> GetServices();
}
IGameConstructor занимается разрешением зависимостей, получая центральный реестр в качестве аргумента.
public interface IGameConstructor
{
void ConstructGame(IGameLocator serviceLocator);
}
Теперь нам нужно написать классы, которые будут реализовывать эти контракты.
В нашем проекте, фактически есть два модуля: система игрока и система пользовательского ввода. Поэтому для каждого модуля и напишем свою реализацию
В классе
PlayerInstaller
будем описывать систему игрока.В классе
InputInstaller
будем описывать систему ввода.
Прилагаю код:
public sealed class PlayerInstaller : MonoBehaviour,
IGameServiceProvider,
IGameListenerProvider,
IGameConstructor
{
[SerializeField]
private Player player;
[SerializeField]
private MoveController moveController;
//TODO: Подключить контроллер камеры:
//[SerializeField]
//private CameraController cameraController;
IEnumerable<object> IGameServiceProvider.GetServices()
{
yield return this.player;
}
IEnumerable<object> IGameListenerProvider.GetListeners()
{
yield return this.moveController;
//yield return this.cameraController;
}
void IGameConstructor.ConstructGame(IGameLocator serviceLocator)
{
var keyboardInput = serviceLocator.GetService<IMoveInput>();
this.moveController.Construct(keyboardInput, this.player);
//var camera = serviceLocator.GetService<WorldCamera>();
// this.cameraController.Construct(camera, this.player)
}
}
public sealed class InputInstaller : MonoBehaviour,
IGameServiceProvider,
IGameListenerProvider
{
[SerializeField]
private KeyboardInput keyboardInput;
//TODO: подключить ввод с мыши
//[SerializeField]
//private MouseInput mouseInput;
IEnumerable<object> IGameServiceProvider.GetServices()
{
yield return this.keyboardInput;
//yield return this.mouseInput;
}
IEnumerable<object> IGameListenerProvider.GetListeners()
{
yield return this.keyboardInput;
//yield return this.mouseInput;
}
}
Отлично, код написан! Добавляем GameContextInstaller
, PlayerInstaller
и InputInstaller
на сцену:

Запускаем игру. Сейчас регистрация компонентов системы происходит в методе GameContextInstaller.Awake()
, а внедрение зависимости — в методе GameContextInstaller.Start()
. Дальше в “монобехе” GameContext через контекстное меню вызываем метод GameContext.StartGame()
. Вуаля, все работает!
В результате наша система выглядит так:

Оптимизация игры
Теперь было было здорово, чтобы наша игровая логика не зависела от монобехов. Во-первых, такие классы проще переиспользовать и тестировать, так как можно создать экземпляр в любом месте кода. Во-вторых, уйдя от монобехов и гейм-объектов, можно оптимизировать память и производительность. В третьих, появляется возможность работать в многопоточном коде, поскольку GameObject’ы и “монобехи” можно использовать только в главном потоке
Таким образом, MoveController и KeyboardInput можно сделать обычными классами и создавать экземпляры этих классов прямо в инсталлере:
public sealed class InputInstaller : MonoBehaviour,
IGameServiceProvider,
IGameListenerProvider
{
private readonly KeyboardInput keyboardInput = new();
//Other code…
}
public sealed class PlayerInstaller : MonoBehaviour,
IGameServiceProvider,
IGameListenerProvider,
IGameConstructor
{
private readonly MoveController moveController = new();
//private readonly CameraController cameraController = new();
//Other code…
}
Но у KeyboardInput есть метод Update()
, который вызывается из движка, чтобы трекать пользовательский ввод. Этот нюанс тоже можно решить, если сделать специальный интерфейс IUpdateGameListener
, который будет вызывать GameContext
:
public interface IUpdateGameListener
{
void OnUpdate(float deltaTime);
}
Таким образом, KeyboardInput
будет выглядеть так:
public sealed class KeyboardInput :
IMoveInput,
IStartGameListener,
IUpdateGameListener,
IFinishGameListener
{
public event Action<Vector3> OnMove;
private bool isActive;
void IUpdateGameListener.OnUpdate(float deltaTime)
{
if (this.isActive)
{
this.HandleKeyboard();
}
}
//Other code…
}
А в класс GameContext
добавляем следующий код:
public sealed class GameContext : MonoBehaviour, IGameLocator, IGameMachine
{
private readonly List<IUpdateGameListener> updateListeners = new();
private void Awake()
{
this.enabled = false;
}
//Вызывается только, если игра запущена
private void Update()
{
var deltaTime = Time.deltaTime;
for (int i = 0, count = this.updateListeners.Count; i < count; i++)
{
var listener = this.updateListeners[i];
listener.OnUpdate(deltaTime);
}
}
//Other code…
}
Ремарка: при необходимости можно также прикрутить методы
FixedUpdate()
иLateUpdate()
, реализовав соответствующие интерфейсы:IFixedUpdateListener
иILateUpdateListener
Еще одним преимуществом написания кода без монобехов будет разделение ответственности на уровне проекта. Поскольку код игры пишется на чистом C#, то и все внесение изменений тоже будет решаться на уровне кода… Тем самым это уменьшает необходимость трогать игровые объекты при переписывании кода, и позволяет другим специалистам, таким как левел-дизайнерам работать на сцене параллельно с большей уверенностью, что они ничего не сломают :).
В результате оптимизации видим, что теперь монобехи больше не нужны, так как с игровой логикой теперь можно работать на уровне кода:

Проверяем работоспособность игры… Все работает!
Единая точка входа
Несложно обнаружить, что на игровой сцене нет единой точки входа, и запуск игры происходит как попало:
Компоненты регистрируются в методе
GameContextInstaller.Awake()
.Внедрение зависимостей происходит в методе
GameContextInstaller.Start()
.А игра вообще запускается вручную через инспектор.
Пока что это все выглядит немного ridiculous. Так что давайте сделаем единую точку входа в игру.
Для того, чтобы сделать процесс запуска игры последовательным, нужно поместить вызов всех действий в одном месте.

Например, давайте выделим ответственность новому классу GameLauncher
, который будет заниматься запуском игры в одном месте.
public sealed class GameLauncher : MonoBehaviour
{
[SerializeField]
private GameContextInstaller installer;
[SerializeField]
private GameContext context;
[ContextMenu("Launch Game")]
public void LaunchGame()
{
this.installer.RegisterComponents();
this.installer.ConstructGame();
this.context.StartGame();
}
}
Если посмотреть на класс GameLauncher
, можно заметить, что методы класса GameContextInstaller.RegisterComponents()
и GameContextInstaller.ConstructGame()
стали публичными. Теперь они вызываются в методе GameLauncher.LaunchGame()
, а не через методы Awake()
и Start()
, как было ранее. Это сделано, для того, чтобы у разработчика всегда был контроль над управлением системы, и это хорошо :)
Опытные читатели, взглянув на класс GameLauncher
, быстро заметят, что этот класс нарушает 2-й принцип SOLID Open-Closed, так при добавлении или удалении команды в процесс загрузки, нам придется изменять этот класс. Поэтому давайте отрефакторим GameLauncher, используя полиморфизм, чтобы порядок инициализации был более гибким:
public abstract class GameTask : ScriptableObject
{
public abstract Task Do();
}
public sealed class GameLauncher : MonoBehaviour
{
[SerializeField]
private bool autoRun = true;
[SerializeField]
private List<GameTask> taskList;
private async void Start()
{
if (this.autoRun)
{
await this.LaunchGame();
}
}
[ContextMenu("Launch Game")]
public async Task LaunchGame()
{
foreach (var task in this.taskList)
{
await task.Do();
}
}
}
Теперь наш пайплайн запуска состоит из асинхронных задач и может запускаться автоматически с помощью галочки autoRun. Асинхронная загрузка позволяет подгружать ресурсы и прогресс игрока в других потоках, что может потребоваться на боевом проекте.
Каждую задачу можно сделать ScriptableObject’ом, чтобы потом легко ее можно было добавить в список загрузки. Давайте посмотрим, как это выглядит в коде:
Задача регистрации компонентов:
[CreateAssetMenu(
fileName = "Task «Register Components»",
menuName = "GameTasks/Task «Register Components»"
)]
public sealed class GameTask_RegisterComponents : GameTask
{
public override Task Do()
{
var installer = FindObjectOfType<GameContextInstaller>();
installer.RegisterComponents();
return Task.CompletedTask;
}
}
Задача разрешения зависимостей:
[CreateAssetMenu(
fileName = "Task «Construct Game»",
menuName = "GameTasks/Task «Construct Game»"
)]
public sealed class GameTask_ConstructGame : GameTask
{
public override Task Do()
{
var installer = FindObjectOfType<GameContextInstaller>();
installer.ConstructGame();
return Task.CompletedTask;
}
}
Задача запуска игры:
[CreateAssetMenu(
fileName = "Task «Start Game»",
menuName = "GameTasks/Task «Start Game»"
)]
public sealed class GameTask_StartGame : GameTask
{
public override Task Do()
{
var gameContext = FindObjectOfType<GameContext>();
gameContext.StartGame();
return Task.CompletedTask;
}
}
Ремарка: В данном примере мы видим использование методов
GameObject.FindObjectOfType
, которые помогают найти ключевые компоненты на сцене. Несмотря на то, что производительность методаGameObject.FindObjectOfType
напрямую зависит от кол-ва объектов на сцене, на этапе запуска это будет не критично, так как процесс загрузки не относиться к самому геймплею игры.
Ремарка: Если потребуется задача оптимизации, то можно сделать поиск объектов через метод
GameObject.FindWithTag(“Game”)
. Поскольку Unity делает кеширование объектов по тегу в массивы, то такой подход будет гораздо быстрее. Ну или в самом-самом крайнем случае сделайте синглтонGameContext.Instance
, к которому можно обращаться только на этапе загрузки…
Ну что ж, добавляем GameLauncher
на сцену, определяем список задач:

Запускаем игру… Все работает :)
Теперь можно убрать класс GameContextInstaller
и перенести его логику в задачи, чтобы упростить систему:
[CreateAssetMenu(
fileName = "Task «Construct Game»",
menuName = "GameTasks/Task «Construct Game»"
)]
public sealed class GameTask_ConstructGame : GameTask
{
public override Task Do()
{
var gameContext = GameObject
.FindGameObjectWithTag(TagManager.GAME_CONTEXT)
.GetComponent<GameContext>();
var installers = GameObject
.FindGameObjectsWithTag(TagManager.GAME_INSTALLER);
foreach (var installer in installers)
{
if (installer.TryGetComponent(out IGameServiceProvider serviceProvider))
{
gameContext.AddServices(serviceProvider.GetServices());
}
if (installer.TryGetComponent(out IGameListenerProvider listenerProvider))
{
gameContext.AddListeners(listenerProvider.GetListeners());
}
}
foreach (var installer in installers)
{
if (installer.TryGetComponent(out IGameConstructor constructor))
{
constructor.ConstructGame(gameContext);
}
}
return Task.CompletedTask;
}
}
[CreateAssetMenu(
fileName = "Task «Start Game»",
menuName = "GameTasks/Task «Start Game»"
)]
public sealed class GameTask_StartGame : GameTask
{
public override Task Do()
{
GameObject
.FindGameObjectWithTag(TagManager.GAME_CONTEXT)
.GetComponent<GameContext>()
.StartGame();
return Task.CompletedTask;
}
}
Финальная схема выглядит так:

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

А если нужно сделать несколько этапов пост инициализации: например, загрузить прогресс игрока — то это можно сделать после внедрения зависимостей.

В общем теперь вы сами, как разработчик, решаете, когда и как в вашей игре должен происходить запуск. Самое главное — вы всегда знаете, что у вас есть GameLauncher, который является единой точкой входа в игру, а GameContext управляет ее процессом и хранит в себе сервисы.
Выводы
Таким образом, мы реализовали простую и оптимизированную архитектуру игры на Unity без Zenject на примере перемещения кубика.

Теперь в нашей архитектуре:
Есть
GameLauncher
, который является точкой входа в игру и делает запуск по списку задач.Есть
GameContext
, который управляет состоянием игры и вызывает Update у элементов системы.Есть механизм разрешения зависимостей, который реализован в коде через инсталлеры и сервис-локатор.
Вся игровая логика реализована на классах C#.
Самое главное — у разработчика есть полный контроль над системой, и он может затачивать такую архитектуру под свой проект, тем самым влиять на оптимизацию в отличие от использования Zenject.
Ремарка: в будущем, если вы хотите описывать ваши инсталлеры более декларативно, то вам потребуется рефлексия и атрибуты для того, чтобы собирать слушателей и сервисы, а также же делать внедрение зависимостей. Например, у меня в проектах это выглядит примерно так:
public sealed class PlayerInstaller : GameInstaller
{
[GameService]
[SerializeField]
private Player player;
[GameListener]
private readonly MoveController moveController = new();
[GameListener]
private readonly CameraController cameraController = new()
}
public sealed class InputInstaller : GameInstaller
{
[GameService, GameListener]
private readonly KeyboardInput keyboardInput = new();
[GameService, GameListener]
private readonly MouseInput mouseInput = new();
}
Такую архитектуру я использую в своих проектах:
Поэтому нет предела совершенству, самое главное, выбирайте архитектуру игры с умом, ведь у каждого проекта есть свои требования, дедлайн и команда, как бы это банально не звучало .
Прилагаю ссылку на код к статье.
Также хочу порекомендовать вам еще один полезный урок, где изучим паттерн Model-View-Presenter на примере попапа (pop-up) магазина. Подробнее про урок можете узнать по этой ссылке.
На этом у меня все, всем спасибо за внимание :)
DizzyJump
Мне кажется стоило назвать цикл статей в духе "Отказываемся от zenject и пишем свой велосипед", оно бы лучше отражало суть написаного. Но я так и не увидел мотивационную часть, а зачем? Какие преимущества важные для данного проекта мы получили отказавшись от библиотеки, которая является в некотором смысле индустриальным стандартом di-фреймворка в юнити и написав свой велосипед? Во имя чего всё это?
StarKRE
Привет! Спасибо за фидбек)
На мой взгляд, архитектура не может быть велосипедом, потому что она делается под конкретный проект в определенных условиях (сроки, команда, бюджет, технологии и т.д.)
Одна из ключевых идей, которую я хотел донести в статье это то, что разработчики сами могут построить свою простую архитектуру и заточить ее под без использования фреймворков Zenject или VContainer
Почему мне фреймворки не нравятся:
Во первых, они построены на инверсии управления и забирают у разработчика возможность управлять потоком исполнения. И если тебе нужно перестроить ход инициализации приложения, то это может очень быть проблематично.
Во-вторых, если говорить конкретно про Zenject, то для меня это выглядит сложно. Нужно потратить немало времени на изучение фреймворка прежде чем приступать к реализации. Зачем это все: 1000 способов биндингов, различные контексты, встроенные фабрики, пулы, интерфейсы IInitializable, ITickable, если это все мне может не понадобиться в общем случае?
Поэтому для меня гораздо проще будет сделать свое решение по принципу KISS (Keep It Simple Stupid), которое будем понятным и простым для разработчиков. И заложить в архитектуру только те опции, которые будут нужны в проекте. И если нужно управлять ими или оптимизировать.
Уже говорил неоднократно, что "серебряных пуль не бывает"!
Надеюсь, ответил на вопрос. Если в команде нету опыта проектирования архитектуры, то берите Zenject или VContainer :)
HexGrimm
Я бы пожалуй возразил про:
Так как управлять потоком выполнения на самом деле мешает Юнити и неопределённость вызова методов у MonoBehaviour. Как раз это и корень проблемы. А например конструкторы - не важно кем выполненные, не являются такой проблемой. И не важно, выполняет их контейнер активатором или сам рантайм.
И действительно, в Zendject написано 90% бесполезного мусора, который только мешает понимать суть проблемы, тк этот сахар живёт по правилам убогого фреймворка. Но сама суть Di от этого хуже не становится, просто нужно использовать инъекцию только в конструктор и абстрактные фабрики. Для всех случаев в разработке игр этого будет достаточно. Единственный ООП кейс в котором будет неудобно, это параметризированный конструктор, где сигнатура зависит от типа. Такое скорее всего можно обойти абстрактным методом Init(x, y) у общего типа, и скрыть внутри своей фабрики, или наоборот, сделать пост-иньекцию в метод, а сами конструкторы сделать одинаковыми.
StarKRE
По поводу конструкторов и методов пост-инъекции вопросов нет. Тут речь идет про то, что должна быть единая точка входа в программу, и разработчик решает в какой момент и куда и как делать инъекцию, а не так, что через Script Execution Order фреймворк сам побежал "шурудить" все игровые объекты по сцене через рефлексию
HexGrimm
Всё так, но почему в примерах статьи так много наследников от MonoBehaviour когда это обычные классы которые нуждаются в конструкторе?
Не нужно иметь под рукой сервис локатор или метод GetService, если в конструктор всё приходит, и выстроено в правильном порядке. А то что накликано в юнити мышкой, ну фиг знает, скорее всего DI нарушен многократно.
StarKRE
Хм, к концу статьи у нас получилось всего 4 монобеха в системе)
Вот ссылка на код-базу
----------
Монобехами являются GameContext, GameLauncher, PlayerInstaller & InputInstaller, а вся игровая логика сделана на обычных классах
GameContext — монобех, чтобы можно было вручную делать старт/паузу игры и видеть стейт
GameLauncher — монобех, чтобы запускать игру с помощью списка задач, которые можно выставлять в инспекторе
А инсталлеры — PlayerInstaller & InputInstaller чтобы можно было через инспектор видеть состояния классов и дебажить их (Но можно их сделать и ScriptableObject'ами)
----------
По поводу сервис-локатора согласен, что класс, который выполняет бизнес-логику не должен иметь зависимость на ServiceLocator, а должен получать зависимости через конструктор или в метод пост-инъекции. И если у тебя есть инсталлеры, которые хранят классы с бизнес-логикой, то ты можешь поручить им ответственность за создание экземпляров этих классов и внедрения зависимостей.
Просто внедрение зависимости не ограничивается получением сервисов из ServiceLocator'а. Про накликано в юнити мышкой пока что не оч понял, скорее всего речь про инспектор)
redHurt69
Все еще довольно сомнительно.
Не понимаю кто в здравом уме между вариантами "костылить свой фреймворк ради IoC" и "посмотреть 5-минутное видео про Zenject" выберет второе.
Если опять же у нас архитектура делается под "сроки, команду, бюджет и технологии", то в каком варианте этих составляющих, предпочтение сделается в пользу создания своего сервис локатора, который пытается быть похожим на di вместо использования "индустриального стандарта di-фреймворка"?)
Я бы понял еще если бы шел разговор о том, что "вот Zenject не подходит, у него нет такого-то списка фичей, которые нам тут нужны", но тут мы имеем всего лишь пример с кубиком и по сути вся речь о том "как сделать тоже самое, только хуже и свое".
StarKRE
Готов поспорить, что мое решение отработает быстрее чем Zenject
redHurt69
А никто не говорит про скорость работы. Да и козырять ей на примере с кубиком очень уж сомнительно) Уверен, что после 5 лет разработки можно понять, что нет смысла хвастаться из-за сэкономленной миллисекунды, особенно если потратил гораздо больше времени, костыля свой фреймворк) Хотя я бы с радостью посмотрел на эти тесты
Ну и для кучи можно еще VContainer протестировать, раз уж мы вдруг заговорили про скорость)
StarKRE
Когда я писал первую часть статьи, я обозначил проблемы, которые нам нужно решить. Напротив каждой задачи буду ставить плюсик, есть ли эта фича в Zenject:
Механизм внедрения зависимостей (+)
Оптимизация архитектуры и уход от монобехов (+)
Единая точка входа в приложение (-)
Порядок инициализации игры (-)
Работа с состояниями игры (старт/пауза/завершение) (-)
Оповещение компонентов системы об изменение состояния игры (+-)
Суть в том, что в основном Zenject решает проблему внедрения зависимости, но под оставшиеся задачи нужно все-равно писать кастомный код... И поэтому, я считаю, что если есть возможность выстроить целостную инфраструкуру под решение всех задач, то в долгосрочной перспективе это будет лучше, чем потом думать, а как это можно состыковать с выбранным фреймворком
HexGrimm
Мне кажется что 3 и 4 пункты как раз в Zenject есть. Это момент когда у рутового объекта будет вызван единственный Resolve. Этот объект и может иметь единственную точку входа и единственный Update() метод во всём проекте, и дальше работать уже со своим деревом просто как c C# классами.
А вот как раз 5 и 6 и не должны быть в функционале контейнера, и если в сахаре Zenject это есть, то использовать то не стоит.
StarKRE
И да и нет :)
Метод Resolve не может быть единственным, поскольку у каждого контекста инициализация и разрешение зависимости в своём Di контейнере происходит в разные моменты выполнения программы. Например, у ProjectContext'а в момент его создания, у GameObjectContext'а — в методе Construct, у SceneContext'а — в Awake() (причем первый в ScriptExecutionOrder'е)
На мой взгляд, это больше относиться к автоматическому механизму разрешения зависимостей с большим набором опций, но к процессу инициализации игры это никак не относиться, так как процесс инициализации может в себя включать соединение с сервером и загрузку пользовательских данных, и вообще может быть асинхронным
---
По поводу Update(), он тоже не единственный, так как этот метод вызывается у каждого MonoKernel, который крепиться дополнительно к каждому гейм-обджекту, на котором висит компонент ProjectContext / SceneContext / GameObjectContext
---
5 и 6 пункт, согласен :)
DizzyJump
Управления жизненным циклом приложения и создание точки инициализации можно через бутстрап сцену сделать. Зенжект тут ничем не мешает совершенно.
StarKRE
Да, так обычно и делается :)
DizzyJump
По поводу того, что в зенжекте много функционала я согласен, но не согласен с тем, что это плохо. Большая часть перечисленного не обязательна к использованию, но если оно понадобилось то уже под рукой готовое и протестированное на сотнях проектов.
Ну и я вцелом не согласен что лучше писать свой велосипед на любой чих потому что он комуто-там (прежде всего автору) понятен. Прелесть использования широко распространенных библиотек и фреймворков как раз в том что новый разработчик если знаком с тем же зенжектом может сразу плюс минус идти в бой, даже на относительно крупном проекте. Но нет никаких шансов нанять инженера который раньше уже катался на всех ваших велосипедах, которые вы напишете для реализации давно решенных задач.
StarKRE
Да, тут я согласен, что популярность фреймворков упрощает интеграцию новых разработчиков в проект, и это, действительно, оправданный выбор использовать Zenject с точки зрения бизнеса.
С другой стороны надо понимать, что Zenject — это универсальное решение и под капотом оно делает много телодвижений, которые в вашем проекте могут просто не понадобиться.
Таким образом, вы можете сделать все то же самое, и это будет работать быстрее, проще и компактнее, потому что вы взяли только то, что вам нужно для вашего проекта и отсекли все лишнее :)
А вопрос велосипедов — это вопрос опыта разработки архитектуры
И если в команде нет такого опыта, то лучше делегировать ответственность фреймворку и не париться ????