feat: add controller disconnect/reconnect handling, MAC identification, lighting pause/resume system
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled

- Рефакторинг системы подсветки: вместо длительности в секундах теперь
  используется количество циклов (float). Добавлена таблица длительности
  циклов эффектов `LightCycleDurations` для пересчёта циклов в секунды.
  Префикс команд подсветки изменён с `85` на `86`.
- `LightSequence`: добавлены `Pause()` / `Resume()` с запоминанием текущего
  шага последовательности; возобновление продолжает с сохранённой позиции.
- `LightController`: добавлены `PauseAll()` / `ResumeAll()` для
  приостановки и восстановления подсветки и мигания шляпы при
  отключении/переподключении платы. Свойства `IsSequencePaused`,
  `IsBlinkPaused`.
- `ComDeviceClient`: после успешной авторизации отправляются команды
  `890003` и `8600`. Добавлен запрос MAC-адреса через команду `06`
  (ответ — 12 hex-символов без префикса). Новое событие `ConnectionLost`
  при начале KeepAlive-реконнекта.
- `ComHub`: очередь `_disconnectedQueue` с типом `DeviceDisconnectMessage`
  (DeviceId + Role + Mac) — сохраняет идентификатор устройства до его
  удаления из словаря. Подписка на `ConnectionLost`.
  Добавлен `GetDeviceMac()`.
- `ComSystemManager`: при инициализации, если плата не обнаружена,
  подсветка сразу ставится на паузу. При отвале дефолтного устройства —
  `PauseAll()`, при переподключении — сравнение по MAC и роли через
  `IsSameDevice()`: та же плата → `ResumeAll()`, другая → `Blink()` с нуля.
  Добавлены событие `OnDeviceDisconnected` и метод `GetDeviceMac()`.
- `COMInput`: при событии `OnDeviceDisconnected` сбрасываются все состояния
  кнопок через `ResetAllButtons()`, генерируя `buttonUp` для зажатых кнопок.
  Устраняет баг залипания управления после переподключения платы.
- `GameManager`: расширена последовательность световых эффектов в меню
  (добавлены `DiagonalWave3`, `WaveUp`, `OrangeWave`); вызовы `SetLight`
  для game over/won переведены на float-циклы.
- Обновлены `ARCHITECTURE.md` и `README.md`: переписана документация
  на актуальную структуру проекта и текущие возможности игры.
This commit is contained in:
Данил Ярош 2026-03-04 13:55:08 +03:00
parent 4374c6f63f
commit 79b66b2d17
16 changed files with 3521 additions and 2055 deletions

View File

@ -1,515 +1,230 @@
# Архитектура проекта "Mario-like platformer"
## Обзор
Это 2D-платформер на Unity, созданный по образу классических игр Mario. Проект реализован на C# с использованием Unity 2D и включает систему управления через клавиатуру или COM-порт (для аркадных автоматов).
---
Проект представляет собой 2D-платформер на Unity. Архитектура построена вокруг одного постоянного `GameManager`, набора сервисных менеджеров в `Globals`, компонентного игрового поведения на сценах и отдельной системы ввода, которая может работать как от клавиатуры, так и от COM-устройств.
## Основные технологии
- **Unity Engine** (версия для 2D)
- **Unity Input System** - новая система ввода
- **Unity Audio Mixer** - управление звуком
- **Visual Effect Graph** - визуальные эффекты
- **Addressables** - динамическая загрузка ресурсов
- **Universal Render Pipeline (URP)** - графический конвейер
Код в основном разделен по доменным папкам внутри `Assets/Scripts`, а логика сцены собирается из независимых `MonoBehaviour`-компонентов.
---
## Технологический стек
## Структура проекта
- Unity (2D-проект);
- C#;
- Unity Input System;
- Unity UI (`com.unity.ugui`);
- Cinemachine;
- Universal Render Pipeline;
- Visual Effect Graph;
- `PlayerPrefs` для части runtime-данных;
- JSON через `JsonUtility` для сохранения настроек.
### 📁 Assets/Scripts/
## Актуальная структура проекта
#### **1. Abstract/** - Базовые абстракции
- `Interfaces.cs` - интерфейсы проекта:
- `IRecallable` - для возвращаемых объектов
- `IPickable` - для подбираемых предметов
- `IDamageable` - для объектов, получающих урон
- `ILaunchable` - для запускаемых снарядов
- `IBouncy` - для отскакивающих объектов
- `Enums.cs` - перечисления (Direction2D и др.)
### `Assets/Scripts/Abstract`
#### **2. Managers/** - Центральные менеджеры (паттерн Singleton)
Основные управляющие классы игры:
Базовые перечисления и интерфейсы, на которых держатся общие контракты игрового кода:
##### `GameManager.cs` - Главный менеджер игры
- **Singleton** с автоинициализацией через `[RuntimeInitializeOnLoadMethod]`
- Управляет всеми остальными менеджерами
- Хранит игровое состояние:
- Жизни игрока (Lives)
- Монеты (Coins)
- Текущий мир и уровень (World, Stage)
- Состояние паузы (IsPaused)
- Управляет спавном игрока
- Система событий:
- `PauseStateChanged` - изменение паузы
- `PlayerSpawned` - спавн игрока
- `CoinsChanged`, `LivesChanged`, `MaxLivesChanged` - изменение игровых параметров
- `GameOverEvent`, `GameWonEvent` - завершение игры
- Интегрирует логирование через `CompositeLogger`
- `Enums.cs`;
- `Interfaces.cs`.
##### `SettingsManager.cs` - Управление настройками
- Загрузка/сохранение настроек в JSON
- Работа с `GameSettings` ScriptableObject
- События: `OnSettingsLoaded`, `OnSettingsSaved`
- Применение настроек к игре
### `Assets/Scripts/Managers`
##### `AudioManager.cs` - Управление звуком
- Управление фоновой музыкой:
- Плавные переходы (fade in/out)
- Кроссфейд между треками
- Пул AudioSource для звуковых эффектов (SFX)
- Интеграция с AudioMixer
- Round-robin система для проигрывания SFX
Глобальные менеджеры и сервисы верхнего уровня:
##### `PaymentManager.cs` - Система оплаты
- Управление кредитами для аркадного автомата
- Интеграция с COM-портом для приема монет
- События: `OnCreditsChanged`, `OnCreditAdded`, `OnCreditRemoved`
- Поддержка бесплатной игры
- Опциональное сохранение кредитов между сессиями
- `GameManager` - центральная точка управления сессией;
- `AudioManager` - музыка, пауза и SFX;
- `PaymentManager` - кредиты и логика оплаты;
- `BalanceManager` - баланс для запуска/продолжения игры;
- `InactivityManager` - реакция на бездействие;
- `GlobalsManager` - инициализация общих зависимостей;
- `PrizeSystemManager` - управление призовой системой.
##### `PrizeSystemManager.cs` - Система призов
- Управление выдачей призов
- Интеграция с внешним оборудованием через COM
### `Assets/Scripts/Settings`
##### `GlobalsManager.cs` - Глобальные настройки
- Загрузка глобальных префабов
- DontDestroyOnLoad объекты
Новая система настроек, отделенная от игрового цикла:
#### **3. Behaviour/** - Игровое поведение
- `Core/SettingsManager` - хранение значений, загрузка, сохранение, рассылка событий;
- `Core/SettingsDatabase` - схема настроек и значения по умолчанию;
- `Core/SettingsApplier` - применение значений к рантайм-системам;
- `UI/*` - генерация и связывание интерфейса настроек.
##### **Camera/**
- `CameraFollow.cs` - Следование камеры за игроком:
- Плавное следование с damping
- Offset в зависимости от направления взгляда игрока
- Автоматический поиск игрока на сцене через корутину `BindLoop()`
- Snap к игроку при первом обнаружении
- `CameraManager.cs` - Управление камерой на уровне
- `CameraControlTrigger.cs` - Триггеры для особого поведения камеры
- `CinemachineFindPlayer.cs` - Интеграция с Cinemachine
### `Assets/Scripts/Controls`
##### **Controllers/**
- `PlayerController.cs` - Управление игроком:
- Физика через Rigidbody2D
- Input System для управления (moveAction, jumpAction)
- Настройки движения: скорость, ускорение, торможение
- Прыжки с variable jump height
- Проверка приземления через GroundCheck
- Интеграция с системой здоровья (Health)
- Анимации через Animator
- Звуковые эффекты (шаги, урон, прыжки)
- Реализует интерфейс `IDamageable`
Подсистема ввода и интеграции с внешним оборудованием:
- `EnemyController.cs` - Управление врагами
- `MusicController.cs` - Контроллер музыки на сценах
- `Inputs/In.cs` - фасад поверх выбранного источника ввода;
- `Inputs/InputSource*` - абстракции и реализации источников;
- `Inputs/KeyboardInput.cs` - клавиатурное управление;
- `Inputs/COMInput.cs` - чтение данных с COM-контроллера;
- `Inputs/ComSystem/*` - подключение устройств, handshake, освещение, призовые стойки и маршрутизация сообщений;
- `ComInputToInputSystemAdapter.cs` - адаптер в сторону Unity Input System.
##### **Checkpoints/**
- `Checkpoint.cs` - Чекпоинты на уровне
- `PlayerSpawn.cs` - Точки спавна игрока
- `CheckpointSystem.cs` - Система управления чекпоинтами
### `Assets/Scripts/Behaviour`
##### **Projectiles/** - Система снарядов
- `Projectile2D.cs` - Базовый снаряд
- `BasicProjectile.cs` - Простой снаряд
- `PhysicsProjectile.cs` - Снаряд с физикой
- `PickableProjectile.cs` - Подбираемый снаряд
- `EnemyBasicProjectile.cs` - Снаряды врагов
Основная игровая логика, размещаемая на объектах сцен:
##### **ThrowProjectiles/** - Система стрельбы
- `FireScript2D.cs` - Базовая стрельба
- `PlayerFireScript.cs` - Стрельба игрока
- `EnemyFireScript.cs` - Стрельба врагов
- `Controllers/*` - игрок, враги, сценовая музыка;
- `Camera/*` - слежение и ограничение поведения камеры;
- `Checkpoints/*` - точки возрождения и текущий активный чекпоинт;
- `Projectiles/*` и `ThrowProjectiles/*` - снаряды и логика стрельбы;
- `Sequence/*` - сценарные последовательности действий;
- `Health`, `LevelFinish`, `ScoreSystem`, `MessageController`, `Parallax`, `LightDimmer`, пулы VFX и окружения.
##### **Другое**
- `Health.cs` - Компонент здоровья:
- Максимальное и текущее здоровье
- TakeDamage, Heal, ResetHealth
- Связь с владельцем через `IDamageable`
- `Parallax.cs` - Эффект параллакса для фона
- `LightDimmer.cs` - Управление освещением
### `Assets/Scripts/Entities`
#### **4. Entities/** - Игровые сущности
Интерактивные сущности уровня:
##### **Платформы:**
- `MovingPlatform.cs` - Движущиеся платформы
- `Oscillator.cs` - Осциллирующие платформы
- `ShiftingPlatform.cs` - Смещающиеся платформы
- `VanishingPlatform.cs` - Исчезающие платформы:
- Плавное исчезновение/появление (fade in/out)
- Корутины `Vanish()` и `Reappear()`
- Отключение CompositeCollider2D
- Работа с массивом SpriteRenderer для анимации прозрачности
- платформы (`MovingPlatform`, `ShiftingPlatform`, `TrajectoryPlatform`, `VanishingPlatform`, `TriggeringPlatform`, `HidingTriggeringPlatform`);
- собираемые объекты (`CoinCollectible`, `LeafCollectible`, `BatCollectible`);
- ловушки и рычаги (`Trap`, `Lever`, `DeactivatingLever`);
- прочие физические объекты (`JumpPad2D`, `Rotator`, `Oscillator`, `LightActionsController`).
##### **Props/** - Интерактивные объекты
- `Collectible.cs` - Собираемые предметы
- `CoinCollectible.cs` - Монеты
- `Trap.cs` - Ловушки
### `Assets/Scripts/UI`
##### **Другое:**
- `JumpPad2D.cs` - Трамплины
- `Rotator.cs` - Вращающиеся объекты
- `LightActionsController.cs` - Управление светом
Пользовательский интерфейс игры и служебные окна:
#### **5. Controls/** - Система управления
- главное меню, пауза, выбор уровня, финальные баннеры;
- верхняя панель статуса, отображение кредитов и баланса;
- модальная система (`UI/Modal`);
- текстовый ввод (`UI/TextInput`);
- служебные окна статистики и QR-кодов.
##### **Inputs/** - Абстракция ввода
- `In.cs` - Статический фасад для унифицированного доступа к вводу:
- Выбор источника ввода (клавиатура/COM)
- Автоматическая инициализация
- Единый интерфейс для игровой логики
### Дополнительные домены
- `InputSource.cs` - Базовый класс источника ввода
- `InputSourceType.cs` - Типы источников (Keyboard/COM)
- `KeyboardInput.cs` - Ввод с клавиатуры
- `COMInput.cs` - Ввод через COM-порт
- `Audio` - обвязка звуковых эффектов и сценовых аудио-наборов;
- `Animations` - обработчики animation events;
- `FX` - запуск визуальных эффектов;
- `ScriptableObjects` - конфигурация врагов, SFX и sequence actions;
- `Utilities` - логирование, утилиты, атрибуты, QR-коды, форматирование и отладочные настройки;
- `Editor` - кастомные инспекторы и drawers.
##### **ComSystem/** - Работа с COM-портом
- `ComWrapper.cs` - Обертка для работы с COM
- `ComNativeWrapper.cs` - Нативные вызовы
- `PrizeSystem.cs` - Интеграция призовой системы
## Ключевые runtime-компоненты
##### **Другое:**
- `PlayerAction.cs` - Действия игрока
- `ButtonPinMapping.cs` - Маппинг кнопок на пины
- `ComInputToInputSystemAdapter.cs` - Адаптер COM → Input System
### `GameManager`
#### **6. UI/** - Пользовательский интерфейс
- `MainMenu.cs` - Главное меню
- `PauseMenu.cs` - Меню паузы
- `GameEndUIController.cs` - UI завершения игры
- `LoseBannerController.cs` - Баннер поражения (с таймером продолжения)
- `SettingsUI.cs` - Модальное окно настроек
- `LevelSelectModal.cs` - Модальное окно выбора уровня
- `LevelButton.cs` - Кнопка уровня
- `TopPanelTextController.cs` - Верхняя панель с информацией
- `UICreditsTextController.cs` - Отображение кредитов
- `QRCodeUIController.cs` - Отображение QR-кодов
- `UIPointer.cs` - Указатель в меню
- `ScrollSelectionFollower.cs` - Следование скролла за выбранным элементом
- `ModalFocusTrap.cs` - Захват фокуса в модальных окнах
- `FullscreenToggle.cs` - Переключение полноэкранного режима
`GameManager` создается автоматически через `RuntimeInitializeOnLoadMethod`, живет между сценами и является главным координационным узлом:
#### **7. Settings/** - Система настроек
- **Core/**
- `SettingsManager.cs` - Менеджер настроек (синглтон)
- `SettingsDatabase.cs` - ScriptableObject база данных настроек
- `SettingsApplier.cs` - Применение настроек (AudioMixer и т.д.)
- `SettingsButtonDispatcher.cs` - Диспетчер событий кнопок настроек
- `SettingsButtonActions.cs` - Готовые действия для кнопок
- **UI/**
- `SettingsUIGenerator.cs` - Генератор UI из базы данных
- `SettingsGroupPanel.cs` - Сворачиваемая группа настроек
- `SettingUIItem.cs` - Элемент настройки (toggle, slider, button и т.д.)
- `SettingsButtonBinder.cs` - Биндер кнопок для взаимодействия с объектами сцены
- `SettingToggleBinder.cs`, `SettingSliderBinder.cs` - Биндеры для ручной привязки
- инициализирует логгер;
- поднимает `AudioManager`;
- загружает prefab `Resources/Prefabs/Global/Globals`;
- получает ссылки на `SettingsManager`, `PaymentManager`, `BalanceManager`, `ComSystemManager`;
- управляет паузой, жизнями, монетами, переходами между уровнями и спавном игрока;
- публикует основные события игры (`PauseStateChanged`, `PlayerSpawned`, `CoinsChanged`, `LivesChanged`, `GameOverEvent`, `GameWonEvent`).
#### **8. ScriptableObjects/** - Ресурсы конфигурации
- `SFXAsset.cs` - Набор звуковых эффектов
- `EnemySFXAsset.cs` - Звуки врагов
- `EnemyControllerConfiguration.cs` - Конфигурация врагов
- `EnemyFireConfiguration.cs` - Конфигурация стрельбы врагов
### `Globals`
#### **9. Animations/** - Анимационные события
- `PlayerAnimationEvents.cs` - События анимаций игрока
- `EnemyAnimationEvents.cs` - События анимаций врагов
Глобальный prefab из `Resources/Prefabs/Global/Globals` выступает контейнером для постоянных компонентов, которые должны существовать независимо от текущей сцены. Через него `GameManager` подцепляет сервисы настроек, оплаты и COM-системы.
#### **10. Audio/** - Звуковая система
- `SoundEffect.cs` - Базовый звуковой эффект
- `LevelPlayerSFX.cs` - SFX для уровня
### `SettingsManager`
#### **11. FX/** - Визуальные эффекты
- `PlayerRunVFX.cs` - Эффекты бега игрока
- `CheckPointFX.cs` - Эффекты чекпоинтов
Новая подсистема настроек работает независимо от старой игровой логики и хранит значения в `persistentDataPath/settings.json`:
#### **12. Utilities/** - Вспомогательные утилиты
- `FileLogger.cs` - Логирование в файл
- `CompositeLogger.cs` - Комбинированное логирование
- `LoggerInitializer.cs` - Инициализация логгера
- `DebugSettings.cs` - Настройки отладки
- `QRCodeUtils.cs` - Утилиты для QR-кодов
- `PhoneNumberUtils.cs` - Валидация телефонных номеров (мультистрановая)
- `AutoCursorHider.cs` - Автоматическое скрытие курсора
- `ReadOnlyInInspectorAttribute.cs` - Атрибут только для чтения в Inspector
- `AdvancedLoggingExample.cs` - Пример продвинутого логирования
- читает схему из `SettingsDatabase`;
- приводит входные значения к допустимому диапазону через `Sanitize`;
- применяет изменения сразу через `SettingsApplier`;
- сообщает об изменениях через `OnSettingsLoaded`, `OnSettingsSaved`, `OnSettingChanged`.
#### **13. Editor/** - Редакторские скрипты
- `MusicController_Inspector.cs` - Кастомный инспектор для MusicController
- `SettingEntryDrawer.cs` - Кастомный drawer для настроек
- `SettingsDatabaseEditor.cs` - Редактор базы данных настроек
### `ComSystemManager`
---
Подсистема внешних устройств отвечает за:
## Архитектурные паттерны
- обнаружение одного или нескольких COM-устройств;
- handshake и идентификацию роли устройства;
- прием монет и обновление кредитов;
- маршрутизацию входных битов от контроллера;
- управление подсветкой через `LightController`;
- подключение специализированных обработчиков, например призовой стойки.
### 1. **Singleton Pattern**
Все менеджеры реализованы как синглтоны с автоинициализацией:
```csharp
public static GameManager Instance { get; private set; }
## Потоки управления
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
if (Instance != null) return;
GameObject go = new GameObject(nameof(GameManager));
Instance = go.AddComponent<GameManager>();
DontDestroyOnLoad(go);
}
```
### Запуск приложения
### 2. **Event-Driven Architecture**
Все менеджеры используют события для коммуникации:
```csharp
public event Action<bool> PauseStateChanged;
public event Action<GameObject> PlayerSpawned;
public UnityEvent<int> OnCreditsChanged;
```
1. Unity вызывает автоматическую инициализацию `GameManager`.
2. `GameManager.Awake()` создает логгер, аудио и поднимает `Globals`.
3. Из `Globals` получаются ссылки на настройки, оплату, баланс и COM-систему.
4. После загрузки сцены уровня `GameManager` пытается заспавнить игрока в `PlayerSpawn`.
### 3. **Dependency Injection через Inspector**
Компоненты конфигурируются через Inspector с использованием SerializeField:
```csharp
[SerializeField] private InputActionReference moveAction;
[SerializeField] private float moveSpeed = 8f;
```
### Старт игровой сессии
### 4. **Component-Based Architecture**
Игровые объекты состоят из переиспользуемых компонентов:
- `Health` - здоровье
- `PlayerController` - управление
- `Animator` - анимация
1. `GameManager.NewGame()` проверяет режим бесплатной игры и доступный баланс.
2. Баланс конвертируется в стартовое число жизней.
3. Сбрасываются монеты и статистика (`ScoreSystem`).
4. Загружается сцена первого уровня и аддитивно сцена `Level UI`.
### 5. **Interface Segregation**
Четко определенные интерфейсы для разных типов поведения:
- `IDamageable` - может получать урон
- `IPickable` - можно подобрать
- `ILaunchable` - можно запустить
### Смерть и возрождение
### 6. **Facade Pattern**
`In.cs` предоставляет единый интерфейс для работы с разными источниками ввода.
1. Игровой объект получает урон через `Health`.
2. При необходимости вызывается `GameManager.RespawnPlayer()`.
3. Менеджер уменьшает число жизней.
4. Если жизни есть, игрок переносится к активному чекпоинту или стартовой точке.
5. Если жизни закончились, вызывается событие окончания игры.
### 7. **Object Pool**
AudioManager использует пул AudioSource для эффективного проигрывания SFX.
### Переход между уровнями
### 8. **ScriptableObject Configuration**
Конфигурация через ScriptableObjects для удобного редактирования:
- `GameSettings`
- `SFXAsset`
- `EnemyControllerConfiguration`
1. `LevelFinish` или другая игровая логика вызывает `GameManager.NextLevel()`.
2. `GameManager` вычисляет следующую сцену по схеме `world-stage`.
3. Если сцена существует в build settings, она загружается.
4. Если следующей сцены нет, игра считается пройденной.
---
## Архитектурные принципы
## Потоки данных
### Компонентная модель Unity
### 1. **Инициализация игры**
```
Runtime Initialize → GameManager.Awake()
├── Инициализация Logger
├── Загрузка Globals
├── Инициализация SettingsManager
├── Инициализация AudioManager
└── Инициализация PaymentManager
```
Игровая логика вынесена в независимые `MonoBehaviour`, которые комбинируются на prefab-ах и объектах сцен. Большинство сущностей собирается из небольших компонентов вместо глубокой иерархии наследования.
### 2. **Игровой цикл**
```
Player Input → InputSystem/COMInput
↓
PlayerController.Update/FixedUpdate
↓
Physics (Rigidbody2D)
↓
Animator
↓
Camera Follow
```
### Централизованное orchestration + событийная связь
### 3. **Система урона**
```
Trap/Enemy → IDamageable.TakeDamage()
↓
Health.TakeDamage()
↓
Health <= 0? → IDamageable.Die()
↓
GameManager.GameOverEvent
```
`GameManager` координирует верхнеуровневый поток игры, но отдельные подсистемы общаются через события, а не через жесткие прямые вызовы между всеми слоями.
### 4. **Система настроек**
```
SettingsUI → GameSettings (ScriptableObject)
↓
SettingsManager.SaveSettings()
↓
JSON File (persistentDataPath)
↓
SettingsManager.LoadSettings()
↓
GameSettings.ApplySettings()
```
### Конфигурация через `ScriptableObject`
---
Настройки, конфигурации врагов, аудио-наборы и сценарные действия вынесены в ресурсы, чтобы часть поведения менялась без переписывания кода.
## Ключевые особенности
### Разделение по доменам
### 1. **Поддержка аркадных автоматов**
- Ввод через COM-порт
- Система приема монет
- Выдача призов
- Бесплатная игра (настраиваемо)
Код разделен не по техническим слоям MVC, а по игровым областям ответственности: `Managers`, `Behaviour`, `Controls`, `UI`, `Entities`, `Settings`.
### 2. **Гибкая система управления**
- Unity Input System
- Поддержка клавиатуры и аркадных кнопок
- Легко расширяемая через `InputSource`
### Фасад над вводом
### 3. **Продвинутое логирование**
- Составное логирование (консоль + файл)
- Категории логов
- Уровни важности
`In.cs` скрывает конкретный источник сигналов от остального игрового кода. Благодаря этому один и тот же gameplay-код может работать и с клавиатурой, и с COM-платой.
### 4. **Система сохранения**
- JSON сериализация настроек
- Сохранение в `persistentDataPath`
- Валидация при загрузке
## Сцены и ресурсы
### 5. **Аудио система**
- Плавные переходы музыки
- Эффективный пул для SFX
- Интеграция с AudioMixer
### Сцены
### 6. **UI навигация**
- Поддержка навигации без мыши (для автоматов)
- Визуальный указатель выбора
- Автоскролл к выбранному элементу
- Модальные окна с фокусом
В проекте явно присутствуют:
---
- `MainMenu`;
- `Level UI`;
- игровые сцены уровня (например, `1-1`, `99-99`) по шаблону `world-stage`.
### `Resources`
Критичные runtime-ресурсы, которые подгружаются из кода:
- `Prefabs/Global/Globals`;
- `Prefabs/Player_New` (или альтернативный player prefab);
- `Audio/Master`;
- `SettingsDatabase`.
Это означает, что проект по-прежнему опирается на `Resources.Load` для части базовой инициализации.
## Пространства имен
Проект использует следующие пространства имен:
- `RR.Game2D.Core` - основной функционал игры
- `RR.Game2D.Core.Managers` - менеджеры
- `RR.Game2D.Behaviour` - поведение объектов
- `RR.Game2D.Entities` - игровые сущности
- `RR.Common.Managers` - общие менеджеры
- `RR.Common.Controls` - система управления
- `RR.Common.UI` - пользовательский интерфейс
- `RR.Common.Interfaces` - интерфейсы
- `RR.Common.Utility` - утилиты
- `RR.Common.Audio` - звуковая система
- `RR.Common.Behaviour.ScriptableObjects` - ScriptableObjects
Основные пространства имен в текущем коде:
---
- `RR.Game2D.Core.Managers`;
- `RR.Common.Settings`;
- `RR.Common.Controls`;
- `RR.Common.Behaviour`;
- `RR.Game2D.Behaviour`;
- `RR.Game2D.Entities`;
- `RR.Common.Utility`.
## Зависимости и интеграции
## Что важно учитывать при изменениях
### Unity пакеты:
- Input System
- Universal Render Pipeline (URP)
- Visual Effect Graph
- Addressables
- Cinemachine (опционально)
- TextMeshPro
### Внешние зависимости:
- System.IO.Ports (для COM-порта)
- System.Text.Json (для сохранения настроек)
---
## Принципы разработки
1. **Модульность** - каждый компонент независим и переиспользуем
2. **Расширяемость** - легко добавлять новые функции через интерфейсы
3. **Конфигурируемость** - большинство параметров настраивается в Inspector
4. **Отказоустойчивость** - валидация, проверки на null, fallback значения
5. **Производительность** - пулы объектов, кэширование компонентов
6. **Читаемость** - четкие названия, комментарии, XML документация
7. **Отладка** - продвинутая система логирования
---
## Структура сцен
### Persistent Scene (DontDestroyOnLoad):
- GameManager
- SettingsManager
- AudioManager
- PaymentManager
- Globals
### Level Scene:
- Player
- Enemies
- Platforms
- Collectibles
- UI Canvas
- Camera
- Lighting
---
## Порядок выполнения (Execution Order)
1. **-1000**: GameManager (через `[DefaultExecutionOrder(-1000)]`)
2. **Default**: Остальные компоненты
3. **Late Update**: Camera Follow
---
## Рекомендации по расширению
### Добавление нового врага:
1. Создать префаб с компонентами Rigidbody2D, Collider2D, Animator
2. Добавить компонент `Health` и реализовать `IDamageable`
3. Добавить `EnemyController` или создать новый контроллер
4. Создать `EnemySFXAsset` и `EnemyControllerConfiguration`
### Добавление новой платформы:
1. Наследоваться от MonoBehaviour
2. Использовать Rigidbody2D для физики
3. Реагировать на `OnCollisionEnter2D`/`OnCollisionExit2D`
4. Использовать корутины для анимаций
### Добавление нового источника ввода:
1. Создать класс, наследующий `InputSource`
2. Реализовать методы `Initialize()`, `GetAxis()`, `GetButton()` и т.д.
3. Добавить новый тип в `InputSourceType`
4. Обновить switch в `In.Initialize()`
### Добавление новой настройки:
1. Открыть `MainSettingsDatabase.asset` в инспекторе
2. Добавить новую запись в список `Entries`:
- `Key` - уникальный ключ (например, `gameplay.newSetting`)
- `Display Name` - отображаемое имя
- `Group` - группа настроек
- `Value Type` - тип значения (Bool, Int, Float, String, Button, OptionPicker)
- Значения по умолчанию и диапазоны
3. Если нужно применять настройку автоматически:
- Указать `Apply Target` (например, AudioMixerFloat01ToDb)
- Или добавить логику в `SettingsApplier.cs`
4. Для кнопок - использовать `SettingsButtonBinder` на объекте сцены
5. Для доступа из кода - добавить ключ в `SettingKeys` и свойство в `SettingsManager`
---
## Известные особенности
1. **Система настроек**: Использует ScriptableObject `SettingsDatabase` для хранения конфигурации и JSON-файл для сохранения значений.
2. **Временные файлы**: Файлы с суффиксом `_Temp` являются временными решениями или устаревшими версиями.
3. **COM-интеграция**: Работа с COM-портом требует правильной настройки прав доступа в Windows и корректного маппинга пинов.
4. **Логирование**: Логи пишутся в файл `game.log` в корневой папке проекта (рядом с Assets).
5. **Ресурсы**: Префабы загружаются из папки Resources по заданным путям (настраиваемо в Inspector).
---
## Автор
Проект разработан с использованием пространства имен `RR` (предположительно инициалы разработчика или компании).
---
*Документ создан автоматически на основе анализа структуры проекта.*
- `GameManager` остается точкой входа и зависит от структуры `Globals`; изменения в prefab-е могут ломать инициализацию всей игры.
- Схема имен сцен (`1-1`, `1-2` и т.д.) встроена в логику переходов между уровнями.
- Настройки частично влияют на базовый gameplay (`startLives`, `additionalLives`, параметры оплаты).
- COM-подсистема тесно связана с аркадным режимом: кредиты, свет, призовые устройства и внешний ввод.
- Для части критичных объектов используются пути в `Resources`, поэтому переименование или перенос этих ассетов требует обновления строковых путей в коде.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -208538,6 +208538,19 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_Target: {fileID: 1476321865}
m_TargetAssemblyTypeName: RR.Game2D.Behaviour.EnvironmentalCollectibleSpawner,
Assembly-CSharp
m_MethodName: SetEntryByName
m_Mode: 5
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument: bat
m_BoolArgument: 0
m_CallState: 2
OnEventTriggerExit:
m_PersistentCalls:
m_Calls: []
@ -208616,7 +208629,7 @@ Transform:
m_GameObject: {fileID: 1476321861}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 13, z: 0}
m_LocalPosition: {x: 0, y: 15, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@ -212052,6 +212065,19 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_Target: {fileID: 1476321865}
m_TargetAssemblyTypeName: RR.Game2D.Behaviour.EnvironmentalCollectibleSpawner,
Assembly-CSharp
m_MethodName: SetEntryByName
m_Mode: 5
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument: bat
m_BoolArgument: 0
m_CallState: 2
OnEventTriggerExit:
m_PersistentCalls:
m_Calls: []

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@ namespace RR.Common.Controls
//ОБЯЗАТЕЛЬНЫЙ БЛОК
ComSystemManager.OnInputReceived.AddListener(UpdateInputState);
ComSystemManager.OnDeviceDisconnected?.AddListener(OnDeviceDisconnected);
}
catch (Exception e)
{
@ -42,6 +42,28 @@ namespace RR.Common.Controls
return true;
}
/// <summary>
/// Сбрасывает все состояния кнопок в «не нажато».
/// Вызывается при потере связи с платой.
/// </summary>
private void ResetAllButtons()
{
for (int i = 0; i < PIN_COUNT; i++)
{
if (buttonState[i])
{
buttonUp[i] = true;
}
buttonState[i] = false;
buttonDown[i] = false;
}
}
private void OnDeviceDisconnected(string deviceId)
{
ResetAllButtons();
}
#region PUBLIC_API
//ЭТА ГРУППА МЕТОДОВ ДЛЯ ПОЛУЧЕНИЯ СОСТОЯНИЯ ЛЮБОЙ КНОПКИ ПО ПИНУ (enum ButtonPinMapping)

View File

@ -53,6 +53,7 @@ namespace RR.Common.Controls
private TaskCompletionSource<bool> _handshakeCompletion;
private TaskCompletionSource<bool> _chalReceivedCompletion;
private TaskCompletionSource<string> _roleCompletion;
private TaskCompletionSource<string> _macCompletion;
private enum HandshakeStage
{
@ -69,12 +70,14 @@ namespace RR.Common.Controls
public bool IsOperational => _isOperational;
public string LastInputBits => _lastInputBits;
public string ControllerRole { get; private set; } = string.Empty;
public string MacAddress { get; private set; } = string.Empty;
public event Action<string> CoinAccepted;
public event Action<string, string> InputReceived;
public event Action<string, string> RawLineReceived;
public event Action<string> Authorized;
public event Action<string> Disconnected;
public event Action<string> ConnectionLost;
public ComDeviceClient(
string portName,
@ -152,6 +155,7 @@ namespace RR.Common.Controls
_handshakeCompletion?.TrySetCanceled();
_chalReceivedCompletion?.TrySetCanceled();
_roleCompletion?.TrySetCanceled();
_macCompletion?.TrySetCanceled();
ControllerRole = string.Empty;
_keepAliveTask = null;
@ -225,6 +229,11 @@ namespace RR.Common.Controls
return;
}
if (TryHandleMacLine(line))
{
return;
}
if (line.StartsWith("INPUT", StringComparison.Ordinal))
{
_inputToggle = 1;
@ -280,7 +289,7 @@ namespace RR.Common.Controls
return;
}
Debug.LogWarning($"[COM AUTH][{DeviceId}] Handshake timeout. Last board response: {_lastHandshakeResponse}");
GameManager.Logger.LogWarning($"COM AUTH [{DeviceId}]", $"Handshake timeout. Last board response: {_lastHandshakeResponse}");
FailHandshake($"Таймаут рукопожатия. Последний ответ платы: {_lastHandshakeResponse}");
}
@ -414,6 +423,8 @@ namespace RR.Common.Controls
Debug.Log($"[COM AUTH][{DeviceId}] Device marked operational");
SendRaw("2171", true);
SendRaw("890003", true);
SendRaw("8600", true);
if (_keepAlive && (_keepAliveTask == null || _keepAliveTask.IsCompleted))
{
@ -494,6 +505,9 @@ namespace RR.Common.Controls
_lastInputBits = DefaultInputBits;
_inputToggle = 0;
ControllerRole = string.Empty;
MacAddress = string.Empty;
ConnectionLost?.Invoke(DeviceId);
lock (_ioLock)
{
@ -595,9 +609,50 @@ namespace RR.Common.Controls
return false;
}
private bool TryHandleMacLine(string line)
{
if (_macCompletion == null) return false;
if (!_macCompletion.Task.IsCompleted && TryExtractMac(line, out string mac))
{
MacAddress = mac;
_macCompletion.TrySetResult(mac);
return true;
}
return false;
}
private static bool TryExtractMac(string line, out string mac)
{
mac = string.Empty;
if (string.IsNullOrWhiteSpace(line)) return false;
string trimmed = line.Trim();
// Ответ на "06" — голый MAC без двоеточий, 12 hex-символов
if (trimmed.Length == 12 && IsHexString(trimmed))
{
mac = trimmed.ToUpperInvariant();
return true;
}
return false;
}
private static bool IsHexString(string s)
{
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
return false;
}
return true;
}
private async Task ResolveRoleAfterAuthorizationAsync(CancellationToken token)
{
// Роль считываем только после завершенной авторизации рукопожатия.
// Роль и MAC считываем только после завершенной авторизации рукопожатия.
if (!_enableEncryptedHandshake || !_isAuthorized || string.IsNullOrWhiteSpace(_roleRequestCommand))
{
Authorized?.Invoke(DeviceId);
@ -606,6 +661,7 @@ namespace RR.Common.Controls
try
{
// --- Запрос роли ---
_roleCompletion = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
SendRaw(_roleRequestCommand, false);
@ -622,6 +678,26 @@ namespace RR.Common.Controls
GameManager.Logger?.LogWarning("ComDeviceClient",
$"[{DeviceId}] Не удалось получить роль контроллера за {_roleRequestTimeoutMs}ms");
}
// --- Запрос MAC-адреса ---
_macCompletion = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
SendRaw("06", false);
Task macTimeoutTask = Task.Delay(_roleRequestTimeoutMs, token);
Task macFinished = await Task.WhenAny(_macCompletion.Task, macTimeoutTask);
if (macFinished == _macCompletion.Task &&
_macCompletion.Task.IsCompletedSuccessfully &&
!string.IsNullOrWhiteSpace(_macCompletion.Task.Result))
{
MacAddress = _macCompletion.Task.Result;
GameManager.Logger?.Log("ComDeviceClient",
$"[{DeviceId}] MAC-адрес: {MacAddress}");
}
else
{
GameManager.Logger?.LogWarning("ComDeviceClient",
$"[{DeviceId}] Не удалось получить MAC-адрес за {_roleRequestTimeoutMs}ms");
}
}
catch (OperationCanceledException)
{
@ -629,7 +705,7 @@ namespace RR.Common.Controls
}
catch (Exception e)
{
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Role resolve error: {e.Message}");
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Role/MAC resolve error: {e.Message}");
}
finally
{

View File

@ -13,6 +13,7 @@ namespace RR.Common.Controls
private readonly ConcurrentQueue<string> _coinQueue = new();
private readonly ConcurrentQueue<DeviceInputMessage> _inputQueue = new();
private readonly ConcurrentQueue<string> _authorizedQueue = new();
private readonly ConcurrentQueue<DeviceDisconnectMessage> _disconnectedQueue = new();
private readonly bool _useAllDetectedPorts;
private readonly string[] _explicitPorts;
@ -106,6 +107,10 @@ namespace RR.Common.Controls
while (_authorizedQueue.TryDequeue(out _))
{
}
while (_disconnectedQueue.TryDequeue(out _))
{
}
}
public bool SendToDefault(string message)
@ -160,6 +165,8 @@ namespace RR.Common.Controls
public bool TryDequeueAuthorized(out string deviceId) => _authorizedQueue.TryDequeue(out deviceId);
public bool TryDequeueDisconnected(out DeviceDisconnectMessage message) => _disconnectedQueue.TryDequeue(out message);
public string GetDeviceRole(string deviceId)
{
if (string.IsNullOrWhiteSpace(deviceId))
@ -172,6 +179,18 @@ namespace RR.Common.Controls
: string.Empty;
}
public string GetDeviceMac(string deviceId)
{
if (string.IsNullOrWhiteSpace(deviceId))
{
return string.Empty;
}
return _devices.TryGetValue(deviceId, out ComDeviceClient client)
? client.MacAddress
: string.Empty;
}
public PrizeStandController GetPrizeStandController(string deviceId = null)
{
if (!string.IsNullOrWhiteSpace(deviceId))
@ -248,6 +267,7 @@ namespace RR.Common.Controls
client.RawLineReceived += OnDeviceRawLineReceived;
client.Authorized += OnDeviceAuthorized;
client.Disconnected += OnDeviceDisconnected;
client.ConnectionLost += OnDeviceConnectionLost;
}
private void UnsubscribeDevice(ComDeviceClient client)
@ -257,6 +277,7 @@ namespace RR.Common.Controls
client.RawLineReceived -= OnDeviceRawLineReceived;
client.Authorized -= OnDeviceAuthorized;
client.Disconnected -= OnDeviceDisconnected;
client.ConnectionLost -= OnDeviceConnectionLost;
}
private void OnDeviceCoinAccepted(string deviceId)
@ -330,6 +351,13 @@ namespace RR.Common.Controls
{
_defaultDeviceId = _devices.Keys.FirstOrDefault() ?? string.Empty;
}
_disconnectedQueue.Enqueue(deviceId);
}
private void OnDeviceConnectionLost(string deviceId)
{
_disconnectedQueue.Enqueue(deviceId);
}
private IComControllerHandler CreateHandlerForDevice(string deviceId)
@ -356,6 +384,20 @@ namespace RR.Common.Controls
InputBits = inputBits;
}
}
internal readonly struct DeviceDisconnectMessage
{
public readonly string DeviceId;
public readonly string Role;
public readonly string Mac;
public DeviceDisconnectMessage(string deviceId, string role, string mac)
{
DeviceId = deviceId;
Role = role ?? string.Empty;
Mac = mac ?? string.Empty;
}
}
}

View File

@ -59,6 +59,7 @@ namespace RR.Common.Controls
public static UnityEvent<string, string> OnDeviceInputReceived;
public static UnityEvent<string> OnDeviceAuthorized;
public static UnityEvent<string> OnDeviceDisconnected;
private static ComSystemManager _instance;
private static int _creditsCount;
@ -66,6 +67,9 @@ namespace RR.Common.Controls
private ComHub _manager;
private LightController _light;
private bool _initializeInvoked;
private string _lastDefaultDeviceId = string.Empty;
private string _lastDefaultRole = string.Empty;
private string _lastDefaultMac = string.Empty;
public LightController Light => _light;
@ -78,6 +82,7 @@ namespace RR.Common.Controls
OnInputReceived = new UnityEvent<string>();
OnDeviceInputReceived = new UnityEvent<string, string>();
OnDeviceAuthorized = new UnityEvent<string>();
OnDeviceDisconnected = new UnityEvent<string>();
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
_creditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
@ -98,6 +103,14 @@ namespace RR.Common.Controls
CreateRoleHandler);
_manager.InitializeDevices();
// Если плата изначально не подключена — ставим подсветку на паузу
if (string.IsNullOrWhiteSpace(_manager.DefaultDeviceId))
{
_light.PauseAll();
GameManager.Logger?.LogWarning("ComSystemManager",
"Плата не обнаружена при инициализации — подсветка приостановлена");
}
}
public static void AddCredit()
@ -189,6 +202,11 @@ namespace RR.Common.Controls
return _instance?._manager?.GetDeviceRole(deviceId) ?? string.Empty;
}
public static string GetDeviceMac(string deviceId)
{
return _instance?._manager?.GetDeviceMac(deviceId) ?? string.Empty;
}
public static PrizeStandController GetPrizeStandController(string deviceId = null)
{
return _instance?._manager?.GetPrizeStandController(deviceId);
@ -265,6 +283,23 @@ namespace RR.Common.Controls
}
}
while (_manager.TryDequeueDisconnected(out DeviceDisconnectMessage dcMsg))
{
// Запоминаем идентификатор отключившегося устройства по умолчанию
if (dcMsg.DeviceId == _lastDefaultDeviceId || string.IsNullOrWhiteSpace(_manager.DefaultDeviceId))
{
_lastDefaultRole = dcMsg.Role;
_lastDefaultMac = dcMsg.Mac;
GameManager.Logger?.Log("ComSystemManager",
$"Устройство [{dcMsg.DeviceId}] отключилось (role={_lastDefaultRole}, mac={_lastDefaultMac})");
_light.PauseAll();
}
OnDeviceDisconnected?.Invoke(dcMsg.DeviceId);
}
while (_manager.TryDequeueAuthorized(out string deviceId))
{
OnDeviceAuthorized?.Invoke(deviceId);
@ -277,7 +312,29 @@ namespace RR.Common.Controls
if (deviceId == _manager.DefaultDeviceId)
{
_light.Blink();
string newRole = _manager.GetDeviceRole(deviceId);
string newMac = _manager.GetDeviceMac(deviceId);
bool isSameDevice = IsSameDevice(_lastDefaultRole, _lastDefaultMac, newRole, newMac);
GameManager.Logger?.Log("ComSystemManager",
$"Устройство [{deviceId}] авторизовано (role={newRole}, mac={newMac}, " +
$"sameDevice={isSameDevice})");
_lastDefaultDeviceId = deviceId;
_lastDefaultRole = newRole;
_lastDefaultMac = newMac;
if (isSameDevice && (_light.IsSequencePaused || _light.IsBlinkPaused))
{
// Та же плата переподключилась — восстанавливаем эффекты
_light.ResumeAll();
}
else
{
// Другая плата или первое подключение — запускаем мигание с нуля
_light.StopSequence();
_light.Blink();
}
}
}
@ -300,6 +357,36 @@ namespace RR.Common.Controls
_manager = null;
_initializeInvoked = false;
}
/// <summary>
/// Сравнивает идентификатор устройства по роли и MAC-адресу.
/// Если MAC известен у обоих — сравниваем по MAC (самый надёжный).
/// Если MAC неизвестен — сравниваем по роли.
/// Если ничего нет — считаем что это то же устройство (reconnect на том же порту).
/// </summary>
private static bool IsSameDevice(string oldRole, string oldMac, string newRole, string newMac)
{
bool hasOldMac = !string.IsNullOrWhiteSpace(oldMac);
bool hasNewMac = !string.IsNullOrWhiteSpace(newMac);
// Если оба MAC-адреса известны — сравниваем по MAC
if (hasOldMac && hasNewMac)
{
return string.Equals(oldMac, newMac, StringComparison.OrdinalIgnoreCase);
}
bool hasOldRole = !string.IsNullOrWhiteSpace(oldRole);
bool hasNewRole = !string.IsNullOrWhiteSpace(newRole);
// Если оба роля известны — сравниваем по роли
if (hasOldRole && hasNewRole)
{
return string.Equals(oldRole, newRole, StringComparison.OrdinalIgnoreCase);
}
// Нет данных для сравнения — считаем что это то же устройство (reconnect)
return true;
}
}
}

View File

@ -35,43 +35,45 @@ namespace RR.Common.Controls
[Header("Световые эффекты")]
[Tooltip("Эффект по умолчанию при вызове SetLight без аргументов")]
[SerializeField] private LightMode defaultLightMode = LightMode.Rainbow;
[Tooltip("Время эффекта по умолчанию (секунды, 0–99)")]
[SerializeField] [Range(0, 99)] private int defaultDuration = 3;
[Tooltip("Количество циклов эффекта по умолчанию")]
[SerializeField] private float defaultCycles = 1f;
// ───────────────────────── Приватное состояние ─────────────────────────
private Coroutine _blinkingCoroutine;
private LightSequence _activeSequence;
private bool _blinkWasPaused;
private float _pausedBlinkInterval;
private const string EffectPrefix = "85";
private const string EffectPrefix = "86";
// ───────────────────────── Свойства ─────────────────────────
/// <summary>Время эффекта по умолчанию (используется в <see cref="LightSequence.Then(LightMode)"/>).</summary>
public int DefaultDuration => defaultDuration;
/// <summary>Количество циклов по умолчанию (используется в <see cref="LightSequence.Then(LightMode)"/>).</summary>
public float DefaultCycles => defaultCycles;
// ===========================start: PUBLIC API===========================
/// <summary>
/// Включает эффект по умолчанию и возвращает <see cref="LightSequence"/> для построения цепочки.
/// </summary>
public LightSequence SetLight() => SetLight(defaultLightMode, defaultDuration);
public LightSequence SetLight() => SetLight(defaultLightMode, defaultCycles);
/// <summary>
/// Включает эффект на время по умолчанию и возвращает <see cref="LightSequence"/> для построения цепочки.
/// Включает эффект с количеством циклов по умолчанию и возвращает <see cref="LightSequence"/> для построения цепочки.
/// </summary>
public LightSequence SetLight(LightMode mode) => SetLight(mode, defaultDuration);
public LightSequence SetLight(LightMode mode) => SetLight(mode, defaultCycles);
/// <summary>
/// Включает световой эффект на указанное время и возвращает <see cref="LightSequence"/> для построения цепочки.
/// Включает световой эффект на указанное количество циклов и возвращает <see cref="LightSequence"/> для построения цепочки.
/// <para>Если вызвать без <c>.Then()</c> / <c>.Loop()</c> — эффект запустится немедленно как одиночный.</para>
/// </summary>
/// <param name="mode">Эффект подсветки</param>
/// <param name="durationSeconds">Время эффекта (0–99 секунд, либо <c>-1</c> для команды без <c>YY</c>)</param>
public LightSequence SetLight(LightMode mode, int durationSeconds)
/// <param name="cycles">Количество циклов (дробное: 0.5 = половина цикла)</param>
public LightSequence SetLight(LightMode mode, float cycles)
{
StopSequence();
_activeSequence = new LightSequence(this, mode, durationSeconds);
_activeSequence = new LightSequence(this, mode, cycles);
_activeSequence.Play();
return _activeSequence;
}
@ -91,7 +93,7 @@ namespace RR.Common.Controls
public void StopAllEffects()
{
StopSequence();
ComSystemManager.Send(EffectPrefix);
ComSystemManager.Send(EffectPrefix + "00");
}
/// <summary>
@ -138,6 +140,51 @@ namespace RR.Common.Controls
/// <summary>Возвращает <c>true</c>, если последовательность эффектов сейчас активна.</summary>
public bool IsSequencePlaying => _activeSequence?.IsPlaying ?? false;
/// <summary>Возвращает <c>true</c>, если последовательность эффектов приостановлена.</summary>
public bool IsSequencePaused => _activeSequence?.IsPaused ?? false;
/// <summary>Возвращает <c>true</c>, если мигание было приостановлено.</summary>
public bool IsBlinkPaused => _blinkWasPaused;
/// <summary>
/// Приостанавливает все активные эффекты (подсветку и мигание).
/// Вызывается при отвале платы контроллера.
/// </summary>
public void PauseAll()
{
if (_blinkingCoroutine != null)
{
_blinkWasPaused = true;
_pausedBlinkInterval = blinkInterval;
StopCoroutine(_blinkingCoroutine);
_blinkingCoroutine = null;
}
else
{
_blinkWasPaused = false;
}
_activeSequence?.Pause();
}
/// <summary>
/// Возобновляет приостановленные эффекты (подсветку и мигание).
/// Вызывается при переподключении платы контроллера.
/// </summary>
public void ResumeAll()
{
if (_blinkWasPaused)
{
_blinkWasPaused = false;
_blinkingCoroutine = StartCoroutine(BlinkingCoroutine(_pausedBlinkInterval));
}
if (_activeSequence != null && _activeSequence.IsPaused)
{
_activeSequence.Resume();
}
}
// ===========================end: PUBLIC API===========================
/// <summary>

View File

@ -0,0 +1,51 @@
using System.Collections.Generic;
namespace RR.Common.Controls
{
/// <summary>
/// Таблица длительности одного цикла каждого светового эффекта (в миллисекундах).
/// Используется в <see cref="LightSequence"/> для пересчёта количества циклов в секунды.
/// </summary>
public static class LightCycleDurations
{
private static readonly Dictionary<LightMode, int> CycleMs = new()
{
{ LightMode.HorizontalWave, 1000 }, // 40
{ LightMode.VerticalWave, 1000 }, // 41
{ LightMode.DiagonalWave, 3000 }, // 42
{ LightMode.Rainbow, 3000 }, // 43
{ LightMode.WaveUp, 3000 }, // 44
{ LightMode.VerticalWaveUp, 1000 }, // 45
{ LightMode.FinalWave, 1000 }, // 46
{ LightMode.BlueWave, 1000 }, // 47
{ LightMode.DiagonalWave2, 5000 }, // 48
{ LightMode.FallingRows, 6000 }, // 49
{ LightMode.OrangeWave, 3000 }, // 50
{ LightMode.SeaWave, 3000 }, // 51
{ LightMode.DiagonalWave3, 4000 }, // 52
};
/// <summary>Длительность одного цикла в миллисекундах. Для <see cref="LightMode.Off"/> возвращает 0.</summary>
public static int GetCycleMs(LightMode mode)
{
return CycleMs.TryGetValue(mode, out int ms) ? ms : 0;
}
/// <summary>Длительность одного цикла в секундах (float).</summary>
public static float GetCycleSeconds(LightMode mode)
{
return GetCycleMs(mode) / 1000f;
}
/// <summary>
/// Пересчитывает количество циклов в целые секунды для COM-команды.
/// Дробные циклы поддерживаются (0.5 = половина цикла).
/// </summary>
public static int CyclesToCommandSeconds(LightMode mode, float cycles)
{
float totalSeconds = GetCycleSeconds(mode) * cycles;
return (int)(totalSeconds + 0.5f); // округление до ближайшего целого
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0f1a1aca47a0465aab347b449c9ca4b4
timeCreated: 1772541577

View File

@ -6,11 +6,11 @@ namespace RR.Common.Controls
{
/// <summary>
/// Строитель последовательности световых эффектов.
/// Создаётся через <see cref="LightController.SetLight(LightMode,int)"/>.
/// Создаётся через <see cref="LightController"/>.
/// <example><code>
/// SetLight(LightMode.VerticalWave, 3).Then(LightMode.DiagonalWave, 3).Loop();
/// SetLight(LightMode.VerticalWave, 3).Then(LightMode.Off);
/// SetLight(LightMode.Rainbow, 5);
/// SetLight(LightMode.VerticalWave, 3f).Then(LightMode.DiagonalWave, 1.5f).Loop();
/// SetLight(LightMode.VerticalWave, 3f).Then(LightMode.Off);
/// SetLight(LightMode.Rainbow, 2f);
/// </code></example>
/// </summary>
public class LightSequence
@ -18,28 +18,30 @@ namespace RR.Common.Controls
private readonly struct Step
{
public readonly LightMode Mode;
public readonly int DurationSeconds;
public Step(LightMode mode, int durationSeconds) { Mode = mode; DurationSeconds = durationSeconds; }
public readonly float Cycles;
public Step(LightMode mode, float cycles) { Mode = mode; Cycles = cycles; }
}
private readonly List<Step> _steps = new();
private readonly LightController _controller;
private bool _looping;
private Coroutine _coroutine;
private int _currentStepIndex;
private bool _paused;
internal LightSequence(LightController controller, LightMode firstMode, int firstDuration)
internal LightSequence(LightController controller, LightMode firstMode, float firstCycles)
{
_controller = controller;
_steps.Add(new Step(firstMode, firstDuration));
_steps.Add(new Step(firstMode, firstCycles));
}
/// <summary>Добавляет следующий эффект с временем по умолчанию.</summary>
public LightSequence Then(LightMode mode) => Then(mode, _controller.DefaultDuration);
/// <summary>Добавляет следующий эффект с количеством циклов по умолчанию.</summary>
public LightSequence Then(LightMode mode) => Then(mode, _controller.DefaultCycles);
/// <summary>Добавляет следующий эффект с указанным временем.</summary>
public LightSequence Then(LightMode mode, int durationSeconds)
/// <summary>Добавляет следующий эффект с указанным количеством циклов.</summary>
public LightSequence Then(LightMode mode, float cycles)
{
_steps.Add(new Step(mode, durationSeconds));
_steps.Add(new Step(mode, cycles));
return this;
}
@ -57,6 +59,8 @@ namespace RR.Common.Controls
public LightSequence Play()
{
Stop();
_currentStepIndex = 0;
_paused = false;
_coroutine = _controller.StartCoroutine(RunSequence());
return this;
}
@ -67,23 +71,56 @@ namespace RR.Common.Controls
if (_coroutine == null) return;
_controller.StopCoroutine(_coroutine);
_coroutine = null;
_paused = false;
}
/// <summary>
/// Приостанавливает последовательность с сохранением текущей позиции.
/// </summary>
public void Pause()
{
if (_coroutine == null) return;
_controller.StopCoroutine(_coroutine);
_coroutine = null;
_paused = true;
}
/// <summary>
/// Возобновляет приостановленную последовательность с сохранённой позиции.
/// </summary>
public void Resume()
{
if (!_paused) return;
_paused = false;
_coroutine = _controller.StartCoroutine(RunSequence());
}
/// <summary>Возвращает <c>true</c>, если последовательность сейчас выполняется.</summary>
public bool IsPlaying => _coroutine != null;
/// <summary>Возвращает <c>true</c>, если последовательность приостановлена.</summary>
public bool IsPaused => _paused;
// ReSharper disable once IteratorNeverReturns — цикл намеренно бесконечный при _looping=true
private IEnumerator RunSequence()
{
do
{
foreach (Step step in _steps)
for (int i = _currentStepIndex; i < _steps.Count; i++)
{
_controller.SendCommand(step.Mode, step.DurationSeconds);
_currentStepIndex = i;
Step step = _steps[i];
if (step.Mode != LightMode.Off && step.DurationSeconds > 0)
yield return new WaitForSecondsRealtime(step.DurationSeconds);
int commandSeconds = LightCycleDurations.CyclesToCommandSeconds(step.Mode, step.Cycles);
_controller.SendCommand(step.Mode, commandSeconds);
float waitSeconds = LightCycleDurations.GetCycleSeconds(step.Mode) * step.Cycles;
if (step.Mode != LightMode.Off && waitSeconds > 0f)
yield return new WaitForSecondsRealtime(waitSeconds);
}
_currentStepIndex = 0;
}
while (_looping);

View File

@ -163,8 +163,11 @@ namespace RR.Game2D.Core.Managers
if (Plate != null) Light = Plate.Light;
if (Light) Light.SetLight(LightMode.DiagonalWave)
.Then(LightMode.DiagonalWave2)
.Then(LightMode.DiagonalWave3)
.Then(LightMode.Rainbow)
.Then(LightMode.WaveUp)
.Then(LightMode.FallingRows)
.Then(LightMode.OrangeWave)
.Then(LightMode.SeaWave)
.Loop();
@ -400,7 +403,7 @@ public void TogglePause() => SetPause(!IsPaused);
// Отключаем отслеживание бездействия
if (Inactivity != null) Inactivity.DisableInactivityCheck();
Light?.SetLight(LightMode.BlueWave, 30);
Light?.SetLight(LightMode.BlueWave, 30f);
GameOverEvent?.Invoke(totalTime);
}
@ -413,7 +416,7 @@ public void TogglePause() => SetPause(!IsPaused);
// Отключаем отслеживание бездействия
if (Inactivity != null) Inactivity.DisableInactivityCheck();
Light?.SetLight(LightMode.FinalWave, 30);
Light?.SetLight(LightMode.FinalWave, 30f);
GameWonEvent?.Invoke(totalTime);
}

View File

@ -1,29 +1,47 @@
# Данро (Danro)
## Короткое описание
`Danro` - это 2D-платформер на Unity с аркадным режимом запуска. Проект совмещает классический side-scrolling gameplay с логикой кредитов, баланса, внешнего COM-контроллера и призовой системой для инсталляций.
Это 2D-платформер в духе классических Mario-игр: уровни с прыжками, платформами и врагами, простая управляемая механика и визуальная стилизация под ретро/пиксельарт. Проект содержит игровые сцены, контроллеры персонажей, UI-элементы и инструменты для разработки уровней.
## Актуальное состояние игры
## Ключевые особенности
Сейчас в проекте реализованы и подключены в `Build Settings`:
- Платформерная физика и управление персонажем (бег, прыжок, атака/взаимодействие).
- Несколько уровней с различными препятствиями и врагами.
- Система сообщений и модальных окон для диалогов и уведомлений.
- Визуальные и звуковые эффекты, простая система уровней и прогрессии.
- главное меню (`MainMenu`);
- отдельная UI-сцена игрового интерфейса (`Level UI`);
- три игровых уровня: `1-1`, `1-2`, `1-3`.
## Как играть
Игра уже включает:
- Движение: стрелки или A/D.
- Прыжок: пробел или клавиша Jump настроенная в проекте.
- Взаимодействие: E или кнопка, указанная в игре.
- прохождение уровней с автоматическим переходом между сценами;
- систему жизней, монет и подсчета времени сессии;
- чекпоинты и респавн без полного сброса прогресса по уровню;
- врагов, ловушки, движущиеся и исчезающие платформы, трамплины, снаряды и интерактивные объекты;
- меню паузы, окно настроек, модальное окно выбора уровней;
- экраны победы и поражения;
- продолжение после поражения при достаточном балансе;
- аркадную механику кредитов, импульсов оплаты и бесплатного режима;
- окно статистики и админ-статистики;
- генерацию и показ QR-кодов на экране победы для призовой системы;
- поддержку клавиатуры и внешнего COM-управления.
## Где искать билды
## Управление
- Локально: проверьте папку `Builds/` в корне проекта (если она есть). Там могут находиться готовые сборки по платформам (Windows, macOS, WebGL и т.д.).
- В репозитории: сборки и релизы также могут быть выгружены в раздел "Releases" на платформе хостинга (GitHub, Gitea).
- Если вы не находите готовые билды, свяжитесь с автором проекта или смотрите ветки/артефакты CI, где могли быть опубликованы сборки.
Базовое управление с клавиатуры:
- движение: `A` / `D` или стрелки;
- прыжок: `Space`;
- навигация по UI: через назначенные действия `Unity Input System`.
## Примечание
Если игра используется в аркадной стойке, ввод может приходить с внешней платы через COM-подсистему. В этом режиме часть действий, кредиты, подсветка и выдача призов зависят от конфигурации установки.
Этот README содержит только общие сведения о проекте и рекомендации, где искать сборки. Для технической документации, инструкции по сборке или разработке смотрите соответствующие файлы в репозитории или обратитесь за дополнительной информацией.
## Что важно для запуска и разработки
- Версия Unity проекта: `6000.3.8f1`.
- Основной стек: `Unity`, `C#`, `Input System`, `UGUI`, `Cinemachine`, `URP`, `VFX Graph`.
- Ключевая точка входа в runtime: `GameManager`.
## Документация
- `README.md` описывает проект с точки зрения текущих возможностей игры.
- `ARCHITECTURE.md` содержит краткий обзор структуры проекта.
- `TECHDOC.md` содержит подробную техническую документацию по архитектурным решениям, подсистемам и ограничениям.

520
TECHDOC.md Normal file
View File

@ -0,0 +1,520 @@
# TECHDOC
## Назначение документа
Этот файл описывает технические решения проекта `Danro` на уровне runtime-логики, архитектуры и эксплуатационных ограничений. В отличие от `README.md`, здесь собраны детали, важные для разработки, сопровождения и дальнейшего расширения проекта.
## 1. Общая техническая картина
Проект представляет собой 2D-платформер на Unity с двумя режимами эксплуатации:
- обычный игровой режим с клавиатурным управлением;
- аркадный режим с внешним COM-устройством, кредитами, балансом, световыми эффектами и опциональной призовой механикой.
Архитектурно игра построена вокруг одного постоянного `GameManager`, набора глобальных сервисов из prefab `Globals`, сценовой компонентной логики и отдельной подсистемы настроек.
## 2. Стек и зависимости
### Движок и язык
- `Unity 6000.3.8f1`
- `C#`
### Основные Unity-пакеты
По `Packages/manifest.json` проект использует:
- `com.unity.inputsystem`
- `com.unity.ugui`
- `com.unity.cinemachine`
- `com.unity.render-pipelines.universal`
- `com.unity.visualeffectgraph`
- `com.unity.feature.2d`
- `com.unity.test-framework`
Дополнительно подключен `NuGetForUnity`, а также `com.unity.ai.inference`, но ключевой gameplay-код завязан в первую очередь на стандартные Unity-подсистемы.
## 3. Сцены и структура запуска
### Активные сцены в Build Settings
Сейчас включены:
- `Assets/Scenes/MainMenu.unity`
- `Assets/Scenes/Level UI.unity`
- `Assets/Scenes/Levels/1-1.unity`
- `Assets/Scenes/Levels/1-2.unity`
- `Assets/Scenes/Levels/1-3.unity`
Это означает, что фактическая игровая цепочка на текущий момент состоит из трех игровых уровней. Старые сцены в `Assets/Scenes/Levels/Old` в билд не включены.
### Принцип загрузки
`GameManager.LoadScene()` использует такую схему:
- основная сцена загружается как active scene;
- при `useUI = true` отдельно догружается `Level UI` в additive-режиме;
- для сцен без игрового UI (например, меню) игрок не спавнится.
### Схема именования уровней
Переход между уровнями завязан на шаблон имени сцены `world-stage`, например:
- `1-1`
- `1-2`
- `1-3`
`GameManager.NextLevel()` инкрементирует `Stage`, а при переполнении переходит к следующему `World`. Проверка наличия следующей сцены выполняется через `SceneManager.sceneCountInBuildSettings`. Если следующей сцены нет, игра считается завершенной.
Это важное ограничение: любое изменение схемы названий уровней потребует правки логики переходов.
## 4. Точка входа и жизненный цикл
### `GameManager`
Главный runtime-координатор находится в `Assets/Scripts/Managers/GameManager.cs`.
Ключевые особенности:
- создается автоматически через `RuntimeInitializeOnLoadMethod`;
- живет между сценами через `DontDestroyOnLoad`;
- хранит глобальное состояние игровой сессии;
- поднимает базовые подсистемы при старте приложения.
### Что инициализирует `GameManager`
В `Awake()` он:
- создает и настраивает `CompositeLogger` с файловым логгером в `game.log`;
- создает `AudioManager`;
- загружает `Resources/Prefabs/Global/Globals`;
- получает ссылки на `SettingsManager`, `PaymentManager`, `BalanceManager`, `ComSystemManager`;
- создает `InactivityManager`;
- подцепляет `LightController` из COM-подсистемы;
- подписывается на события настроек и оплаты.
### Runtime-события `GameManager`
`GameManager` публикует верхнеуровневые события:
- `PauseStateChanged`
- `PlayerSpawned`
- `CoinsChanged`
- `MaxLivesChanged`
- `LivesChanged`
- `GameOverEvent`
- `GameWonEvent`
Эти события используются UI-слоем и другими системами для синхронизации без жесткой связности.
## 5. Глобальные сервисы
### `Globals` prefab
Проект зависит от prefab `Resources/Prefabs/Global/Globals`. Это контейнер долгоживущих компонентов, которые должны быть доступны независимо от текущей сцены.
Если структура этого prefab изменится, инициализация игры может сломаться, потому что `GameManager` ожидает получить из него конкретные компоненты.
### `AudioManager`
Отвечает за:
- запуск и паузу музыки;
- управление SFX;
- реакцию на глобальную паузу.
`GameManager.SetPause()` меняет `Time.timeScale` и вызывает `Audio.SmoothPauseMusic(pause)`.
### `InactivityManager`
Используется для аркадного режима:
- включается при старте игры;
- выключается при победе или поражении;
- предназначен для реакции на бездействие игрока и возврата в безопасное состояние.
### Логирование
Используется `CompositeLogger`, который пишет в `game.log` в корне проекта. Это отдельная эксплуатационная подсистема, полезная при отладке инсталляции и поведения железа.
## 6. Игровой цикл
### Старт новой игры
`GameManager.NewGame()` выполняет:
1. Проверку режима `free game`.
2. Проверку достаточности баланса для старта.
3. Расчет числа стартовых жизней.
4. Списание текущего баланса.
5. Сброс монет и статистики.
6. Включение игрового светового режима.
7. Включение контроля бездействия.
8. Загрузку уровня `1-1`.
### Расчет жизней
Текущая логика такая:
- первая оплата дает `StartingLives`;
- каждая дополнительная оплата добавляет `LivesIncrement`;
- итоговое число жизней рассчитывается от накопленного баланса и стоимости импульса оплаты.
Таким образом, жизни завязаны не только на gameplay, но и на экономику аркадной установки.
### Поражение
При поражении:
- вычисляется полное время сессии;
- отключается проверка бездействия;
- включается специальный световой сценарий;
- вызывается `GameOverEvent`.
Дальше UI показывает `LoseBannerController`, где игрок может:
- продолжить игру, если хватает баланса;
- дождаться завершения таймера и вернуться в меню.
### Победа
При победе:
- вычисляется время сессии;
- отключается контроль бездействия;
- запускается финальный световой режим;
- вызывается `GameWonEvent`.
Победный экран показывает итоговое время и, при наличии данных, QR-коды призовой системы.
## 7. Спавн, смерть и чекпоинты
### Первый спавн
После загрузки игровой сцены `GameManager` ищет `PlayerSpawn`:
- если точка найдена, берет ее позицию и направление;
- если нет, использует запасную позицию `(0, 1.3, 0)`.
Игрок создается из prefab `Resources/Prefabs/Player_New`, если ссылка не задана в инспекторе.
### Респавн
`GameManager.RespawnPlayer()`:
- уменьшает число жизней;
- при нуле жизней завершает сессию;
- ищет активный чекпоинт через `CheckpointSystem`;
- телепортирует игрока или создает его заново;
- сбрасывает скорость `Rigidbody2D`;
- восстанавливает здоровье через `Health.ResetHealth()`.
### Что не сбрасывается
В текущей реализации при респавне:
- уровень не перезагружается;
- монеты не обнуляются.
Это заметное отличие от старой модели `ResetLevel()`, которая помечена как устаревшая.
## 8. Игровые сущности и сцена
### Основные gameplay-компоненты
Ключевая сценовая логика лежит в `Assets/Scripts/Behaviour` и `Assets/Scripts/Entities`.
Реализованы:
- враги (`EnemyController`);
- игрок (`PlayerController`);
- здоровье (`Health`);
- завершение уровня (`LevelFinish`);
- система монет и статистики (`ScoreSystem`);
- сообщения и сценарные последовательности (`MessageController`, `Sequencer`);
- снаряды игрока и врагов;
- VFX-пулы и окружение.
### Интерактивные объекты
В `Assets/Scripts/Entities/Props` и подкаталогах находятся:
- монеты и другие коллектаблы;
- ловушки;
- рычаги;
- прозрачный передний план;
- платформы разных типов.
Поддерживаются:
- `MovingPlatform`
- `ShiftingPlatform`
- `TrajectoryPlatform`
- `TriggeringPlatform`
- `HidingTriggeringPlatform`
- `VanishingPlatform`
Это означает, что уровень собирается из компонентов с локальной логикой, а не из одного большого контроллера.
## 9. Система ввода
### Абстракция ввода
Ввод изолирован в `Assets/Scripts/Controls`.
Центральная идея:
- игровой код не должен знать, пришел сигнал с клавиатуры или с внешней платы;
- источник ввода скрыт за фасадом `In.cs`.
### Поддерживаемые источники
В проекте присутствуют:
- `KeyboardInput`
- `COMInput`
- адаптер `ComInputToInputSystemAdapter` для интеграции с `Unity Input System`
Это дает два параллельных слоя совместимости:
- gameplay-код может работать с абстракцией собственного ввода;
- UI и часть действий могут работать через стандартные `InputActionReference`.
### COM-подсистема
В `Assets/Scripts/Controls/Inputs/ComSystem` собраны:
- подключение COM-устройств;
- handshake и маршрутизация;
- работа с кредитами;
- управление подсветкой;
- поддержка призовой стойки.
Ключевые классы:
- `ComSystemManager`
- `ComHub`
- `ComDeviceClient`
- `LightController`
- `PrizeStandController`
- `PrizeSystem`
Это один из самых чувствительных участков проекта: он завязан на внешнее железо и требует особенно аккуратных изменений.
## 10. Платежная модель, кредиты и баланс
### `PaymentManager`
`PaymentManager` хранит и синхронизирует число кредитов.
Он:
- реагирует на события COM-подсистемы;
- умеет добавлять кредиты вручную для тестирования;
- поддерживает free play;
- уведомляет другие системы через `UnityEvent`.
Ключевые события:
- `OnCreditsChanged`
- `OnCreditAdded`
- `OnCreditRemoved`
### `BalanceManager`
`BalanceManager` является более высоким уровнем экономики.
Он:
- хранит числовой баланс;
- начисляет его за импульсы оплаты;
- списывает баланс за старт и продолжение;
- кеширует значения из настроек;
- синхронизирует количество кредитов с COM-системой.
Важный нюанс: в текущем `GameManager.NewGame()` при старте списывается весь накопленный баланс, а не только фиксированная стоимость старта. Это сознательно влияет на число выдаваемых жизней и на модель одной игровой сессии.
### Free Play
Если в настройках включен `payment.freeGame`:
- старт игры доступен без баланса;
- продолжение доступно без списаний;
- часть UI-ограничений отключается.
## 11. Настройки
### Новая система настроек
Подсистема находится в `Assets/Scripts/Settings`.
Она состоит из:
- `SettingsDatabase` - схема настроек;
- `SettingsManager` - загрузка, хранение и события;
- `SettingsApplier` - применение значений к runtime;
- `SettingsUIGenerator` и binder-классов - построение UI.
### Типы настроек
В коде поддерживаются:
- `Bool`
- `Int`
- `Float`
- `String`
- `Button`
- `OptionPicker`
Это позволяет хранить не только значения, но и описывать кнопочные действия и enum-подобные селекторы в одной общей схеме.
### Хранение и валидация
Система:
- использует схему из `SettingsDatabase`;
- валидирует значения через `Sanitize`;
- приводит значения к диапазонам;
- сохраняет настройки в JSON в `persistentDataPath`.
### Практическое применение
По текущему коду настройки влияют как минимум на:
- стартовые жизни;
- дополнительные жизни;
- free play;
- стоимость импульса оплаты;
- стоимость старта;
- стоимость продолжения;
- номер поддержки;
- описание суперприза;
- номер телефона для призовой системы.
Это уже не декоративный UI, а реально рабочая конфигурационная подсистема.
## 12. Пользовательский интерфейс
### Основные UI-экраны
В `Assets/Scripts/UI` реализованы:
- `MainMenu`
- `PauseMenu`
- `SettingsUI`
- `LevelSelectModal`
- `GameEndUIController`
- `WinBannerController`
- `LoseBannerController`
### Что делает UI
На текущий момент UI умеет:
- блокировать старт игры при недостаточном балансе;
- показывать платежные подсказки;
- открывать настройки;
- собирать список уровней из `Build Settings`;
- показывать время сессии на экранах победы/поражения;
- разрешать продолжение только при достаточном балансе;
- динамически создавать QR-коды на победном экране.
### Статистика
Есть два отдельных режима статистики:
- `StatsWindowUIController` - простой просмотр;
- `AdminStatsWindowUIController` - просмотр плюс сброс обнуляемых счетчиков.
Статистика хранится через `PlayerPrefs`.
## 13. Призовая система и QR-коды
### `PrizeSystemManager`
Эта подсистема:
- читает параметры из настроек;
- готовит номер телефона;
- генерирует QR-коды заранее;
- может инициировать выдачу приза через hopper.
### Отображение QR
Актуальная логика показа QR-кодов перенесена в `WinBannerController`:
- при победе он получает `Texture2D[]` из `PrizeSystemManager`;
- создает `Image`-элементы динамически;
- очищает их при закрытии окна.
`QRCodeUIController` помечен как устаревший и использовать его больше не нужно.
## 14. Система ресурсов и зависимости от `Resources`
Проект по-прежнему опирается на `Resources.Load` для критичных runtime-объектов:
- `Prefabs/Global/Globals`
- `Prefabs/Player_New`
- `Audio/Master`
Это удобно для ранней инициализации, но создает ограничения:
- переименование ассетов требует обновления строковых путей;
- ошибки проявляются только в runtime;
- часть связности остается строковой, а не ссылочной.
## 15. Ключевые технические риски
### 1. Жесткая зависимость от `Globals`
Если из prefab убрать или переименовать нужный компонент, `GameManager` потеряет доступ к базовым сервисам.
### 2. Завязка на соглашения по именам сцен
Переход между уровнями работает только для формата `world-stage`.
### 3. Связь с внешним железом
Любые изменения вокруг COM-контуров, кредита, подсветки и призовой стойки нужно тестировать отдельно от обычного desktop-режима.
### 4. Смешение аркадной экономики и gameplay
Количество жизней напрямую зависит от баланса и стоимости импульсов, поэтому изменения в экономике автоматически меняют сложность игры.
### 5. Частичное хранение данных в `PlayerPrefs`
Баланс, статистика и часть служебного состояния могут сохраняться между сессиями. Это нужно учитывать при тестах и при подготовке демо-сборок.
## 16. Практические правила для изменений
При изменениях в проекте стоит проверять:
1. Загружается ли `Globals` и поднимаются ли `SettingsManager`, `BalanceManager`, `PaymentManager`.
2. Не сломалась ли загрузка `MainMenu`, `Level UI` и уровней `1-1`..`1-3`.
3. Работает ли старт новой игры при обычном и `free play` режимах.
4. Корректно ли доступны респавн, чекпоинты и завершение уровня.
5. Не нарушился ли сценарий продолжения после поражения.
6. Не перестали ли отображаться QR-коды на победе.
7. Не нарушилась ли совместимость клавиатуры и COM-ввода.
## 17. Карта основных директорий
- `Assets/Scripts/Managers` - глобальные менеджеры и orchestration.
- `Assets/Scripts/Settings` - схема, хранение и UI настроек.
- `Assets/Scripts/Controls` - ввод и интеграция с COM-оборудованием.
- `Assets/Scripts/Behaviour` - игровая логика на сценах.
- `Assets/Scripts/Entities` - интерактивные сущности и платформы.
- `Assets/Scripts/UI` - меню, модальные окна, баннеры и служебные панели.
- `Assets/Scripts/Utilities` - логирование, утилиты и вспомогательный код.
## 18. Итог
Текущее техническое решение проекта можно описать так:
- gameplay сделан как набор независимых Unity-компонентов;
- orchestration централизован в `GameManager`;
- аркадные функции вынесены в отдельные менеджеры;
- конфигурация постепенно переводится в самостоятельную систему настроек;
- проект уже ориентирован не только на домашний запуск, но и на работу в аппаратной инсталляции.
Если `README.md` отвечает на вопрос "что умеет игра", то этот документ отвечает на вопрос "как именно это собрано и где это менять".