Подробный список изменений: - Перенесены эффекты частиц из C#-кода в готовые префабы-ассеты (Vfx Prefabs) в папке Assets/Resources/Vfx/ с автоматической генерацией материалов в Editor. - Исправлены ошибки duration на играющих системах частиц и розовые текстуры в GameVfxService.cs. - Исправлена навигация и фокус кнопок на оверлеях ContinueGameOverlay и PrizeResultOverlay с автоматическим подсвечиванием кнопок лимонно-зеленым цветом при фокусе и проигрыванием звука. - Добавлена проверка настроек в GameplayHudController.cs для скрытия ContinueGameOverlay, если продолжение игры отключено. - Расширен сбор статистики: создана структура GameplaySessionRecord, добавлена история до 50 раундов в PrizeSessionStateService и JsonPrizeSessionStateRepository, выведен расчет средних показателей и список 5 последних раундов в сервисной статистике. - Повышена надежность COM-транспорта: добавлен sleep (80мс) в Dispose для graceful shutdown потоков, реализован цикл переподключения до 10 попыток при обрыве связи. - Интегрированы настройки громкости с микшером: изменен шаг изменения громкости на 5% (0.05) для удобства, добавлен статус-чек и логи в ApplyServiceVolumes, расширены юнит-тесты на GetFloat параметры. - Созданы новые архитектурные гайды по аппаратному COM-транспорту и призовой экономике автомата в папке GUIDES/architecture/. - Оптимизирован и обновлен RAG-индекс проекта.
7.9 KiB
Полное руководство по Zenject (Extenject)
Zenject — это мощный фреймворк для внедрения зависимостей (Dependency Injection / Inversion of Control) в Unity. В этом проекте мы используем форк Extenject. Zenject решает проблему жесткой связности классов (spaghetti code) и избавляет от необходимости использовать паттерн Singleton.
1. Основы привязок (Bindings)
Настройка зависимостей происходит в классах-инсталляторах (обычно MonoInstaller). Главный принцип: мы говорим контейнеру, какую реализацию отдать, когда кто-то запрашивает интерфейс или класс.
Основные методы привязки
public class GameplayInstaller : MonoInstaller
{
[SerializeField] private PlayerSettings settings;
public override void InstallBindings()
{
// 1. AsSingle() — создает один экземпляр (Singleton в рамках контейнера)
Container.Bind<IPlayerMovement>().To<JumpPlayerController>().AsSingle();
// 2. AsTransient() — создает НОВЫЙ экземпляр при каждом запросе [Inject]
Container.Bind<EnemyAI>().AsTransient();
// 3. AsCached() — создает экземпляр один раз для данного типа привязки
Container.Bind<IWeapon>().To<Sword>().AsCached();
// 4. BindInstance() — привязка уже существующего объекта (например, из Инспектора)
Container.BindInstance(settings).AsSingle();
// 5. BindInterfacesAndSelfTo — класс будет доступен и по своему типу, и по всем его интерфейсам
Container.BindInterfacesAndSelfTo<GameAudioService>().AsSingle();
}
}
2. Способы внедрения (Injection)
Существует три основных способа внедрения зависимостей.
2.1 Конструктор (Constructor Injection) — РЕКОМЕНДУЕТСЯ
Используется для всех C# классов, которые не являются MonoBehaviour.
public class EnemySpawner
{
private readonly IPlayerMovement player;
private readonly EnemySettings settings;
// Зависимости передаются явно. Невозможно создать класс без них.
public EnemySpawner(IPlayerMovement player, EnemySettings settings)
{
this.player = player;
this.settings = settings;
}
}
2.2 Метод (Method Injection) — ДЛЯ MONOBEHAVIOUR
Unity создает MonoBehaviour самостоятельно, минуя конструктор. Поэтому для них используется метод, помеченный атрибутом [Inject]. Он вызывается сразу после Awake().
public class PlayerHealth : MonoBehaviour
{
private GameAudioService audioService;
[Inject]
public void Construct(GameAudioService audioService)
{
this.audioService = audioService;
}
}
2.3 Поля (Field Injection) — НЕ РЕКОМЕНДУЕТСЯ
Хоть Zenject и поддерживает инъекцию прямо в приватные поля через [Inject], это нарушает инкапсуляцию и усложняет чтение кода. Избегайте этого.
3. Жизненный цикл (Interfaces)
Zenject предоставляет интерфейсы, заменяющие стандартные Start, Update и OnDestroy Unity, что позволяет писать чистые C# классы без наследования от MonoBehaviour.
// Чтобы эти интерфейсы работали, класс нужно привязать через BindInterfacesAndSelfTo()
public class GameManager : IInitializable, ITickable, IDisposable
{
// Аналог Start()
public void Initialize()
{
Debug.Log("Game started!");
}
// Аналог Update()
public void Tick()
{
// Выполняется каждый кадр
}
// Аналог OnDestroy()
public void Dispose()
{
// Очистка ресурсов, отписка от событий
}
}
4. Контексты (Contexts)
Зависимости живут в контейнерах, которые иерархичны.
- ProjectContext: Создается один раз при старте игры. Здесь лежат глобальные сервисы (
AudioService,NetworkManager). - SceneContext: Создается при загрузке сцены. Здесь лежит всё, что нужно только для текущего уровня (
Player,LevelManager). Имеет доступ ко всему изProjectContext. - GameObjectContext: Локальный контейнер для сложных префабов (например, машина со своими колесами и двигателем).
5. Фабрики (Factories)
Никогда не используйте new для создания классов, у которых есть зависимости. Используйте фабрики Zenject!
Простая фабрика (PlaceholderFactory)
// 1. Объявляем класс
public class Bullet { /* ... */ }
// 2. Объявляем фабрику (внутри класса или отдельно)
public class BulletFactory : PlaceholderFactory<Bullet> { }
// 3. Биндим в инсталлере
Container.BindFactory<Bullet, BulletFactory>();
// 4. Используем
public class Weapon
{
private BulletFactory factory;
public Weapon(BulletFactory factory) => this.factory = factory;
public void Shoot()
{
Bullet bullet = factory.Create(); // Пуля создана, все инъекции внутри нее выполнены
}
}
6. Zenject Signals (Паттерн Event Bus)
Сигналы (Signals) — это потрясающий способ избавиться от жестких связей. Компоненты ничего не знают друг о друге, они просто отправляют и слушают сообщения (например, PlayerDiedSignal).
Настройка
В инсталлере необходимо добавить SignalBusInstaller.Install(Container); и объявить сигналы:
SignalBusInstaller.Install(Container);
Container.DeclareSignal<PlayerDiedSignal>();
Отправка сигнала
public class PlayerHealth
{
private SignalBus signalBus;
public PlayerHealth(SignalBus signalBus) => this.signalBus = signalBus;
public void Die()
{
signalBus.Fire<PlayerDiedSignal>(); // Сигнал улетел!
}
}
Подписка на сигнал
public class UIManager : IInitializable, IDisposable
{
private SignalBus signalBus;
public UIManager(SignalBus signalBus) => this.signalBus = signalBus;
public void Initialize()
{
signalBus.Subscribe<PlayerDiedSignal>(OnPlayerDied);
}
public void Dispose()
{
signalBus.Unsubscribe<PlayerDiedSignal>(OnPlayerDied);
}
private void OnPlayerDied()
{
// Показать экран GameOver
}
}
Tip
При использовании
[Inject]методов в MonoBehaviour, вы можете инжектитьSignalBusи подписываться на сигналы прямо там, а отписываться вOnDestroy().