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

5.6 KiB
Raw Blame History

Продвинутые конструкции C# и архитектурные решения

Этот документ описывает принципы написания чистого и производительного C# кода, применяемые (или рекомендуемые к применению) в проекте Danro Jump.

1. Паттерн "Стек" для UI (Управление состояниями)

Для управления всплывающими окнами (MainMenu -> Settings -> QR Code Info) используется структура данных Стек (Stack<T>). Она работает по принципу LIFO (Last In, First Out).

using System.Collections.Generic;

public class ModalStackController
{
    private readonly Stack<ModalWindowBase> modals = new Stack<ModalWindowBase>();

    public void Open(ModalWindowBase newModal)
    {
        // "Замораживаем" текущее верхнее окно (например, убираем у него фокус или делаем полупрозрачным)
        if (modals.TryPeek(out var currentTop))
        {
            currentTop.Suspend(); 
        }

        modals.Push(newModal);
        newModal.Activate();
    }

    public void CloseTop()
    {
        if (modals.Count == 0) return;

        // Закрываем и удаляем верхнее
        var top = modals.Pop();
        top.Deactivate();

        // "Размораживаем" то, которое оказалось под ним
        if (modals.TryPeek(out var newTop))
        {
            newTop.Resume();
        }
    }
}

2. Разделение через Интерфейсы (SOLID: Dependency Inversion)

Главное правило чистой архитектуры: Высокоуровневые модули не должны зависеть от низкоуровневых. Оба должны зависеть от абстракций (интерфейсов).

Плохо: GameManager знает о классе ComPortInputBridge. Хорошо: GameManager знает об интерфейсе IInputProvider.

// 1. Создаем абстракцию
public interface IInputProvider
{
    bool IsJumpPressed();
}

// 2. Реализуем для реального железа (COM)
public class ComInputProvider : IInputProvider { /* ... */ }

// 3. Реализуем для тестирования в редакторе (Keyboard)
public class KeyboardInputProvider : IInputProvider { /* ... */ }

// 4. Игроку всё равно, откуда пришел ввод!
public class PlayerController
{
    private readonly IInputProvider input;
    
    // Зависимость подставляется через Zenject
    public PlayerController(IInputProvider input) => this.input = input;

    public void Update()
    {
        if (input.IsJumpPressed()) Jump();
    }
}

3. Производительность (Избегание аллокаций)

Unity использует сборщик мусора (Garbage Collector). Каждая аллокация (создание объекта через new, выделение памяти под строки или массивы) рано или поздно приведет к скачку (GC Spike) и фризу (подвисанию игры).

Избегайте LINQ в горячих путях (Update)

Методы вроде .Where(), .ToList(), .Select() создают объекты "под капотом".

ПЛОХО (в методе Update):

// Аллокация памяти каждый кадр!
var activeEnemies = allEnemies.Where(e => e.IsActive).ToList(); 
foreach(var e in activeEnemies) { /* ... */ }

ХОРОШО:

// Ноль аллокаций
for (int i = 0; i < allEnemies.Count; i++)
{
    if (allEnemies[i].IsActive)
    {
        // ...
    }
}

Структуры вместо Классов для данных (Structs vs Classes)

Если вам нужен просто контейнер для данных (например, координаты и цвет), используйте struct (Value Type) вместо class (Reference Type). Структуры выделяются в стеке (Stack) и не напрягают сборщик мусора.

// Хороший кандидат на struct (весит 16 байт: float + float + float + int)
public struct SpawnPointData
{
    public float X;
    public float Y;
    public float Z;
    public int TypeId;
}

4. Модификатор sealed

Вы могли заметить, что многие классы в проекте объявлены как sealed class.

public sealed class MainMenuController : MonoBehaviour

Это делает класс недоступным для наследования. Зачем:

  1. Дизайн: Явно говорит программисту "это финальная логика, не пытайся переопределять её методы, создай новый компонент".
  2. Производительность (Девиртуализация): В компиляторе IL2CPP (используемом Unity для билдов) компилятор C++ может заменить вызовы виртуальных методов (Virtual Dispatch) на прямые вызовы, если класс sealed. Это дает бесплатный (пусть и крошечный) прирост производительности.