Подробный список изменений: - Перенесены эффекты частиц из C#-кода в готовые префабы-ассеты (Vfx Prefabs) в папке Assets/Resources/Vfx/ с автоматической генерацией материалов в Editor. - Исправлены ошибки duration на играющих системах частиц и розовые текстуры в GameVfxService.cs. - Исправлена навигация и фокус кнопок на оверлеях ContinueGameOverlay и PrizeResultOverlay с автоматическим подсвечиванием кнопок лимонно-зеленым цветом при фокусе и проигрыванием звука. - Добавлена проверка настроек в GameplayHudController.cs для скрытия ContinueGameOverlay, если продолжение игры отключено. - Расширен сбор статистики: создана структура GameplaySessionRecord, добавлена история до 50 раундов в PrizeSessionStateService и JsonPrizeSessionStateRepository, выведен расчет средних показателей и список 5 последних раундов в сервисной статистике. - Повышена надежность COM-транспорта: добавлен sleep (80мс) в Dispose для graceful shutdown потоков, реализован цикл переподключения до 10 попыток при обрыве связи. - Интегрированы настройки громкости с микшером: изменен шаг изменения громкости на 5% (0.05) для удобства, добавлен статус-чек и логи в ApplyServiceVolumes, расширены юнит-тесты на GetFloat параметры. - Созданы новые архитектурные гайды по аппаратному COM-транспорту и призовой экономике автомата в папке GUIDES/architecture/. - Оптимизирован и обновлен RAG-индекс проекта.
Архитектура аппаратной интеграции и COM-взаимодействия
В этом руководстве описывается архитектура аппаратной интеграции в Danro Jump — принципы взаимодействия Unity-приложения с физической платой управления аркадного автомата через последовательный COM-порт.
1. Обзор аппаратной платформы
Аркадный автомат управляется специализированным контроллером (микроконтроллером), который опрашивает органы управления автомата и управляет периферийными устройствами:
- Органы ввода (Inputs): джойстик (влево, вправо), кнопка старта, монетоприемник (Coin Acceptor), датчики выдачи призов (оптопары хопперов и ячеек витрины).
- Органы вывода (Outputs): светодиодная подсветка автомата (LED-ленты), моторы выдачи призов (хоппер монет, диспенсер призов витрины).
Связь между игровым ПК и контроллером осуществляется по кабелю USB-to-UART (виртуальный COM-порт) на скорости 115200 бод.
2. Протокол обмена и команды
Обмен информацией ведется текстовыми строками, разделяемыми символами перевода строки \n или \r\n. Каждая команда или сообщение представляет собой текстовый пакет.
2.1. Инициализация и Авторизация (Handshake)
Для предотвращения использования неавторизованного оборудования и защиты прошивки контроллера реализована двухсторонняя криптографическая проверка на базе алгоритма AES-128:
- Запрос роли: При открытии порта ПК отправляет команду запроса роли
97(командаroleRequestCommand). - Ответ платы: Плата возвращает строку
ROLE:<role>, напримерROLE:mainдля главного игрового контроллера илиROLE:vitrinaдля контроллера призового стенда. - Защищенное рукопожатие (CHAL / RAES):
- Компьютер генерирует случайный 16-байтовый массив, шифрует его по алгоритму AES-128 с секретным ключом проекта и отправляет Hex-строку плате в виде команды
CHAL:<hex_block>. - Плата расшифровывает блок на своей стороне, прибавляет к нему контрольную соль, шифрует обратно и возвращает ответ
RAES:<hex_block>. - Компьютер расшифровывает ответ платы и сравнивает с исходными данными. В случае успеха плата переводится в состояние Operational (рабочее состояние).
- Если проверка провалена, подключение закрывается.
- Компьютер генерирует случайный 16-байтовый массив, шифрует его по алгоритму AES-128 с секретным ключом проекта и отправляет Hex-строку плате в виде команды
2.2. Потоки данных в рабочем режиме
- Запрос состояния ввода (Пинг/Keep-Alive): Компьютер с интервалом
8секунд шлет команду опроса ввода (обычно01). - События ввода (Input Messages): Плата отправляет строку состояния кнопок в формате
INPUT:<bitmask>, где каждый бит соответствует нажатой кнопке или датчику:- Бит 0: кнопка влево
- Бит 1: кнопка вправо
- Бит 2: кнопка старта/выбора
- Бит 3: сигнал монетоприемника (импульс зачисления кредита)
- Бит 4: оптический датчик выдачи приза хоппером
- Команды вывода (Output Commands):
- Освещение: команды управления цветом и анимацией подсветки (например, переключение на зеленый при старте).
- Выдача приза: команда запуска мотора хоппера
12или открытие конкретной ячейки призовой витрины.
3. Многопоточная архитектура в Unity
Последовательные порты в .NET/Unity работают медленно и синхронно. Блокировка главного потока Unity операциями ввода-вывода приведет к микрофризам игры (Stuttering), что недопустимо для динамичного платформера.
Для решения этой проблемы в ComSystem реализована полностью асинхронная многопоточная модель на базе UniTask и пула потоков (ThreadPool):
graph TD
subgraph Unity Main Thread
A[ComSystem Update] -->|Опрос очередей| B(coinQueue / inputQueue / mainThreadQueue)
B -->|Вызов событий| C[Игровая логика / UI]
end
subgraph ThreadPool
D[ReadLoopAsync] -->|Синхронное блокирующее чтение| E[serialPort.ReadLine]
E -->|Данные получены| F[ProcessLine]
F -->|Enqueue| B
G[KeepAliveLoopAsync] -->|Каждые 8 сек| H[Send '01' пинг]
I[ReconnectAsync] -->|При сбое связи| J[Переоткрытие порта / Повторный Handshake]
end
3.1. Основные циклы (Tasks)
-
ReadLoopAsync (Цикл чтения): Выполняется в фоновом потоке на
ThreadPool. Вызывает синхронный блокирующий методReadLine(). Чтобы поток не висел заблокированным вечно при отсутствии данных, таймаут чтения (ReadTimeout) в настройках последовательного порта выставлен на50миллисекунд. Каждые 50мс вызов прерывается поTimeoutException, поток просыпается, проверяет токен отмены (CancellationToken.IsCancellationRequested) и уходит на следующий цикл ожидания. -
KeepAliveLoopAsync (Цикл контроля связи): Каждые 8 секунд отправляет на плату запрос ввода. Если плата не присылает ответный
INPUTпакет в течение 3 попыток подряд (missedInputResponsesBeforeReconnect = 3), это классифицируется как потеря связи, и запускается процедура переподключения. -
ReconnectAsync (Цикл автореконнекта): При потере пингов или возникновении системной ошибки ввода-вывода (
IOExceptionвReadLoopпри отключении USB-кабеля):- Переводит устройство в статус
isOperational = false. - Сбрасывает имя роли и MAC-адрес.
- Закрывает порт.
- Запускает цикл из 10 попыток открыть порт заново с интервалом в 1 секунду. При каждой попытке проверяется отмена токена, чтобы не открыть порт в фоне во время выхода из игры.
- При успешном переоткрытии порта автоматически инициируется повторная авторизация (Handshake) и плата возвращается в строй без перезапуска игры.
- Переводит устройство в статус
3.2. Graceful Shutdown (Безопасный выход)
В .NET на Windows вызов SerialPort.Close() или .Dispose() во время активного блокирующего чтения в параллельном потоке гарантированно приводит к зависанию (Deadlock) драйвера порта и зависанию всего приложения Unity при выходе.
Для предотвращения этого в методе ComDeviceConnection.Dispose реализован двухэтапный выход:
- Вызывается
localCts.Cancel(), сигнализируя фоновым потокам о необходимости завершения. - Главный поток делает паузу
System.Threading.Thread.Sleep(80). Так как таймаут чтения порта равен 50мс, за 80мс фоновый поток чтения гарантированно успевает проснуться по таймауту, обнаружить отмену токена и безопасно завершить работу. - Только после этого вызывается
ClosePort(), закрывающий и освобождающий последовательный порт под блокировкойlock (ioLock).
4. Потокобезопасная маршрутизация
Поскольку фоновые циклы работают в потоках ThreadPool, они не могут напрямую вызывать методы Unity API (изменение UI, спавн объектов, проигрывание звуков), так как Unity API не является потокобезопасным.
Для передачи данных в главный поток используются потокобезопасные очереди ConcurrentQueue:
coinQueue— очередь зачислений монет.inputQueue— очередь нажатий клавиш управления.mainThreadQueue— очередь произвольных делегатовAction(для логов, статусов подключения и т.д.).
Метод Update() класса ComSystem выполняется в главном потоке Unity каждый кадр. Он опрашивает эти очереди и безопасно вызывает соответствующие Unity-события:
private void Update()
{
// Перенос общих действий в главный поток
while (mainThreadQueue.TryDequeue(out Action action))
{
action?.Invoke();
}
// Зачисление кредитов
while (coinQueue.TryDequeue(out _))
{
AddPaymentImpulse(1);
}
// Обработка нажатий управления
while (inputQueue.TryDequeue(out ComInputMessage message))
{
if (message.DeviceId == CurrentMainDeviceId)
{
mainInputReceived.Invoke(message.InputBits);
}
}
}
5. Рекомендации по разработке и отладке
- Эмуляция клавиатурой: Для тестирования без физической платы в Unity Editor или Development-сборках включена эмуляция: нажатие клавиши
F6генерирует импульс монеты. - Логирование: В инспекторе
ComSystemможно включать каналы отладки (logLifecycle,logCredits,logRawEvents), чтобы изолировать проблемы с подключением, зачислением денег или обработкой протокола. - Использование блокировок: Любой доступ к объекту
SerialPort(записьWriteLineили закрытиеClose) должен производиться строго внутри блокаlock (ioLock)для исключения одновременного обращения из разных потоков.