2026-01-09 08:45:22 +03:00

394 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// Основной класс-обертка для работы с COM-портом и периферийной платой.
/// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов.
/// </summary>
public class ComWrapper : MonoBehaviour
{
//Настраиваем из инспектора
[SerializeField] bool saveMoneyBetweenSessions;
[SerializeField] bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
public static UnityEvent<int,int> onCoinAdded;
public static UnityEvent<int,int> onCoinRemoved;
public static UnityEvent<string> onInputRecieved;
public static System.Action OnInitialize;
#region Приватные поля
private const string REQUEST_INPUT = "91";
private const string LIGHTSIGNAL_PREFIX = "80";
private static int handle1; //идентификатор хэндла платы 1
private static int handle2; //идентификатор хэндла платы 2
private static byte[] buf = new byte[1024]; //Байт-буфер куда будут приходить данные с порта
// StringBuilder для эффективного формирования строки INPUT без лишних аллокаций
private static System.Text.StringBuilder inputBuilder = new System.Text.StringBuilder(32);
// Кэш последних состояний INPUT для каждой платы (16 бит от каждой платы)
// ВАЖНО: 0 = кнопка нажата, 1 = кнопка отпущена, поэтому по умолчанию все единицы
// Каждая плата присылает 16 бит, но мы берём:
// - от платы 1: первые 8 бит (биты 0-7)
// - от платы 2: последние 8 бит (биты 8-15)
private static string lastInputBits1 = "1111111111111111"; // последние 16 бит от платы 1 (все отпущены)
private static string lastInputBits2 = "1111111111111111"; // последние 16 бит от платы 2 (все отпущены)
private static int CreditsPlayer1;
private static int CreditsPlayer2;
#endregion
//===========================start: PUBLIC API===========================//
/// <summary>
/// Инициализирует систему работы с COM-портом.
/// Открывает соединение, настраивает события и запускает периодические запросы состояния.
/// </summary>
/// <remarks>
/// Обязателен к вызову при старте приложения. Выполняет:
/// - Создание UnityEvent'ов для уведомлений
/// - Загрузку количества кредитов из PlayerPrefs
/// - Открытие COM-порта
/// - Запуск периодического опроса пинов (если включена проверка призов)
/// </remarks>
public void Initialize()
{
//Создаем ивенты
onCoinAdded = new UnityEvent<int,int>();
onCoinRemoved = new UnityEvent<int,int>();
onInputRecieved = new();
//Начальная настройка
if (!saveMoneyBetweenSessions)
{
PlayerPrefs.SetInt("Player1_Coins", 0);
PlayerPrefs.SetInt("Player2_Coins", 0);
}
CreditsPlayer1 = PlayerPrefs.GetInt("Player1_Coins", 0);
CreditsPlayer2 = PlayerPrefs.GetInt("Player2_Coins", 0);
initPort(); //вот это важный метод который собственно порт открывает.
//Каждые 5 сек запрашиваем инпуты у платы. Надо потому что при заходе в игру с пинов еще не пришла инфа
if (enablePrizeChecking) InvokeRepeating("requestInputs", 0, 5);
OnInitialize.Invoke();
}
/// <summary>
/// Добавляет один кредит в систему и сохраняет обновленное количество.
/// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта.
/// </summary>
/// <remarks>
/// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков.
/// Автоматически сохраняет состояние в PlayerPrefs.
/// </remarks>
public static void AddCredit(int coins = 1, int playerId = 1)
{
if (playerId == 1)
{
CreditsPlayer1 += coins;
PlayerPrefs.SetInt("Player1_Coins", CreditsPlayer1);
onCoinAdded?.Invoke(playerId, CreditsPlayer1); // передаём номер игрока!
}
else if (playerId == 2)
{
CreditsPlayer2 += coins;
PlayerPrefs.SetInt("Player2_Coins", CreditsPlayer2);
onCoinAdded?.Invoke(playerId, CreditsPlayer2); // передаём номер игрока!
}
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
}
/// <summary>
/// Отправляет произвольную строковую команду на периферийную плату через COM-порт.
/// </summary>
/// <param name="message">Команда для отправки</param>
/// <remarks>
/// Используется для отправки пользовательских команд на плату.
/// Команда должна быть в формате, ожидаемом прошивкой платы.
/// В случае ошибки отправки выводит предупреждение в DebugWindow.
/// </remarks>
public static void Send(string message)
{
try
{
int write1 = ComNativeWrapper.Serial_WriteStringUTF8(handle1, message, 1); //w-количество записаных символов в порт. можно обработать
int write2 = ComNativeWrapper.Serial_WriteStringUTF8(handle2, message, 1); //w-количество записаных символов в порт. можно обработать
//Debug.Log("Wrote: " + w);
}
catch { Debug.Log("Ошибка отправки строкового сигнала."); }
}
/// <summary>
/// Отправляет команду управления подсветкой на периферийную плату.
/// </summary>
/// <param name="lightMode">Режим подсветки из перечисления LightMode</param>
/// <remarks>
/// Формирует команду вида "80XXX" где XXX - код режима подсветки.
/// Для добавления новых режимов необходимо обновить вложенный метод getLightString + добавить режим в перечисление.
/// </remarks>
public static void Send(LightMode lightMode)
{
try
{
string signal = LIGHTSIGNAL_PREFIX + getLightString(lightMode);
Send(signal);
//ВОТЭТОТ МЕТОД ПОД СВОИ ПОДСВЕТКИ НАДО ПОДГОНЯТЬ
string getLightString(LightMode lightMode) =>
lightMode switch
{
LightMode.Idle => "106",
LightMode.Orange => "107",
LightMode.Blue => "108",
LightMode.Win => "109",
LightMode.Lose => "110",
LightMode.Prize => "111",
_ => "106"
};
}
catch { Debug.Log("Ошибка отправки светового сигнала."); }
}
/// <summary>
/// Выполняет списание платы за запуск игры.
/// Проверяет наличие доступных кредитов или режим бесплатной игры.
/// При успешном списании увеличивает счетчики запусков.
/// </summary>
/// <returns>
/// true - если списание прошло успешно (бесплатный режим или есть кредиты),
/// false - если кредитов недостаточно и платный режим
/// </returns>
/// <remarks>
/// В случае успеха:
/// - Увеличивает общие счетчики запусков (key_starts и key_startsNul)
/// - При платном режиме уменьшает количество кредитов на 1
/// - Вызывает событие onCoinRemoved при списании кредита
/// </remarks>
public static bool Pay(int coins = 1, int playerId = 1)
{
if (SettingsManager.instance.GetSettingValue("Бесплатная игра") == "Вкл") return true;
ref int credits = ref (playerId == 1 ? ref CreditsPlayer1 : ref CreditsPlayer2);
string key = playerId == 1 ? "Player1_Coins" : "Player2_Coins";
if (credits > 0)
{
credits -= coins;
PlayerPrefs.SetInt(key, credits);
onCoinRemoved?.Invoke(playerId, credits);
return true;
}
return false;
}
/// <summary>
/// Получаем баланс игрока по id
/// </summary>
///
public static int GetPlayerCredits(int playerId = 1)
{
return playerId == 1 ? CreditsPlayer1 : CreditsPlayer2;
}
//===========================end: PUBLIC API===========================//
private void Update() => pollPort();
private void OnDisable() => ClosePort();
private void OnApplicationQuit() => ClosePort();
#region Приватные методы
//Ну тут по дебаг логам все должно быть понятно
//Если интересно, то h - это хэндл. Что то на плюсовом, типа ID соединения или чтото. Если 0 то нету соед. если меньше то ошибка, больше 0 - норм.
private void initPort()
{
Debug.Log("Старт программы");
ComNativeWrapper.Serial_CloseAll();
var ports = ComNativeWrapper.ListPorts();
Debug.Log("Ports: " + string.Join(", ", ports));
if (ports.Length == 0) { Debug.Log("Нет портов, завершаю"); return; }
// НОВАЯ ПРОВЕРКА: нужно минимум 2 порта
if (ports.Length < 2)
{
Debug.Log($"<color=red>ОШИБКА: Найден только {ports.Length} порт, а нужно минимум 2 для работы с двумя платами!</color>");
return;
}
string port1 = ports[^1];
string port2 = ports[^2];
Debug.Log("Открываю первый с конца порт: " + port1);
Debug.Log("Открываю второй с конца порт: " + port2);
//вот эти настройки лучше не ковырять, но если очень хочется можно поменять readTimeoutMs и writeTimeoutMs
//Если read поставить слишком маленьким оно проебует кредиты когда они идут большой пачкой например при оплате через вендисту.
//Там в порт спамит "COIN" в больших количествах и процентов 10 из этих строк не успевают прочитаться при таймауте 1мс, соотв. монеты проебуются.
//Слишком большой таймаут будет тормозить игру потому что оно все выполняется синхронно и каждый кадр забирает столько мс сколько в переменной указано.
handle1 = ComNativeWrapper.Serial_Open(port1, 115200, 8, 0, 1, 5, 10);
handle2 = ComNativeWrapper.Serial_Open(port2, 115200, 8, 0, 1, 5, 10);
Debug.Log($"handle: {handle1}");
if (handle1 <= 0)
{
Debug.Log("Open failed: " + ComNativeWrapper.GetLastError());
return;
}
Debug.Log($"handle: {handle2}");
if (handle2 <= 0)
{
Debug.Log("Open failed: " + ComNativeWrapper.GetLastError());
return;
}
}
private static void ClosePort()
{
if (handle1 > 0)
{
ComNativeWrapper.Serial_Close(handle1);
handle1 = 0;
}
if (handle2 > 0)
{
ComNativeWrapper.Serial_Close(handle2);
handle2 = 0;
}
}
//Чтение с ДВУХ плат и объединение битов INPUT с использованием кэша
//COIN обрабатывается сразу, чтобы не потерять монеты с обеих плат
private static string ReadBufferLine()
{
string result1 = "";
string result2 = "";
bool hasNewInput1 = false;
bool hasNewInput2 = false;
// Читаем с первой платы (биты 0-7)
int read1 = ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle1, buf, buf.Length);
if (read1 > 0)
{
result1 = Encoding.UTF8.GetString(buf, 0, read1);
// Обрабатываем COIN сразу, чтобы не потерять
if (result1 == "COIN")
{
AddCredit(1, 1); // 1 монета, игрок 1
Debug.Log("COM1: COIN (Игрок 1)");
}
// Если пришёл INPUT - обновляем кэш
else if (result1.StartsWith("INPUT"))
{
lastInputBits1 = result1.Substring(5); // сохраняем биты 0-7
hasNewInput1 = true;
}
}
// Читаем со второй платы (биты 8-15)
int read2 = ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle2, buf, buf.Length);
if (read2 > 0)
{
result2 = Encoding.UTF8.GetString(buf, 0, read2);
// Обрабатываем COIN сразу, чтобы не потерять
if (result2 == "COIN")
{
AddCredit(1, 2); // 1 монета, игрок 2
Debug.Log("COM2: COIN (Игрок 2)");
}
// Если пришёл INPUT - обновляем кэш
else if (result2.StartsWith("INPUT"))
{
lastInputBits2 = result2.Substring(5); // сохраняем биты 8-15
hasNewInput2 = true;
}
}
// Если хотя бы с одной платы пришёл INPUT - объединяем с кэшем
if (hasNewInput1 || hasNewInput2)
{
// Берём нужные биты из каждой платы:
// - от платы 1: первые 8 символов (биты 0-7)
// - от платы 2: последние 8 символов (биты 8-15)
string bits0_7 = lastInputBits1.Substring(0, 8); // первые 8 бит от платы 1
string bits8_15 = lastInputBits2.Substring(8, 8); // последние 8 бит от платы 2
// Формируем строку эффективно через StringBuilder
inputBuilder.Clear();
inputBuilder.Append("INPUT");
inputBuilder.Append(bits0_7);
inputBuilder.Append(bits8_15);
string combined = inputBuilder.ToString();
if (hasNewInput1 && hasNewInput2)
{
Debug.Log($"<color=cyan>Обе платы ответили: [{bits0_7}]+[{bits8_15}]=[{combined}]</color>");
}
else if (hasNewInput1)
{
Debug.Log($"<color=yellow>Плата1 новая, плата2 кэш: [{bits0_7}]+[{bits8_15}]=[{combined}]</color>");
}
else
{
Debug.Log($"<color=yellow>Плата1 кэш, плата2 новая: [{bits0_7}]+[{bits8_15}]=[{combined}]</color>");
}
return combined;
}
return "";
}
//просим прислать состояние цифровых входов. Приходит что то вроде "INPUT111111101"
private void requestInputs() => Send(REQUEST_INPUT);
private void pollPort()
{
//Тут все просто, читаем что нам приходит с порта и сравниваем.
string comInput = ReadBufferLine();
if (comInput != "")
{
Debug.Log("COM: " + comInput);
// COIN уже обработан в ReadBufferLine(), здесь только INPUT
if (comInput.StartsWith("INPUT")) onInputRecieved?.Invoke(comInput.Substring(5)); //инвокаю сразу без префикса "INPUT"
}
}
#endregion
}
public enum LightMode
{
Idle,
Orange,
Blue,
Win,
Lose,
Prize,
}