В мой джентельменский набор разработчика входят Zenject, Addressables и DOTween, значительно облегчающие разработку любого проекта на длинной дистанции.
У Zenject-а есть очевидно лишние (привет, Некоторые системы геймплейного уровня используются многими геймплейными же компонентами игровых объектов и другими системами. А значит, желательно зарегистрировать их в Все эти инсталлеры прикреплены к дочерним GameObject-ам ProjectContext-а для облегчения визуального считывания в иерархии. Префабы, необходимые каждому из инсталлеров привязываются в инспекторе. Очевидно, что все GameObject-ы и их компоненты, создаваемые инсталлерами должны складываться дочерними объектами под соответствующие порождающие инсталлеры, а не одной кучей в В инфраструктурном инсталлере из префаба создается Используется метод И Создается обычный GameObject, помещается под transform инсталлера ( Задается аккуратное имя Из него получается компонент При регистрации зависимостей, имеющих конструкторы (не MonoBeh, обычные C#-классы) Zenject постарается самостоятельно разрешить их зависимости, по правилам хорошего тона указанные как аргументы таких конструкторов. Иногда возникает необходимость передать в качестве зависимостей что-то извне контейнера, например компонент из инстанциированного объекта. Использование и ручной вызов дополнительного метода инициализации или реализация Zenject-интерфейса Zenject предоставляет дополнительный метод для регистрации Однако, конструктор В него так же передается (разумеется, по интерфейсу) Zenject возьмет явно указанные зависимости из переданных аргументов, а оставшиеся постарается разрешить из DI-контейнера. Поэтому нет необходимости дописывать вспомогательные методы для пост-конструирования / инициализации объектов или регистрировать в контейнере (тем более в ProjectContext) ситуативные одноразовые компоненты. Классы, агрегирующие несколько компонентов или управляющие некоторой композицией или системой, зачастую вынуждены иметь большое количество зависимостей. И ввиду того, что эти зависимости они потребляют не самостоятельно, а больше передают подопечным объектам, сложно говорить о разделении ответственностей и вынесении зависимостей в отдельные классы. Применение Zenject у таких (являющихся или похожих на) composition root объектов ведет к нежелательному соседству bind и resolve операций, к потенциальному смешиванию фаз контейнера, и даже циклическим зависимостям. Рассмотрим пример со стейт-машиной игры. В используемой архитектуре весь жизненный цикл игры поделен на состояния (инициализация, загрузка контента, загрузка данных, меню, загрузка уровня, подготовка уровня, геймплей …). Каждое отдельное состояние управляет необходимыми системами и сервисами на отдельном этапе игры и содержит зависимости на эти системы и сервисы. Стейт-машина В классе GSM в своем конструкторе получает все необходимые зависимости, сохраняет для собственных нужд единственный Ручной вызов конструкторов состояний продиктован логикой GSM и необходимостью заполнения словаря. Возникают следующие очевидные проблемы: Стейт-машина перегружена зависимостями. Этими зависимостями при выполнении своих обязанностей она не пользуется, а только раздает их состояниям. Огромный громоздкий конструктор. Очевиден и первый шаг к решению. Раз явно присутствует логика создания состояний, можно применить паттерн фабрики и вынести эту часть ответственностей из самой GSM. Создадим отдельный инсталлер и зарегистрируем состояния в контейнере. По условиям архитектуры каждый экземпляр – уникальный ( Реализуем Далее регистрируем фабрику по интерфейсу в Теперь стейт-машина получает ровно 2 (две) зависимости, которые напрямую и использует, а создание состояний и получение ими собственных зависимостей происходит в отдельной фабрике. Все описанные проблемы стейт-машины не перенесены и спрятаны в фабрике, а решены с помощью нее: конструктор фабрики не взрывается от зависимостей, а все необходимое разрешается самим контейнером. Упрощается расширение проекта и добавление новых состояний. Отдельно отметим, что внедрение фабрики как посредника снимает вопрос циклических зависимостей и упорядочивает процесс регистрации и разрешения зависимостей. Обобщенные методы и словари тип-экземпляр удобно использовать для кеширования и быстрого доступа к уникальным компонентам. Например, компоненты-расширения для Системы и компоненты, встроенные в движок и от сторонних разработчиков зачастую имеют специфическую реализацию и жизненный цикл. Поэтому при внедрении собственной архитектуры в пограничных местах всегда возникают проблемы интеграции: Как запустить, остановить, передать сообщение в стороннюю систему (например, FlowCanvas). Как сообщить уровню и системам, расположенным на нем, что фаза загрузки завершена и определить момент начала их работы. Как передать зависимости из контейнера, тем системам, которые предустановлены на уровне и с контейнером не связаны. Как отследить переключение (загрузку) уровней, и своевременно обновлять данные и зависимости. Возникает потребность в локальном для уровня сервис-локаторе и сервисе, отслеживающем состояние уровня и запускающем его. Zenject позволяет регистрировать зависимости в суб-контейнерах Рассмотрим здесь Далее он может обрасти ссылками на объекты и компоненты сцены, с которыми начнет работать в методе Важно отметить, что получение зависимостей происходит в отдельном методе На каждом уровне, сцене расположен свой В Регистрируется он также в Последовательность передачи сообщений в тройке Service-Watcher-Resolver следующая: Глобальный При загрузке геймплейного уровня GameplayInstaller регистрирует экземпляр (BindInstance) LevelProgressWatcher в Scene-контейнере. Вызывается метод У метод GSM переходит в На входе (метод Использование сервиса в LinkedIn: Roman Molotov Telegram: @thunderomancer
1. Организация сцены при регистрации префабов
Мотивация
DontDestroy и/или ProjectContext.
CurtainService, который помещается под соответствующей группой в иерархии:public class InfrastructureInstaller : MonoInstaller { [SerializeField] private GameObject curtainServicePrefab; public override void InstallBindings() { ... BindServices(); BindFactories(); } private void BindServices() { ... Container.BindInterfacesAndSelfTo<CurtainService>() .FromComponentInNewPrefab(curtainServicePrefab) .WithGameObjectName("Curtain") .UnderTransformGroup("Infrastructure") .AsSingle().NonLazy(); } private void BindFactories() { ... } } UnderTransformGroup (или UnderTransform), а для аккуратного переименования копии – WithGameObjectName.GameplayInstaller выглядит следующим образом:public class GameplayInstaller: MonoInstaller { ... private Camera _mainCamera; public override void InstallBindings() { BindCameraService(); BindInputService(); BindInventory(); } ... }InputService регистрируется другим образом:
this.transform)."Player Input".PlayerInput, который используется далее как аргумент при регистрации сервиса InputService: private void BindInputService() { var playerInput = Instantiate(playerInputPrefab, this.transform) .GetComponent<PlayerInput>(); playerInput.gameObject.name = "Player Input"; var inputProvider = _playerVirtualCamera .GetComponent<CinemachineInputProvider>(); Container.BindInterfacesAndSelfTo<InputService>() .AsSingle() .WithArguments(playerInput, inputProvider) .NonLazy(); }
2. Аргументы конструктора при регистрации
Мотивация
IInitializable будут размывать логику конструктора.Реализация
WithArguments, позволяющий передать дополнительные аргументы (зависимости) непосредственно в конструктор. Пример использования этого метода был приведен в предыдущем разделе, для передачи компонентов PlayerInput и CinemachineInputProvider в конструктор InputService.InputService выглядит следующим образом:public InputService( PlayerInput playerInput, CinemachineInputProvider inputProvider, ILoggingService logger) { _loggingService = logger; _playerInput = playerInput; _controls = new PlayerControls(); _inputProvider = inputProvider; }LoggingService, регистрирующийся ранее в контейнере.
3. Циклические зависимости и фабрики
Мотивация
GameStateMachine, управляющая этими состояниями отвечала и за их создание, инициализацию и передачу зависимостей. Множество состояний покрывает все этапы игры, поэтому сумма зависимостей состояний стремится к сумме всех систем и сервисов проекта и ведет к взрыву конструктора стейт-машины.GameStateMachine определен словарь вида тип-экземпляр, использующийся для доступа к текущим состояниям GSM: состояние каждого типа = уникальная фаза игры. Определен обобщенный метод Enter для входа в состояние и реализован интерфейс IInitializable от Zenject, отправляющий GSM в начальное состояние BootStrapState:public class GameStateMachine : IInitializable { private readonly ILoggingService _logger; private readonly Dictionary<Type, IExitableState> _states; private IExitableState _currentState; public GameStateMachine( SceneLoader sceneLoader, ILoggingService loggingService, IStaticDataService staticDataService, IPersistentDataService persistentDataService, ISaveLoadService saveLoadService, IEconomyService economyService, IUIFactory uiFactory, IHeroFactory heroFactory, IStageFactory stageFactory, IEnemyFactory enemyFactory ) { _logger = loggingService; _states = new Dictionary<Type, IExitableState> { [typeof(BootstrapState)] = new BootstrapState( this, staticDataService), [typeof(LoadProgressState)] = new LoadProgressState( this, persistentDataService, saveLoadService, economyService), [typeof(LoadMetaState)] = new LoadMetaState( this, uiFactory, sceneLoader), [typeof(LoadLevelState)] = new LoadLevelState( this, sceneLoader, uiFactory, heroFactory, stageFactory), [typeof(GameLoopState)] = new GameLoopState( this, heroFactory, enemyFactory) }; } public void Initialize() => Enter<BootstrapState>(); }LoggingService сервис, а остальные зависимости передает конкретным состояниям, экземплярами которых заполняется словарь _states. И только после завершения стадии конструирования и получения зависимостей происходит этап инициализации – переход в начальное состояние.
Реализация
AsSingle):public class StateMachineInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<BootstrapState>().AsSingle().NonLazy(); Container.Bind<LoadProgressState>().AsSingle().NonLazy(); Container.Bind<LoadMetaState>().AsSingle().NonLazy(); Container.Bind<LoadLevelState>().AsSingle().NonLazy(); Container.Bind<GameLoopState>().AsSingle().NonLazy(); Container .BindInterfacesAndSelfTo<GameStateMachine>() .AsSingle(); //GameStateMachine entry point is Initialize } }StateFactory. Передаем в нее DI-контейнер как зависимость – для фабрик и composition root объектов это допустимо. В методе CreateState нужное состояние получается из контейнера:public class StateFactory { private readonly DiContainer _container; public StateFactory(DiContainer container) => _container = container; public T CreateState<T>() where T : IExitableState => _container.Resolve<T>(); } }InfrastructureInstaller и упрощаем реализацию стейт машины:public class GameStateMachine : IInitializable { private readonly StateFactory _stateFactory; private readonly ILoggingService _logger; private Dictionary<Type, IExitableState> _states; private IExitableState _currentState; public GameStateMachine(StateFactory stateFactory, ILoggingService loggingService) { _stateFactory = stateFactory; _logger = loggingService; } public void Initialize() { _states = new Dictionary<Type, IExitableState> { [typeof(BootstrapState)] = _stateFactory .CreateState<BootstrapState>(), [typeof(LoadProgressState)] = _stateFactory .CreateState<LoadProgressState>(), [typeof(LoadMetaState)] = _stateFactory .CreateState<LoadMetaState>(), [typeof(LoadLevelState)] = _stateFactory .CreateState<LoadLevelState>(), [typeof(GameLoopState)] = _stateFactory .CreateState<GameLoopState>() }; Enter<BootstrapState>(); } }Обобщенные методы и рефлексия
Cinemachine:public class CameraService : ICameraService, IInitializable { private readonly Dictionary< Type, CinemachineExtension> _cinemachineExtensions = new(); public void Initialize() { foreach (var extension in _playerVirtualCamera .GetComponents<CinemachineExtension>()) _cinemachineExtensions[extension.GetType()] = extension; } [CanBeNull] public T GetCinemachineExtension<T>() where T :CinemachineExtension => (T)_cinemachineExtensions[typeof(T)]; }
4. SceneContext и зависимости уровней
Мотивация
SceneContext, ограниченных рамками сцены (уровня). Можно сказать, что для уровня он является composition root объектом, и значит требуемая иерархия сцены и взаимосвязи выглядят так:
Реализация
LevelProgressWatcher и связанный с ним сервис, позволяющие определить точку и время запуска систем уровня и являющиеся фасадом для объектов сцены. Сервис-локатор будет рассмотрен отдельно, в контексте работы с FlowCanvas.LevelProgressWatcher – фасад уровня. Его задача – после загрузки уровня
связаться с состоянием или GSM и предоставить метод для запуска уровня. Поэтому выглядит он следующим образом:public class LevelProgressWatcher : SerializedMonoBehaviour { private GameStateMachine _gameStateMachine; private ILoggingService _loggingService; [Inject] private void Construct(GameStateMachine gameStateMachine,ILoggingService loggingService) { _gameStateMachine = gameStateMachine; _loggingService = loggingService; } public void RunLevel() { _loggingService.LogMessage($"level ran", this); } } RunLevel. В нем же может появиться CompleteLevel, запускающий поток информации в обратном направлении – от уровня к GSM / GameloopState.Construct с атрибутом Inject, так как MonoBeh-и не имеют конструкторов.LevelProgressWatcher, поэтому регистрируется они на каждой сцене отдельным инсталлером, в SceneContext:public class GameplayInstaller : MonoInstaller { [SerializeField] private LevelProgressWatcher levelProgressWatcher; public override void InstallBindings() { Container.BindInstance(levelProgressWatcher); } }ProjectContext в InfrastructureInstaller регистрируется сервис:public class LevelProgressService : ILevelProgressService { public LevelProgressWatcher LevelProgressWatcher { get; set; } public void InitForLevel(LevelProgressWatcher levelController) => LevelProgressWatcher = levelController; } Service и Watcher связываются через дополнительный Resolver: public class LevelProgressServiceResolver : IInitializable, IDisposable { private readonly ILevelProgressService _levelProgressService; private readonly LevelProgressWatcher _levelProgressWatcher; public LevelProgressServiceResolver( ILevelProgressService levelProgressService, [Inject(Source = InjectSources.Local, Optional = true)] LevelProgressWatcher levelProgressWatcher) { _levelProgressService = levelProgressService; _levelProgressWatcher = levelProgressWatcher; } public void Initialize() => _levelProgressService.InitForLevel(_levelProgressWatcher); public void Dispose() => _levelProgressService.InitForLevel(null); }InfrastractureInstaller, но с дополнительной инструкцией:Container .BindInterfacesAndSelfTo<LevelProgressServiceResolver>() .AsSingle() .CopyIntoDirectSubContainers(); Container .BindInterfacesAndSelfTo<LevelProgressService>() .AsSingle() .NonLazy();Использование
InfrastructureInstaller регистрирует LevelProgressServiceResolver и LevelProgressService в Project-контейнере.LevelProgressServiceResolver при этом помечается для копирования в суб-контейнеры (SceneContext).LevelProgressServiceResolver инициализируется согласно IInitializable, но пока значением null. Происходит это 2 раза, для сцен Bootstrap и Meta, которые не являются геймплейными уровнями (не имеют Watcher).Construct у LevelProgressWatcher, передающий ему необходимые зависимости.LevelProgressServiceResolver вызывается сначала конструктор, получающий зависимости на Service и Watcher, а затем – Initialize и инициализация Service в методе InitForLevel экземпляром LevelProgressWatcher.GameloopState.Enter) в состояние у локального Watcher, доступного через глобальный сервис вызывается метод RunLevel.GameloopState:public class GameLoopState : IState { private readonly GameStateMachine _stateMachine; private readonly IEnemyFactory _enemyFactory; private readonly IHeroFactory _heroFactory; private readonly ILevelProgressService _levelProgressService; public GameLoopState( GameStateMachine gameStateMachine, IHeroFactory heroFactory, IEnemyFactory enemyFactory, ILevelProgressService levelProgressService) { _stateMachine = gameStateMachine; _heroFactory = heroFactory; _enemyFactory = enemyFactory; _levelProgressService = levelProgressService; _levelProgressService = levelProgressService; } public void Enter() { _levelProgressService.LevelProgressWatcher.RunLevel(); } public void Exit() { _enemyFactory.CleanUp(); _heroFactory.CleanUp(); } }
Референсы и контакты
ссылка на оригинал статьи https://habr.com/ru/articles/781016/
Добавить комментарий