Подробный список изменений: - Перенесены эффекты частиц из 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-индекс проекта.
5.6 KiB
Продвинутые конструкции 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
Это делает класс недоступным для наследования. Зачем:
- Дизайн: Явно говорит программисту "это финальная логика, не пытайся переопределять её методы, создай новый компонент".
- Производительность (Девиртуализация): В компиляторе IL2CPP (используемом Unity для билдов) компилятор C++ может заменить вызовы виртуальных методов (Virtual Dispatch) на прямые вызовы, если класс
sealed. Это дает бесплатный (пусть и крошечный) прирост производительности.