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
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:
parent
4374c6f63f
commit
79b66b2d17
599
ARCHITECTURE.md
599
ARCHITECTURE.md
@ -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
@ -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
@ -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)
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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); // округление до ближайшего целого
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f1a1aca47a0465aab347b449c9ca4b4
|
||||
timeCreated: 1772541577
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
52
README.md
52
README.md
@ -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
520
TECHDOC.md
Normal 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` отвечает на вопрос "что умеет игра", то этот документ отвечает на вопрос "как именно это собрано и где это менять".
|
||||
Loading…
x
Reference in New Issue
Block a user