Danil Iarosh 839a68aef1 Improve VFX, UI overlays, session statistics, COM reliability, audio mixer, and expand architecture guides
Подробный список изменений:
- Перенесены эффекты частиц из 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-индекс проекта.
2026-07-03 15:04:08 +03:00

7.9 KiB
Raw Blame History

Полное руководство по 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)

Зависимости живут в контейнерах, которые иерархичны.

  1. ProjectContext: Создается один раз при старте игры. Здесь лежат глобальные сервисы (AudioService, NetworkManager).
  2. SceneContext: Создается при загрузке сцены. Здесь лежит всё, что нужно только для текущего уровня (Player, LevelManager). Имеет доступ ко всему из ProjectContext.
  3. 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().