Подробные изменения: - Добавлены сцены Bootstrap, MainMenu и Game с текущей вертикальной 9:16 структурой проекта. - Добавлены runtime-скрипты gameplay-архитектуры: DI через Extenject, спавнер платформ, прогрессия этапов, one-way платформы, игрок, камера и оборудование JumpPad. - Добавлены ScriptableObject-этапы сложности, weighted-настройки платформ, оборудования, противников и collectibles. - Добавлены UI Toolkit ресурсы главного меню, preview-материалы, шрифты, спрайты, анимации и игровые prefab-ассеты. - Перенесена и оформлена новая COM-система для работы с аппаратными портами и StreamingAssets. - Обновлены проектные гайды, AGENTS.md, правила Git/Git LFS, .gitignore, Unity Packages и ProjectSettings. - Удалён локальный skill start-required-mcp-servers из репозитория.
Dependency Injection через Extenject
Зачем это нужно
Dependency Injection через Extenject нужен, чтобы:
- разделять игровую логику и Unity-обвязку
- уменьшать количество жёстких ссылок между компонентами
- легче тестировать код
- проще собирать разные режимы проекта: игра, сервисный режим, призовой контур, аппаратный слой
Для Danro Jump это особенно полезно, потому что проект со временем будет сочетать:
- аркадный геймплей
- работу с
COM-портами - сервисные экраны для персонала
- призовую витрину
- сценарии выдачи через
QR-коды
Без DI такие подсистемы быстро начинают зависеть друг от друга напрямую и усложняют сопровождение.
Когда Extenject действительно полезен
Extenject особенно хорошо подходит, если в проекте есть:
- несколько отдельных подсистем
- сервисы с долгим жизненным циклом
- необходимость подменять реализации
- разные режимы запуска
- желание держать доменную логику вне
MonoBehaviour
Для Danro Jump типичные кандидаты на DI:
IGameSessionServiceIScoreServiceIPrizeServiceIPrizeShowcaseGatewayIQrRewardServiceIComPortGatewayIServiceModeAccessPolicyIGameBalanceProvider
Как использовать legacy-код из Assets/Temp/Legacy
В Assets/Temp/Legacy сохранён старый код, который может пригодиться для будущей реализации. Его не нужно подключать к runtime целиком: он должен быть источником идей, API-контрактов, протоколов и моделей.
Самые полезные legacy-кандидаты для DI:
Assets/Temp/Legacy/Scripts/Controls/Inputs/ComSystem— основа для аппаратного слоя,COM, handshake, ролей устройств и призовой витриныAssets/Temp/Legacy/Scripts/Managers/PaymentManager.cs— reference для кредитов, оплаты и free play режимаAssets/Temp/Legacy/Scripts/Managers/PrizeSystemManager.cs— reference для orchestration призовAssets/Temp/Legacy/Scripts/Prizes— модели и исполнители призовAssets/Temp/Legacy/Scripts/Settings— база для сервисных настроек персоналаAssets/Temp/Legacy/Scripts/Utilities/QRCodeUtils.cs— генерацияQRи моделиQRCodeInfo
Правило переноса:
- сначала выделить интерфейс новой подсистемы
- затем перенести минимальную доменную модель
- после этого завернуть legacy-алгоритм в новую реализацию сервиса
- только в конце подключить реализацию через installer
Пример: вместо прямого переноса ComSystemManager лучше выделить контракт:
public interface IComDeviceService
{
IReadOnlyList<string> ConnectedDeviceIds { get; }
UniTask InitializeAsync();
UniTask SendAsync(string deviceId, string command);
}
А старую связку ComHub, ComDeviceClient, ComNativeWrapper использовать как reference для реализации ComDeviceService.
Legacy как Anti-Corruption Layer
Если старый код нужно подключить временно, его лучше обернуть anti-corruption layer:
public sealed class LegacyPrizeShowcaseGateway : IPrizeShowcaseGateway, IDisposable
{
private readonly LegacyPrizeStandAdapter _adapter;
public LegacyPrizeShowcaseGateway(LegacyPrizeStandAdapter adapter)
{
_adapter = adapter;
}
public UniTask OpenAsync(int prizeSlotId)
{
return _adapter.TryOpenSlotAsync(prizeSlotId);
}
public void Dispose()
{
_adapter.Dispose();
}
}
Так новый код зависит от IPrizeShowcaseGateway, а не от старых RR.Common.* классов.
Базовая модель Extenject
В Extenject важно понимать три уровня:
ProjectContext— глобальные зависимости на весь runtimeSceneContext— зависимости внутри конкретной сценыGameObjectContext— локальный контейнер для prefab или отдельного объекта
Практическое правило:
- в
ProjectContextскладывай только по-настоящему глобальные вещи - в
SceneContextскладывай зависимости конкретного режима или сцены GameObjectContextиспользуй только там, где действительно нужен локальный composition root
Рекомендуемая стратегия для Danro Jump
ProjectContext
В ProjectContext обычно стоит держать:
- конфигурацию приложения
- логирование
- фабрики системного уровня
- глобальные настройки локализации
- сервисы, которые переживают смену сцен
Примеры:
Container.BindInterfacesAndSelfTo<AppLifetime>().AsSingle();
Container.BindInterfacesAndSelfTo<GameSettingsProvider>().AsSingle();
Container.Bind<IComPortGateway>().To<ComPortGateway>().AsSingle();
Container.Bind<IQrRewardService>().To<QrRewardService>().AsSingle();
Container.Bind<IPrizeService>().To<PrizeService>().AsSingle();
Container.Bind<IPaymentService>().To<PaymentService>().AsSingle();
SceneContext
В SceneContext конкретной сцены лучше держать:
- локальный gameplay flow
- сценовые контроллеры
- scene-specific presenters
- экранные сервисы
Пример для игровой сцены:
public sealed class GameplayInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.BindInterfacesAndSelfTo<GameSessionService>().AsSingle();
Container.BindInterfacesAndSelfTo<PlatformSpawner>().AsSingle();
Container.BindInterfacesAndSelfTo<PlayerDeathHandler>().AsSingle();
Container.BindInterfacesAndSelfTo<GameHudPresenter>().AsSingle();
}
}
Отдельный сервисный режим
Сервисный режим для персонала лучше оформлять отдельной сценой или отдельным сценовым контекстом, а не примешивать к gameplay-сцене напрямую.
Тогда там можно поднимать:
- настройки стоимости игры
- настройки сложности
- настройки призов
- диагностику
COM-оборудования - ручные тесты витрины и выдачи
Composition Root
Главное правило Extenject:
- контейнер знает, как собрать объект
- сам объект не должен знать, где искать свои зависимости
Это значит:
- не использовать
FindObjectOfType - не использовать singleton-поиск как основной способ сборки
- не прокидывать контейнер в доменную логику как сервис-локатор
Хорошо:
public sealed class PrizeService : IPrizeService
{
private readonly IPrizeRepository _repository;
private readonly IPrizeShowcaseGateway _showcaseGateway;
public PrizeService(
IPrizeRepository repository,
IPrizeShowcaseGateway showcaseGateway)
{
_repository = repository;
_showcaseGateway = showcaseGateway;
}
}
Плохо:
public sealed class PrizeService
{
public PrizeService()
{
var gateway = ProjectContext.Instance.Container.Resolve<IPrizeShowcaseGateway>();
}
}
Что именно инжектить
Обычно через Extenject лучше инжектить:
- интерфейсы
- чистые C# сервисы
- конфигурационные объекты
- фабрики
- адаптеры к внешним системам
С осторожностью:
MonoBehaviourScriptableObject- runtime-created prefab-объекты
Если зависимость является Unity-компонентом сцены, лучше явно понимать её жизненный цикл.
Типичный набор ролей в проекте
Для Danro Jump удобно делить классы так:
Domain / Core
- правила игры
- подсчёт наград
- проверка eligibility для выдачи призов
- политика сложности
Application / Services
- orchestration игровых сценариев
- координация сервисного режима
- работа с
QR - orchestration аппаратных операций
Infrastructure
COM-порт- Unity scene loading
- persistence
- конкретная работа с UI и устройствами
Presentation
- HUD presenters
- menu presenters
- service panel presenters
Extenject особенно хорошо работает, когда связывает эти уровни без прямой жёсткой сцепки.
Рекомендуемые binding-паттерны
BindInterfacesAndSelfTo
Удобен для сервисов с несколькими ролями:
Container.BindInterfacesAndSelfTo<GameSessionService>().AsSingle();
FromInstance
Хорошо подходит для конфигурации:
Container.Bind<GameBalanceConfig>().FromInstance(_balanceConfig).AsSingle();
FromComponentInHierarchy
Подходит только для сценовых объектов, которые действительно уже существуют:
Container.Bind<GameplayHudView>().FromComponentInHierarchy().AsSingle();
Factory
Подходит для runtime-создания:
- врагов
- платформ
- popup-элементов
- наградных визуальных объектов
Жизненные циклы
Для сервисов в Extenject чаще всего пригодятся:
IInitializableITickableIDisposable
Практика:
IInitializableиспользуй для явного стартаITickableтолько там, где реально нужен цикл обновленияIDisposableобязательно для внешних ресурсов, особенноCOM-соединений и подписок
Пример:
public sealed class ComPortGateway : IComPortGateway, IInitializable, IDisposable
{
public void Initialize()
{
// Открытие и подготовка порта
}
public void Dispose()
{
// Корректное закрытие порта
}
}
Связка с управлением сценами
Для сцен в Extenject важны два сценария:
- передача зависимостей через
ProjectContext - передача параметров при переходах
Если нужно передавать параметры между сценами, удобнее не использовать статические поля, а оформлять отдельный переходный сервис или ZenjectSceneLoader.
Это особенно важно для переходов вида:
- из attract/menu режима в игровую сессию
- из игры в экран награды
- из игры в сервисный режим
Чего лучше избегать
- делать
ProjectContextсвалкой всех сервисов - инжектить всё подряд в
MonoBehaviour - дублировать bindings в разных installers без правил
- смешивать доменную логику и прямую работу с Unity API
- решать все проблемы через один огромный installer
Практическая схема для этого проекта
На старте проекта удобно держать такой набор installers:
ProjectInstallerGameplayInstallerMenuInstallerServiceModeInstallerPrizeFlowInstallerHardwareInstaller
Пример разбиения ответственности:
ProjectInstaller— глобальные настройки и shared-сервисыGameplayInstaller— игровой цикл и gameplay-сервисыServiceModeInstaller— настройки персоналаHardwareInstaller—COM, диагностика, витрина
Рекомендуемая карта legacy -> новые сервисы
| Legacy source | Новый слой |
|---|---|
ComSystemManager, ComHub, ComDeviceClient |
IComDeviceService, IComPortGateway, IHardwareDiagnosticsService |
PrizeStandController |
IPrizeShowcaseGateway |
PrizeSystemManager, PrizeBase, PrizeDatabase |
IPrizeService, IPrizeRepository, IPrizeExecutor |
PaymentManager, credit-события из ComSystemManager |
IPaymentService, ICreditWallet |
SettingsManager, SettingsDatabase |
IServiceSettingsService, ISettingsRepository |
QRCodeUtils, QRCodeInfo, QRCodePrize |
IQrRewardService, IQrTextureFactory |
Эта карта не означает прямое копирование классов. Она показывает, какие обязанности старого кода стоит сохранить в новой архитектуре.
Как внедрять DI поэтапно
Не нужно переводить весь проект на Extenject за один раз.
Лучше идти по шагам:
- Выбрать один legacy-блок из
Assets/Temp/Legacy, напримерCOMилиPrize. - Описать новый интерфейс без зависимости от
RR.Common.*. - Перенести минимальные модели данных.
- Написать новую реализацию или временный adapter.
- Подключить её через installer.
- Отделить UI от прикладной логики.
- Только потом строить более широкую сетку installers и фабрик.
Для каких задач это даст максимальную пользу в Danro Jump
- для сервисного режима
- для аппаратной интеграции
- для призовой логики
- для разделения игровых режимов
- для тестируемой бизнес-логики без прямой привязки к
MonoBehaviour
Официальные и полезные источники
- https://github.com/Mathijs-Bakker/Extenject
- https://github.com/Mathijs-Bakker/Extenject/releases
GUIDES/packages/zenject/README.md