Commit as of 2026-03-05 11:30:58 by

This commit is contained in:
Данил Ярош 2026-03-05 11:30:59 +03:00
parent e57c0d4b1d
commit 833ec4ca6f
48 changed files with 0 additions and 2685 deletions

View File

@ -1,345 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
namespace Managers
{
public class AudioManager : MonoBehaviour
{
// ==== Singleton ====
public static AudioManager Instance { get; private set; }
[Header("Music")]
[SerializeField] private AudioSource musicSource; // child: MusicSource
[SerializeField] private AudioSource tempMusicSource; // child: TempMusicSource
[Tooltip("Группа микшера для музыки (необязательно)")]
[SerializeField] private AudioMixerGroup musicMixerGroup;
[Tooltip("Целевой громкость музыки (после фейдов)")]
[Range(0f, 1.5f)] public float musicTargetVolume = 1f;
[Tooltip("Длительность фейда по умолчанию (сек)")]
[SerializeField] private float defaultFadeDuration = 0.8f;
[Header("SFX Pool")]
[SerializeField] private int sfxPoolSize = 8;
[SerializeField] private Transform sfxPoolParent; // child: SFX Pool
[Tooltip("Группа микшера для SFX (необязательно)")]
[SerializeField] private AudioMixerGroup sfxMixerGroup;
[Range(0f, 1.5f)] public float sfxDefaultVolume = 1f;
private readonly List<AudioSource> sfxPool = new List<AudioSource>(64);
private int sfxRoundRobinIndex = 0;
// Внутреннее
private Coroutine musicFadeCoroutine;
private Coroutine crossfadeCoroutine;
private float storedMusicVolumeBeforePause = 1f;
private bool isPausedFaded = false;
// ==== Unity Lifetime ====
public void Initialize()
{
// Singleton init
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
EnsureMusicSources();
EnsureSfxPool();
}
//private void Awake()
//{
//}
// Создаёт/находит MusicSource и TempMusicSource
private void EnsureMusicSources()
{
if (musicSource == null)
{
var t = transform.Find("MusicSource");
if (t != null) musicSource = t.GetComponent<AudioSource>();
}
if (tempMusicSource == null)
{
var t = transform.Find("TempMusicSource");
if (t != null) tempMusicSource = t.GetComponent<AudioSource>();
}
if (musicSource == null) musicSource = CreateChildAudioSource("MusicSource", loop: true, output: musicMixerGroup);
if (tempMusicSource == null) tempMusicSource = CreateChildAudioSource("TempMusicSource", loop: true, output: musicMixerGroup);
PrepareMusicSource(musicSource);
PrepareMusicSource(tempMusicSource);
tempMusicSource.volume = 0f; // запасной источник молчит
musicSource.volume = Mathf.Clamp01(musicTargetVolume);
}
private void PrepareMusicSource(AudioSource src)
{
src.playOnAwake = false;
src.loop = true;
src.spatialBlend = 0f; // 2D музыка
if (musicMixerGroup != null) src.outputAudioMixerGroup = musicMixerGroup;
}
private AudioSource CreateChildAudioSource(string name, bool loop, AudioMixerGroup output)
{
var go = new GameObject(name);
go.transform.SetParent(transform, false);
var src = go.AddComponent<AudioSource>();
src.loop = loop;
src.playOnAwake = false;
src.spatialBlend = 0f;
if (output != null) src.outputAudioMixerGroup = output;
return src;
}
// Создаёт/находит контейнер и пул SFX источников
private void EnsureSfxPool()
{
if (sfxPoolParent == null)
{
var t = transform.Find("SFX Pool");
if (t != null) sfxPoolParent = t;
}
if (sfxPoolParent == null)
{
var go = new GameObject("SFX Pool");
sfxPoolParent = go.transform;
sfxPoolParent.SetParent(transform, false);
}
// Если уже есть дети-аудиоисточники — используем их как пул.
sfxPool.Clear();
var existing = sfxPoolParent.GetComponentsInChildren<AudioSource>(includeInactive: true);
foreach (var src in existing)
{
SetupSfxSource(src);
sfxPool.Add(src);
}
// Добираем до нужного размера
for (int i = sfxPool.Count; i < sfxPoolSize; i++)
{
var src = CreateSfxSource($"SFX_{i:00}");
sfxPool.Add(src);
}
}
private AudioSource CreateSfxSource(string name)
{
var go = new GameObject(name);
go.transform.SetParent(sfxPoolParent, false);
var src = go.AddComponent<AudioSource>();
SetupSfxSource(src);
return src;
}
private void SetupSfxSource(AudioSource src)
{
src.playOnAwake = false;
src.loop = false;
src.spatialBlend = 0f; // по умолчанию 2D
src.volume = sfxDefaultVolume;
if (sfxMixerGroup != null) src.outputAudioMixerGroup = sfxMixerGroup;
}
// ==== Public API ====
/// <summary>
/// Мгновенно запустить музыку (без фейда).
/// </summary>
public void PlayMusicImmediate(AudioClip clip, bool loop = true, float targetVolume = -1f)
{
if (clip == null) return;
StopCrossfadeAndFade();
musicSource.clip = clip;
musicSource.loop = loop;
musicSource.volume = (targetVolume >= 0f ? targetVolume : musicTargetVolume);
musicSource.Play();
tempMusicSource.Stop();
tempMusicSource.clip = null;
tempMusicSource.volume = 0f;
}
/// <summary>
/// 2) Плавно сменить музыку (кроссфейд A->B).
/// </summary>
public void CrossfadeMusic(AudioClip newClip, float duration = -1f, bool loop = true, float newTargetVolume = -1f)
{
if (newClip == null) return;
float d = (duration > 0f ? duration : defaultFadeDuration);
float toVol = (newTargetVolume >= 0f ? newTargetVolume : musicTargetVolume);
StopCrossfadeAndFade();
crossfadeCoroutine = StartCoroutine(CrossfadeRoutine(newClip, d, loop, toVol));
}
/// <summary>
/// 1) Плавно приостановить музыку (для паузы).
/// pause=true: плавный фейд к 0 и Pause(). pause=false: UnPause() и плавный фейд к musicTargetVolume.
/// </summary>
public void SmoothPauseMusic(bool pause, float duration = -1f)
{
float d = (duration > 0f ? duration : defaultFadeDuration);
if (pause)
{
if (isPausedFaded) return;
storedMusicVolumeBeforePause = musicSource.volume;
StopCrossfadeAndFade();
musicFadeCoroutine = StartCoroutine(FadeOutThenPause(musicSource, d));
isPausedFaded = true;
}
else
{
if (!isPausedFaded) return;
StopCrossfadeAndFade();
musicSource.UnPause();
musicFadeCoroutine = StartCoroutine(Fade(musicSource, from: 0f, to: Mathf.Clamp01(musicTargetVolume), d));
isPausedFaded = false;
}
}
/// <summary>
/// 3) Воспроизвести SFX из пула (2D).
/// </summary>
public void PlaySfx(AudioClip clip, float volume = 1f, float pitch = 1f)
{
if (clip == null) return;
var src = GetFreeSfxSource();
ConfigureAndPlaySfx(src, clip, volume, pitch, spatialBlend: 0f, position: null);
}
/// <summary>
/// 3) Воспроизвести SFX из пула в мире (3D).
/// </summary>
public void PlaySfxAt(AudioClip clip, Vector3 position, float volume = 1f, float pitch = 1f, float spatialBlend = 1f)
{
if (clip == null) return;
var src = GetFreeSfxSource();
ConfigureAndPlaySfx(src, clip, volume, pitch, Mathf.Clamp01(spatialBlend), position);
}
// ==== Internal helpers ====
private void StopCrossfadeAndFade()
{
if (crossfadeCoroutine != null) { StopCoroutine(crossfadeCoroutine); crossfadeCoroutine = null; }
if (musicFadeCoroutine != null) { StopCoroutine(musicFadeCoroutine); musicFadeCoroutine = null; }
}
private IEnumerator CrossfadeRoutine(AudioClip newClip, float duration, bool loop, float targetVol)
{
// Готовим второй источник
tempMusicSource.clip = newClip;
tempMusicSource.loop = loop;
tempMusicSource.volume = 0f;
tempMusicSource.Play();
float startVolA = musicSource.volume;
float t = 0f;
while (t < duration)
{
t += Time.unscaledDeltaTime; // не зависим от timescale (музыка при паузе всё равно может фейдиться)
float k = Mathf.Clamp01(t / duration);
// кроссфейд
musicSource.volume = Mathf.Lerp(startVolA, 0f, k);
tempMusicSource.volume = Mathf.Lerp(0f, targetVol, k);
yield return null;
}
// Завершение: стопим A, меняем роли (чтобы основной всегда указывал на текущую музыку)
musicSource.Stop();
musicSource.clip = null;
musicSource.volume = targetVol;
// Меняем ссылки местами
var swap = musicSource;
musicSource = tempMusicSource;
tempMusicSource = swap;
// Убедимся, что запасной источник молчит
tempMusicSource.volume = 0f;
tempMusicSource.Stop();
tempMusicSource.clip = null;
crossfadeCoroutine = null;
}
private IEnumerator FadeOutThenPause(AudioSource src, float duration)
{
float startVol = src.volume;
yield return Fade(src, startVol, 0f, duration);
src.Pause();
musicFadeCoroutine = null;
}
private IEnumerator Fade(AudioSource src, float from, float to, float duration)
{
float t = 0f;
src.volume = from;
if (!src.isPlaying) src.Play();
while (t < duration)
{
t += Time.unscaledDeltaTime;
float k = Mathf.Clamp01(t / duration);
src.volume = Mathf.Lerp(from, to, k);
yield return null;
}
src.volume = to;
}
private AudioSource GetFreeSfxSource()
{
// Сначала ищем свободный
for (int i = 0; i < sfxPool.Count; i++)
{
int idx = (sfxRoundRobinIndex + i) % sfxPool.Count;
if (!sfxPool[idx].isPlaying)
{
sfxRoundRobinIndex = (idx + 1) % sfxPool.Count;
return sfxPool[idx];
}
}
// Если все заняты — берём по round-robin (steal)
var src = sfxPool[sfxRoundRobinIndex];
sfxRoundRobinIndex = (sfxRoundRobinIndex + 1) % sfxPool.Count;
return src;
}
private void ConfigureAndPlaySfx(AudioSource src, AudioClip clip, float volume, float pitch, float spatialBlend, Vector3? position)
{
src.clip = clip;
src.volume = Mathf.Clamp(volume * sfxDefaultVolume, 0f, 1.5f);
src.pitch = Mathf.Clamp(pitch, -3f, 3f);
src.spatialBlend = spatialBlend;
if (position.HasValue)
{
src.transform.position = position.Value;
src.rolloffMode = AudioRolloffMode.Linear;
src.minDistance = 1f;
src.maxDistance = 20f;
}
src.Play();
}
}
}

View File

@ -1,306 +0,0 @@
using System;
using System.IO;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace RR.Game2D.Core
{
[DefaultExecutionOrder(-1000)]
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[Header("Пауза")]
public bool IsPaused { get; private set; }
public event Action<bool> PauseStateChanged;
[Header("Информация для игры")]
public int World { get; private set; } = 1;
public int Stage { get; private set; } = 1;
public int StagesByWorld { get; private set; } = 5;
public int Lives { get; private set; } = 3;
public int Coins { get; private set; } = 0;
[Header("Игрок")]
[SerializeField] private GameObject playerPrefab;
[SerializeField] private string playerPrefabResourcePath = "Prefabs/Player";
public GameObject PlayerObj { get; private set; }
private bool _shouldSpawnPlayer;
private void OnEnable() => SceneManager.sceneLoaded += OnSceneLoaded;
private void OnDisable() => SceneManager.sceneLoaded -= OnSceneLoaded;
public event Action<GameObject> PlayerSpawned;
public event Action<int> CoinsChanged;
//---------------------------
// Singleton
//---------------------------
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
if (Instance == null)
{
var go = new GameObject(nameof(GameManager));
Instance = go.AddComponent<GameManager>();
DontDestroyOnLoad(go);
}
}
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
if (SceneManager.GetActiveScene().name != "MainMenu") _shouldSpawnPlayer = true;
}
private void OnDestroy()
{
if (Instance == this) Instance = null;
}
//---------------------------
//
//---------------------------
//private void Start()
//{
// //Application.targetFrameRate = 60;
// //LoadScene("MainMenu", false);
// //NewGame();
// if (SceneManager.GetActiveScene().name != "MainMenu")
// {
// TrySpawnPlayerForActiveScene();
// }
//}
/// <summary>
/// Поиск префаба игрока.
/// Если не найдет префаб из инспектора, ищет в "Resources"
/// </summary>
/// <returns> Префаб игрока </returns>
private GameObject ResolvePlayerPrefab()
{
if (playerPrefab != null) return playerPrefab;
var loaded = Resources.Load<GameObject>(playerPrefabResourcePath);
if (loaded == null)
{
Debug.LogError(
$"GameManager: playerPrefab is null. Resources.Load('{playerPrefabResourcePath}') вернул null. " +
$"Ожидается файл: Assets/Resources/{playerPrefabResourcePath}.prefab (точное совпадение регистра).");
}
return loaded;
}
/// <summary>
/// Начало новой игры.
/// Загружает первый уровень.
/// Устанавливает жизни на 3, монеты на 0
/// </summary>
public void NewGame()
{
Lives = 3;
Coins = 0;
CoinsChanged?.Invoke(Coins);
LoadLevel(1, 1);
}
// Можно заменить на другое поведение
/// <summary>
/// Окончание игры.
/// Загружает сцену главного меню
/// </summary>
public void GameOver() => LoadScene("MainMenu", false);
// --
/// <summary>
/// Загрузка уровня по данным.
/// Загружает уровень по номерам мира и уровня
/// </summary>
/// <param name="world">Номер мира</param>
/// <param name="stage">Номер уровня</param>
public void LoadLevel(int world, int stage)
{
this.World = world;
this.Stage = stage;
string sceneName = $"{world}-{stage}";
if (SceneInBuildSettingsExists(sceneName))
{
LoadScene(sceneName);
}
else
{
//NewGame();
// Можно заменить на другое поведение
print("WIN");
LoadScene("MainMenu", false);
// --
}
}
/// <summary>
/// Загрузка сцены по названию.
/// Вызывает одноименный метод в SceneManager.
/// Может загружать сцены с UI и без.
/// В сценах без UI игрок не спавнится
/// </summary>
/// <param name="sceneName">Строковое название сцены</param>
/// <param name="useUI">Использовать ли UI</param>
public void LoadScene(string sceneName, bool useUI = true)
{
_shouldSpawnPlayer = true;
SceneManager.LoadScene(sceneName);
if (useUI)
{
SceneManager.LoadScene("Level UI", LoadSceneMode.Additive);
}
else _shouldSpawnPlayer = false;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
string currentSceneName = SceneManager.GetActiveScene().name;
TrySpawnPlayerForActiveScene();
if (currentSceneName != $"{World}-{Stage}" && currentSceneName.Contains("-"))
{
string[] levelInfo = currentSceneName.Split("-");
World = Convert.ToInt32(levelInfo[0]);
World = Convert.ToInt32(levelInfo[1]);
}
}
/// <summary>
/// Метод для спавна игрока.
/// Ищет первый спавнпоинт, спавнит игрока на его позиции.
/// Если на карте есть объект игрока, удаляет его и спавнит свой
/// </summary>
private void TrySpawnPlayerForActiveScene()
{
if (!_shouldSpawnPlayer) return;
PlayerSpawn spawn = FindFirstObjectByType<PlayerSpawn>();
Vector3 pos = spawn ? spawn.transform.position : new Vector3(0, 1.3f, 0);
Quaternion rot = spawn ? spawn.transform.rotation : Quaternion.identity;
PlayerObj = GameObject.FindGameObjectWithTag("Player");
if (PlayerObj != null) Destroy(PlayerObj);
var prefab = ResolvePlayerPrefab();
if (prefab == null) return;
PlayerObj = Instantiate(prefab, pos, rot);
SceneManager.MoveGameObjectToScene(PlayerObj, SceneManager.GetActiveScene());
PlayerSpawned?.Invoke(PlayerObj);
}
/// <summary>
/// Метод переход на следующий уровень.
/// Может загружать уровни и миры в прямой зависимости от
/// Количества уровней на мир(StagesByWorld)
/// </summary>
public void NextLevel()
{
if (Stage >= StagesByWorld)
{
World++;
Stage = 1;
}
else Stage++;
LoadLevel(World, Stage);
}
/// <summary>
/// Метод сброса уровня.
/// При сбросе также сбрасывает монеты и отнимает жизни
/// </summary>
public void ResetLevel()
{
Lives--;
Coins = 0;
CoinsChanged?.Invoke(Coins);
//print($"Lives: {lives}");
if (Lives > 0) LoadLevel(World, Stage);
else GameOver();
}
/// <summary>
/// Метод добавления монеты
/// </summary>
public void AddCoin()
{
Coins++;
// Можно заменить на другое поведение
if (Coins >= 100)
{
Coins = 0;
AddLife();
}
// --
CoinsChanged?.Invoke(Coins);
}
/// <summary>
/// Метод паузы игры.
/// Ставит на паузу все SFX и саму игру
/// </summary>
/// <param name="pause">Ставить ли на паузу</param>
public void SetPause(bool pause)
{
if (IsPaused == pause) return;
IsPaused = pause;
Time.timeScale = pause ? 0f : 1f;
AudioListener.pause = pause;
PauseStateChanged?.Invoke(IsPaused);
}
// Можно заменить на другое поведение
/// <summary>
/// Метод переключателя паузы
/// </summary>
public void TogglePause() => SetPause(!IsPaused);
// --
// Можно заменить на другое поведение
/// <summary>
/// Метод для добавления жизни.
/// </summary>
public void AddLife() => Lives++;
// --
/// <summary>
/// Метод проверки сцены на существование в настройках билда
/// </summary>
/// <param name="sceneName">Строковое название сцены</param>
/// <returns></returns>
private static bool SceneInBuildSettingsExists(string sceneName)
{
int count = SceneManager.sceneCountInBuildSettings;
for (int i = 0; i < count; i++)
{
string path = SceneUtility.GetScenePathByBuildIndex(i);
if (Path.GetFileNameWithoutExtension(path) == sceneName)
return true;
}
return false;
}
}
}

View File

@ -1,31 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36603.0 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ComNative", "ComNative\ComNative.vcxproj", "{BCC43995-EF98-4066-BB8D-58819B0AFB01}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BCC43995-EF98-4066-BB8D-58819B0AFB01}.Debug|x64.ActiveCfg = Release|x64
{BCC43995-EF98-4066-BB8D-58819B0AFB01}.Debug|x64.Build.0 = Release|x64
{BCC43995-EF98-4066-BB8D-58819B0AFB01}.Debug|x86.ActiveCfg = Debug|Win32
{BCC43995-EF98-4066-BB8D-58819B0AFB01}.Debug|x86.Build.0 = Debug|Win32
{BCC43995-EF98-4066-BB8D-58819B0AFB01}.Release|x64.ActiveCfg = Release|x64
{BCC43995-EF98-4066-BB8D-58819B0AFB01}.Release|x64.Build.0 = Release|x64
{BCC43995-EF98-4066-BB8D-58819B0AFB01}.Release|x86.ActiveCfg = Release|Win32
{BCC43995-EF98-4066-BB8D-58819B0AFB01}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {022B4E09-68D9-43CB-B2A3-E6ABE4525C96}
EndGlobalSection
EndGlobal

View File

@ -1,154 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{bcc43995-ef98-4066-bb8d-58819b0afb01}</ProjectGuid>
<RootNamespace>ComNative</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;COMNATIVE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;COMNATIVE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;COMNATIVE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;COMNATIVE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="framework.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="SerialNative.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="SerialNative.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Исходные файлы">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Файлы заголовков">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Файлы ресурсов">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="framework.h">
<Filter>Файлы заголовков</Filter>
</ClInclude>
<ClInclude Include="pch.h">
<Filter>Файлы заголовков</Filter>
</ClInclude>
<ClInclude Include="SerialNative.h">
<Filter>Файлы заголовков</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Исходные файлы</Filter>
</ClCompile>
<ClCompile Include="SerialNative.cpp">
<Filter>Исходные файлы</Filter>
</ClCompile>
</ItemGroup>
</Project>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>

View File

@ -1,261 +0,0 @@
// SerialNative.cpp
#include "SerialNative.h"
#include <windows.h>
#include <string>
#include <vector>
#include <unordered_map>
#include <mutex>
static std::mutex gMutex;
static std::unordered_map<int, HANDLE> gHandles;
static int gNextId = 1;
static thread_local std::wstring gLastError;
static void SetLastErrFromWin32(const wchar_t* where, DWORD err = GetLastError()) {
wchar_t* msg = nullptr;
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPWSTR)&msg, 0, nullptr);
if (msg) { gLastError = std::wstring(where) + L": (" + std::to_wstring(err) + L") " + msg; LocalFree(msg); }
else { gLastError = std::wstring(where) + L": error " + std::to_wstring(err); }
}
static std::wstring BuildDevicePath(const std::wstring& portName) {
if (portName.rfind(L"\\\\.\\", 0) == 0) return portName; // уже с префиксом
return L"\\\\.\\" + portName;
}
// --- аккуратный enumerator COM-портов ---
static std::wstring EnumerateComPortsJoined() {
std::vector<std::wstring> found;
wchar_t target[4096];
for (int i = 1; i <= 256; ++i) {
std::wstring name = L"COM" + std::to_wstring(i);
DWORD r = QueryDosDeviceW(name.c_str(), target, static_cast<DWORD>(std::size(target)));
if (r != 0) found.push_back(name);
}
std::wstring joined;
for (size_t i = 0; i < found.size(); ++i) {
if (i) joined += L";";
joined += found[i];
}
return joined;
}
// --- безопасная выдача HANDLE по id ---
static HANDLE GetHandle(int handleId) {
std::lock_guard<std::mutex> lk(gMutex);
auto it = gHandles.find(handleId);
return (it == gHandles.end()) ? nullptr : it->second;
}
// --- внутренняя запись байтов (без экспорта) ---
static int WriteBytes(HANDLE h, const uint8_t* data, int length) {
if (!h || !data || length < 0) { gLastError = L"WriteBytes: invalid args"; return -1; }
DWORD written = 0;
if (!WriteFile(h, data, static_cast<DWORD>(length), &written, nullptr)) {
SetLastErrFromWin32(L"WriteFile");
return -1;
}
return static_cast<int>(written);
}
// --- гарантированная зачистка при выгрузке процесса (Alt+F4 и т.п.) ---
BOOL APIENTRY DllMain(HINSTANCE, DWORD reason, LPVOID) {
if (reason == DLL_PROCESS_DETACH) {
std::lock_guard<std::mutex> lk(gMutex);
for (auto& kv : gHandles) {
HANDLE h = kv.second;
if (h && h != INVALID_HANDLE_VALUE) CloseHandle(h);
}
gHandles.clear();
}
return TRUE;
}
// ---------- Перечень портов ----------
SERIAL_API
int __cdecl Serial_ListPortsLength() {
std::wstring s = EnumerateComPortsJoined();
return static_cast<int>(s.size() + 1);
}
SERIAL_API
int __cdecl Serial_ListPorts(wchar_t* buffer, int bufferChars) {
std::wstring s = EnumerateComPortsJoined();
int need = static_cast<int>(s.size() + 1);
if (!buffer || bufferChars <= 0) return need;
if (bufferChars < need) return -1;
wcsncpy_s(buffer, bufferChars, s.c_str(), _TRUNCATE);
return need;
}
// ---------- Работа с портом ----------
SERIAL_API
int __cdecl Serial_Open(const wchar_t* portName,
int baudRate, int dataBits, int parity, int stopBits,
int readTimeoutMs, int writeTimeoutMs)
{
if (!portName || !*portName) { gLastError = L"Serial_Open: empty portName"; return 0; }
std::wstring dev = BuildDevicePath(portName);
// Retry-цикл, лечит ACCESS_DENIED/SHARING_VIOLATION сразу после Alt+F4
const int kMaxTries = 20; // до ~2 сек
const DWORD kSleepMs = 100;
HANDLE h = INVALID_HANDLE_VALUE;
DWORD lastErr = 0;
for (int i = 0; i < kMaxTries; ++i) {
h = CreateFileW(dev.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (h != INVALID_HANDLE_VALUE) break;
lastErr = GetLastError();
if (lastErr == ERROR_ACCESS_DENIED || lastErr == ERROR_SHARING_VIOLATION) {
Sleep(kSleepMs);
continue;
}
else {
break;
}
}
if (h == INVALID_HANDLE_VALUE) { SetLastErrFromWin32(L"CreateFileW", lastErr); return 0; }
SetupComm(h, 1 << 15, 1 << 15); // 32KB RX/TX
// DCB
DCB dcb{}; dcb.DCBlength = sizeof(DCB);
if (!GetCommState(h, &dcb)) { SetLastErrFromWin32(L"GetCommState"); CloseHandle(h); return 0; }
dcb.BaudRate = static_cast<DWORD>(baudRate);
dcb.ByteSize = static_cast<BYTE>(dataBits > 0 ? dataBits : 8);
dcb.fBinary = TRUE;
dcb.fParity = (parity != 0);
switch (parity) {
case 0: dcb.Parity = NOPARITY; break;
case 1: dcb.Parity = ODDPARITY; break;
case 2: dcb.Parity = EVENPARITY; break;
case 3: dcb.Parity = MARKPARITY; break;
case 4: dcb.Parity = SPACEPARITY;break;
default: dcb.Parity = NOPARITY; break;
}
dcb.StopBits = (stopBits == 2) ? TWOSTOPBITS : ONESTOPBIT;
dcb.fDtrControl = DTR_CONTROL_DISABLE; // как раньше
dcb.fRtsControl = RTS_CONTROL_DISABLE; // как раньше
if (!SetCommState(h, &dcb)) { SetLastErrFromWin32(L"SetCommState"); CloseHandle(h); return 0; }
// Таймауты
COMMTIMEOUTS to{};
to.ReadIntervalTimeout = 50;
to.ReadTotalTimeoutConstant = (readTimeoutMs >= 0) ? static_cast<DWORD>(readTimeoutMs) : 0;
to.ReadTotalTimeoutMultiplier = 0;
to.WriteTotalTimeoutConstant = (writeTimeoutMs >= 0) ? static_cast<DWORD>(writeTimeoutMs) : 0;
to.WriteTotalTimeoutMultiplier = 0;
if (!SetCommTimeouts(h, &to)) { SetLastErrFromWin32(L"SetCommTimeouts"); CloseHandle(h); return 0; }
// Полная очистка очередей на старте
PurgeComm(h, PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT);
// Регистрируем handle
{
std::lock_guard<std::mutex> lk(gMutex);
int id = gNextId++;
gHandles[id] = h;
return id;
}
}
SERIAL_API
int __cdecl Serial_Close(int handleId) {
std::lock_guard<std::mutex> lk(gMutex);
auto it = gHandles.find(handleId);
if (it == gHandles.end()) { gLastError = L"Serial_Close: invalid handleId"; return 0; }
HANDLE h = it->second;
gHandles.erase(it);
if (h && h != INVALID_HANDLE_VALUE) { CloseHandle(h); return 1; }
return 0;
}
SERIAL_API
int __cdecl Serial_CloseAll() {
std::lock_guard<std::mutex> lk(gMutex);
int closed = 0;
for (auto& kv : gHandles) {
HANDLE h = kv.second;
if (h && h != INVALID_HANDLE_VALUE) { CloseHandle(h); ++closed; }
}
gHandles.clear();
return closed;
}
// ---------- Запись ----------
SERIAL_API
int __cdecl Serial_WriteStringUTF8(int handleId, const char* utf8, int appendNewline) {
if (!utf8) { gLastError = L"Serial_WriteStringUTF8: utf8 is null"; return -1; }
HANDLE h = GetHandle(handleId);
if (!h) { gLastError = L"Serial_WriteStringUTF8: invalid handleId"; return -1; }
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(utf8);
int len = static_cast<int>(strlen(utf8));
int total = 0;
int w = WriteBytes(h, bytes, len);
if (w < 0) return -1;
total += w;
if (appendNewline) {
const uint8_t nl = '\n';
w = WriteBytes(h, &nl, 1);
if (w < 0) return -1;
total += w;
}
return total;
}
// ---------- Чтение ----------
SERIAL_API
int __cdecl Serial_ReadLineUTF8(int handleId, char* buffer, int bufferSize) {
HANDLE h = GetHandle(handleId);
if (!h) { gLastError = L"handle incorrect"; return -1; }
if (buffer == nullptr) { gLastError = L"Serial_ReadLineUTF8: buffer is null"; return -1; }
if (bufferSize <= 1) {
gLastError = L"Serial_ReadLineUTF8: bufferSize too small (<=1). Got " + std::to_wstring(bufferSize);
buffer[0] = 0; return -1;
}
int pos = 0;
DWORD read = 0;
char ch = 0;
while (pos < bufferSize - 1) {
BOOL ok = ReadFile(h, &ch, 1, &read, nullptr);
if (!ok) { SetLastErrFromWin32(L"ReadFile"); return -1; }
if (read == 0) { // таймаут
if (pos == 0) { buffer[0] = 0; return 0; }
break;
}
if (ch == '\n') break;
if (ch != '\r') buffer[pos++] = ch;
}
if (pos >= bufferSize - 1) {
buffer[bufferSize - 1] = 0;
return -2; // усечено
}
buffer[pos] = 0;
return pos;
}
// ---------- Диагностика ----------
SERIAL_API
int __cdecl Serial_GetLastErrorW(wchar_t* buffer, int bufferChars) {
int need = static_cast<int>(gLastError.size() + 1);
if (!buffer || bufferChars <= 0) return need;
if (bufferChars < need) return -1;
wcsncpy_s(buffer, bufferChars, gLastError.c_str(), _TRUNCATE);
return need;
}

View File

@ -1,29 +0,0 @@
// SerialNative.h
#pragma once
#include <cstdint>
#ifdef _WIN32
#define SERIAL_API extern "C" __declspec(dllexport)
#else
#define SERIAL_API extern "C"
#endif
// --- Перечень портов ---
SERIAL_API int __cdecl Serial_ListPortsLength();
SERIAL_API int __cdecl Serial_ListPorts(wchar_t* buffer, int bufferChars);
// --- Работа с портом ---
SERIAL_API int __cdecl Serial_Open(const wchar_t* portName,
int baudRate, int dataBits, int parity, int stopBits,
int readTimeoutMs, int writeTimeoutMs);
SERIAL_API int __cdecl Serial_Close(int handleId);
SERIAL_API int __cdecl Serial_CloseAll(); // NEW: глобальная зачистка хэндлов
// --- Запись (оставили только строковую; байтовую убрали из экспорта) ---
SERIAL_API int __cdecl Serial_WriteStringUTF8(int handleId, const char* utf8, int appendNewline);
// --- Чтение (оставили только построчное) ---
SERIAL_API int __cdecl Serial_ReadLineUTF8(int handleId, char* buffer, int bufferSize);
// --- Диагностика ---
SERIAL_API int __cdecl Serial_GetLastErrorW(wchar_t* buffer, int bufferChars);

View File

@ -1,19 +0,0 @@
// dllmain.cpp : Определяет точку входа для приложения DLL.
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

View File

@ -1,5 +0,0 @@
#pragma once
#define WIN32_LEAN_AND_MEAN // Исключите редко используемые компоненты из заголовков Windows
// Файлы заголовков Windows
#include <windows.h>

View File

@ -1,5 +0,0 @@
// pch.cpp: файл исходного кода, соответствующий предварительно скомпилированному заголовочному файлу
#include "pch.h"
// При использовании предварительно скомпилированных заголовочных файлов необходим следующий файл исходного кода для выполнения сборки.

View File

@ -1,13 +0,0 @@
// pch.h: это предварительно скомпилированный заголовочный файл.
// Перечисленные ниже файлы компилируются только один раз, что ускоряет последующие сборки.
// Это также влияет на работу IntelliSense, включая многие функции просмотра и завершения кода.
// Однако изменение любого из приведенных здесь файлов между операциями сборки приведет к повторной компиляции всех(!) этих файлов.
// Не добавляйте сюда файлы, которые планируете часто изменять, так как в этом случае выигрыша в производительности не будет.
#ifndef PCH_H
#define PCH_H
// Добавьте сюда заголовочные файлы для предварительной компиляции
#include "framework.h"
#endif //PCH_H

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<ProjectOutputs>
<ProjectOutput>
<FullPath>C:\Users\Programmer\source\repos\ComNative\x64\Release\ComNative.dll</FullPath>
</ProjectOutput>
</ProjectOutputs>
<ContentFiles />
<SatelliteDlls />
<NonRecipeFileRefs />
</Project>

View File

@ -1,8 +0,0 @@
 SerialNative.cpp
Создается библиотека C:\Users\Programmer\source\repos\ComNative\x64\Release\ComNative.lib и объект C:\Users\Programmer\source\repos\ComNative\x64\Release\ComNative.exp
Создание кода
1 of 376 functions ( 0.3%) were compiled, the rest were copied from previous compilation.
0 functions were new in current compilation
0 functions had inline decision re-evaluated but remain unchanged
Создание кода завершено
ComNative.vcxproj -> C:\Users\Programmer\source\repos\ComNative\x64\Release\ComNative.dll

View File

@ -1,2 +0,0 @@
C:\Users\Programmer\source\repos\ComNative\ComNative\SerialNative.cpp;C:\Users\Programmer\source\repos\ComNative\ComNative\x64\Release\SerialNative.obj
C:\Users\Programmer\source\repos\ComNative\ComNative\pch.cpp;C:\Users\Programmer\source\repos\ComNative\ComNative\x64\Release\pch.obj

View File

@ -1,2 +0,0 @@
PlatformToolSet=v143:VCToolArchitecture=Native64Bit:VCToolsVersion=14.44.35207:TargetPlatformVersion=10.0.26100.0:
Release|x64|C:\Users\Programmer\source\repos\ComNative\|

View File

@ -1,5 +0,0 @@
^C:\USERS\PROGRAMMER\SOURCE\REPOS\COMNATIVE\COMNATIVE\X64\RELEASE\PCH.OBJ|C:\USERS\PROGRAMMER\SOURCE\REPOS\COMNATIVE\COMNATIVE\X64\RELEASE\SERIALNATIVE.OBJ
C:\Users\Programmer\source\repos\ComNative\x64\Release\ComNative.lib
C:\Users\Programmer\source\repos\ComNative\x64\Release\ComNative.EXP
C:\Users\Programmer\source\repos\ComNative\ComNative\x64\Release\ComNative.IPDB
C:\Users\Programmer\source\repos\ComNative\ComNative\x64\Release\ComNative.iobj

View File

@ -1,43 +0,0 @@
/// <summary>
/// Соответствие между кнопками игрового ввода и цифровыми выходами периферийной платы (пины 0-15).
/// Содержит mapping для управления двух игроков, служебных кнопок и датчиков хопера.
/// </summary>
public enum ButtonPinMapping
{
/// <summary>Кнопка движения вверх для первого игрока</summary>
up1 = 0,
/// <summary>Кнопка движения вправо для первого игрока</summary>
right1 = 1,
/// <summary>Кнопка движения вниз для первого игрока</summary>
down1 = 2,
/// <summary>Кнопка движения влево для первого игрока</summary>
left1 = 3,
/// <summary>Действие/огонь для первого игрока</summary>
action1 = 4,
/// <summary>Кнопка движения вверх для второго игрока</summary>
up2 = 5,
/// <summary>Кнопка движения вправо для второго игрока</summary>
right2 = 6,
/// <summary>Кнопка движения вниз для второго игрока</summary>
down2 = 7,
/// <summary>Кнопка движения влево для второго игрока</summary>
left2 = 8,
/// <summary>Действие/огонь для второго игрока</summary>
action2 = 9,
/// <summary>Запуск игры для одного игрока</summary>
startOnePlayer = 10,
/// <summary>Запуск игры для двух игроков</summary>
startTwoPlayers = 11,
/// <summary>Секретная/служебная кнопка для захода в настройки</summary>
secret = 12,
/// <summary>Дополнительная/резервная кнопка</summary>
extra = 13,
/// <summary>Датчик наличия приза в хопере</summary>
hasPrize = 14,
/// <summary>Датчик выдачи приза из хопера</summary>
prizeGiven = 15
}

View File

@ -1,253 +0,0 @@
using System;
using UnityEngine;
public class COMinput : InputSource
{
//Количество = цифровые + 2 датчика.
//На момент написания количество цифровых входов 7*N.
//Настроить количество пинов под свою настройку платы
private const int PIN_COUNT = 16;
private const char POSITIVE = '0';
private const char NEGATIVE = '1';
private bool[] buttonState = new bool[PIN_COUNT];
private bool[] buttonDown = new bool[PIN_COUNT];
private bool[] buttonUp = new bool[PIN_COUNT];
//Свойство для определения текущего игрока
//Должно возвращать 1 или 2
//Используется для получения состояния кнопок с пометкой ...CurrentPlayer
private int currentPlayer => gm.currentPlayer == CellState.Player1 ? 1 : 2;//ПЕРЕОПРЕДЕЛИТЕ ОПРЕДЕЛЕНИЕ ТЕКУЩЕГО ИГРОКА ПОД СВОЮ СИСТЕМУ.
private GameManager gm;//я получаю кто сейчас ходит через этого менеджера. Если у вас другое, допишите.
public override bool Initialize()
{
try
{
gm = GameManager.instance;
//ОБЯЗАТЕЛЬНЫЙ БЛОК
ComWrapper.onInputRecieved.AddListener(UpdateInputState);
}
catch (Exception e)
{
Debug.Log($"Что то в COMinput {e.ToString()}");
return false;
}
return true;
}
#region PUBLIC_API
//ЭТА ГРУППА МЕТОДОВ ДЛЯ ПОЛУЧЕНИЯ СОСТОЯНИЯ ЛЮБОЙ КНОПКИ ПО ПИНУ (enum ButtonPinMapping)
/// <summary>
/// Получает состояние конкретной кнопки по её физическому пину.
/// Возвращает true если кнопка в данный момент нажата.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>True если кнопка нажата в текущем кадре</returns>
public override bool GetButton(ButtonPinMapping button) => buttonState[(int)button];
/// <summary>
/// Проверяет, была ли кнопка нажата в начале текущего кадра.
/// Удерживает true до тех пор, пока кто то не проверит, затем сбрасывает.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>True если кнопка была нажата в этом кадре</returns>
public override bool GetButtonDown(ButtonPinMapping button) => ConsumeButtonEvent(buttonDown, button);
/// <summary>
/// Проверяет, была ли кнопка отпущена в начале текущего кадра.
/// Удерживает true до тех пор, пока кто то не проверит, затем сбрасывает.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>True если кнопка была отпущена в этом кадре</returns>
public override bool GetButtonUp(ButtonPinMapping button) => ConsumeButtonEvent(buttonUp, button);
/// <summary>
/// Получает состояние кнопки по числовому индексу пина напрямую из буфера состояний.
/// Обходит перечисление ButtonPinMapping для прямого доступа к пинам по индексу.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-15)</param>
/// <returns>true если кнопка нажата в текущий момент</returns>
/// <remarks>
/// Используется когда необходим прямой доступ к пинам без приведения к перечислению.
/// Подходит для сценариев где количество пинов может превышать размер перечисления.
/// </remarks>
public override bool GetButton(int pinIndex) => buttonState[pinIndex];
/// <summary>
/// Проверяет, была ли кнопка нажата в начале текущего кадра по числовому индексу пина.
/// Возвращает true только в первом кадре после нажатия и сбрасывает флаг события.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-15)</param>
/// <returns>true если кнопка была нажата в этом кадре</returns>
/// <remarks>
/// Метод "потребляет" событие - после чтения флаг сбрасывается.
/// Использует общий механизм ConsumeButtonEvent для обработки одноразовых событий.
/// </remarks>
public override bool GetButtonDown(int pinIndex) => ConsumeButtonEvent(buttonDown, pinIndex);
/// <summary>
/// Проверяет, была ли кнопка отпущена в начале текущего кадра по числовому индексу пина.
/// Возвращает true только в первом кадре после отпускания и сбрасывает флаг события.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-15)</param>
/// <returns>true если кнопка была отпущена в этом кадре</returns>
/// <remarks>
/// Метод "потребляет" событие - после чтения флаг сбрасывается.
/// Использует общий механизм ConsumeButtonEvent для обработки одноразовых событий.
/// </remarks>
public override bool GetButtonUp(int pinIndex) => ConsumeButtonEvent(buttonUp, pinIndex);
// ------------------------------------------------------------------------------------
//ЭТА ГРУППА МЕТОДОВ ПОЗВОЛЯЕТ УЗНАТЬ СОСТОЯНИЯ КНОПОК АКТИВНОГО ИГРОКА (проверка по enum PlayerAction)
//Служебные кнопки типа startOnePlayer, startTwoPlayers, secret, extra, hasPrize, prizeGiven здесь не проверяются
/// <summary>
/// Получает состояние действия для текущего игрока.
/// Возвращает true если действие активно (кнопка нажата) у текущего игрока.
/// </summary>
/// <param name="button">Действие игрока (up, down, left, right, action, extra)</param>
/// <returns>True если действие активно у текущего игрока</returns>
public override bool GetButtonCurrentPlayer(PlayerAction button)
=> GetButton(GetButtonForCurrentPlayer(button));
/// <summary>
/// Проверяет, было ли действие начато текущим игроком в этом кадре.
/// Возвращает true только в первом кадре после начала действия.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было начато в этом кадре</returns>
public override bool GetButtonDownCurrentPlayer(PlayerAction button)
=> GetButtonDown(GetButtonForCurrentPlayer(button));
/// <summary>
/// Проверяет, было ли действие завершено текущим игроком в этом кадре.
/// Возвращает true только в первом кадре после завершения действия.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было завершено в этом кадре</returns>
public override bool GetButtonUpCurrentPlayer(PlayerAction button)
=> GetButtonUp(GetButtonForCurrentPlayer(button));
// ------------------------------------------------------------------------------------
//ЭТА ГРУППА МЕТОДОВ ПОЗВОЛЯЕТ УЗНАТЬ СОСТОЯНИЯ КНОПОК ЛЮБОГО ИГРОКА (проверка по enum PlayerAction)
//Служебные кнопки типа startOnePlayer, startTwoPlayers, secret, extra, hasPrize, prizeGiven здесь не проверяются
/// <summary>
/// Получает состояние действия для любого из игроков.
/// Возвращает true если действие активно у первого ИЛИ второго игрока.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие активно у любого игрока</returns>
public override bool GetButtonAnyPlayer(PlayerAction button)
=> CheckBothPlayers(button, GetButton);
/// <summary>
/// Проверяет, было ли действие начато любым игроком в этом кадре.
/// Возвращает true если первый ИЛИ второй игрок начали действие в этом кадре.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было начато любым игроком в этом кадре</returns>
public override bool GetButtonDownAnyPlayer(PlayerAction button)
=> CheckBothPlayers(button, GetButtonDown);
/// <summary>
/// Проверяет, было ли действие завершено любым игроком в этом кадре.
/// Возвращает true если первый ИЛИ второй игрок завершили действие в этом кадре.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было завершено любым игроком в этом кадре</returns>
public override bool GetButtonUpAnyPlayer(PlayerAction button)
=> CheckBothPlayers(button, GetButtonUp);
#endregion
//==========ХЕЛПЕРЫ==========
// Проверка нажатия с обоих контроллеров игроков
private bool CheckBothPlayers(PlayerAction action, Func<ButtonPinMapping, bool> buttonFunc)
{
var buttons = _buttonByAction(action);
return buttonFunc(buttons.player1button) || buttonFunc(buttons.player2button);
}
// Проверка нажатия с контроллера текущего игрока
private ButtonPinMapping GetButtonForCurrentPlayer(PlayerAction button)
{
var buttons = _buttonByAction(button);
return currentPlayer == 1 ? buttons.player1button : buttons.player2button;
}
// Маппер из действий игрока => пины платы
private (ButtonPinMapping player1button, ButtonPinMapping player2button) _buttonByAction(PlayerAction playerAction) =>
playerAction switch
{
PlayerAction.up => (ButtonPinMapping.up1, ButtonPinMapping.up2),
PlayerAction.down => (ButtonPinMapping.down1, ButtonPinMapping.down2),
PlayerAction.left => (ButtonPinMapping.left1, ButtonPinMapping.left2),
PlayerAction.right => (ButtonPinMapping.right1, ButtonPinMapping.right2),
PlayerAction.action => (ButtonPinMapping.action1, ButtonPinMapping.action2),
PlayerAction.extra => (ButtonPinMapping.extra, ButtonPinMapping.extra), // одинаковая кнопка
_ => throw new ArgumentException($"Unknown player action: {playerAction}")
};
// Обновляет состояния инпута когда приходит новый ввод с платы. Дергается событием.
private void UpdateInputState(string input)
{
// Будем принимать в нативном формате by Pasha. 0 - нажато 1 - не нажато (чтобы преобразовывать циклом)
// В параметрприходит строка вида "1111011111011111"
for (int i = 0; i < input.Length; i++)
{
// Получаем новое состояние пина
bool newState = input[i] == POSITIVE;
// Состояние пина изменилось => пишем в буферы
if (newState != buttonState[i])
{
if(newState) // Пин был зажат
{
buttonDown[i] = true;
buttonUp[i] = false;
}
else // Пин был отжат
{
buttonDown[i] = false;
buttonUp[i] = true;
}
}
buttonState[i] = newState;
}
if (input[^1] == POSITIVE) PlayerPrefs.SetInt("PrizeGivenBuffer", 1); //пишем в буфер чтобы между кадрами и порядком вызова не проебать
DebugWindow.Message("Инпуты: " + string.Join("", buttonState));//можно вырезать
}
// Потребление "Нажатия"/"Отжатия". Возвращает что там было и чистит флаг из буфера
private bool ConsumeButtonEvent(bool[] buffer, ButtonPinMapping button)
{
bool result = buffer[(int)button];
buffer[(int)button] = false;
return result;
}
/// <summary>
/// Вспомогательный метод для обработки одноразовых событий кнопок по числовому индексу.
/// Возвращает текущее значение флага и сбрасывает его для предотвращения повторной обработки.
/// </summary>
/// <param name="buffer">Буфер событий (buttonDown или buttonUp)</param>
/// <param name="pinIndex">Числовой индекс пина</param>
/// <returns>Состояние флага события до сброса</returns>
private bool ConsumeButtonEvent(bool[] buffer, int pinIndex)
{
bool result = buffer[pinIndex];
buffer[pinIndex] = false;
return result;
}
}

View File

@ -1,57 +0,0 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
public static class ComNativeWrapper
{
private const string Dll = "ComNative"; // имя файла в Plugins/x86_64 без .dll
// --- Порты (Unicode) ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_ListPortsLength();
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_ListPorts(StringBuilder buffer, int bufferChars);
// --- Открытие/закрытие ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_Open(string portName, int baud, int dataBits, int parity, int stopBits,
int readTimeoutMs, int writeTimeoutMs);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern int Serial_Close(int handleId);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern int Serial_CloseAll(); // NEW
// --- Запись/чтение (строкой) ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern int Serial_WriteStringUTF8(int handleId, string utf8, int appendNewline);
// byte[]-вариант к той же native-функции (EntryPoint), чтобы избежать маршаллинга StringBuilder
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "Serial_ReadLineUTF8")]
public static extern int Serial_ReadLineUTF8_bytes(int handleId, byte[] buffer, int bufferSize);
// --- Ошибка (Unicode) ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_GetLastErrorW(StringBuilder buffer, int bufferChars);
public static string GetLastError()
{
int need = Serial_GetLastErrorW(null, 0);
var sb = new StringBuilder(Math.Max(need, 1));
int r = Serial_GetLastErrorW(sb, sb.Capacity);
return (r < 0) ? $"err({r})" : sb.ToString();
}
public static string[] ListPorts()
{
int need = Serial_ListPortsLength();
if (need <= 1) return Array.Empty<string>();
var sb = new StringBuilder(need);
int r = Serial_ListPorts(sb, sb.Capacity);
if (r < 0) return Array.Empty<string>();
string joined = sb.ToString();
return string.IsNullOrWhiteSpace(joined) ? Array.Empty<string>() : joined.Split(';');
}
}

View File

@ -1,262 +0,0 @@
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> onCoinAdded;
public static UnityEvent<int> onCoinRemoved;
public static UnityEvent<string> onInputRecieved;
#region Приватные поля
private const string REQUEST_INPUT = "91";
private const string DEFAULT_INPUT = "INPUT1111111111111111";
private const string LIGHTSIGNAL_PREFIX = "80";
private static int h;//идентификатор хэндла
private static byte[] buf = new byte[1024]; //Байт-буфер куда будут приходить данные с порта
private static int CreditsCount;
private string inputString = DEFAULT_INPUT;
#endregion
//===========================start: PUBLIC API===========================//
/// <summary>
/// Инициализирует систему работы с COM-портом.
/// Открывает соединение, настраивает события и запускает периодические запросы состояния.
/// </summary>
/// <remarks>
/// Обязателен к вызову при старте приложения. Выполняет:
/// - Создание UnityEvent'ов для уведомлений
/// - Загрузку количества кредитов из PlayerPrefs
/// - Открытие COM-порта
/// - Запуск периодического опроса пинов (если включена проверка призов)
/// </remarks>
public void Initialize()
{
//Создаем ивенты
onCoinAdded = new UnityEvent<int>();
onCoinRemoved = new UnityEvent<int>();
//Начальная настройка
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
CreditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
initPort(); //вот это важный метод который собственно порт открывает.
//Каждые 5 сек запрашиваем инпуты у платы. Надо потому что при заходе в игру с пинов еще не пришла инфа
if (enablePrizeChecking) InvokeRepeating("requestInputs", 0, 5);
}
/// <summary>
/// Добавляет один кредит в систему и сохраняет обновленное количество.
/// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта.
/// </summary>
/// <remarks>
/// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков.
/// Автоматически сохраняет состояние в PlayerPrefs.
/// </remarks>
public static void AddCredit()
{
CreditsCount++;
PlayerPrefs.SetInt("CoinsCount", CreditsCount);
onCoinAdded?.Invoke(CreditsCount);
}
/// <summary>
/// Отправляет произвольную строковую команду на периферийную плату через COM-порт.
/// </summary>
/// <param name="message">Команда для отправки</param>
/// <remarks>
/// Используется для отправки пользовательских команд на плату.
/// Команда должна быть в формате, ожидаемом прошивкой платы.
/// В случае ошибки отправки выводит предупреждение в DebugWindow.
/// </remarks>
public static void Send(string message)
{
try
{
int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1); //w-количество записаных символов в порт. можно обработать
//Debug.Log("Wrote: " + w);
}
catch { DebugWindow.Message("Ошибка отправки строкового сигнала."); }
}
/// <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 { DebugWindow.Message("Ошибка отправки светового сигнала."); }
}
/// <summary>
/// Выполняет списание платы за запуск игры.
/// Проверяет наличие доступных кредитов или режим бесплатной игры.
/// При успешном списании увеличивает счетчики запусков.
/// </summary>
/// <returns>
/// true - если списание прошло успешно (бесплатный режим или есть кредиты),
/// false - если кредитов недостаточно и платный режим
/// </returns>
/// <remarks>
/// В случае успеха:
/// - Увеличивает общие счетчики запусков (key_starts и key_startsNul)
/// - При платном режиме уменьшает количество кредитов на 1
/// - Вызывает событие onCoinRemoved при списании кредита
/// </remarks>
public static bool Pay()
{
//блок проверки оплаты
if (SettingsManager.instance.GetSettingValue("Бесплатная игра") == "Вкл")
{
//Счетчики пробега
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
return true;
}
if (CreditsCount > 0)
{
CreditsCount--;
PlayerPrefs.SetInt("CoinsCount", CreditsCount);
//счетчики пробега
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
onCoinRemoved?.Invoke(CreditsCount);
return true;
}
return false;
}
//===========================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; }
string port = ports[^1];
Debug.Log("Открываю первый с конца порт: " + port);
//вот эти настройки лучше не ковырять, но если очень хочется можно поменять readTimeoutMs и writeTimeoutMs
//Если read поставить слишком маленьким оно проебует кредиты когда они идут большой пачкой например при оплате через вендисту.
//Там в порт спамит "COIN" в больших количествах и процентов 10 из этих строк не успевают прочитаться при таймауте 1мс, соотв. монеты проебуются.
//Слишком большой таймаут будет тормозить игру потому что оно все выполняется синхронно и каждый кадр забирает столько мс сколько в переменной указано.
h = ComNativeWrapper.Serial_Open(port, 115200, 8, 0, 1, 5, 10);
Debug.Log($"handle: {h}");
if (h <= 0)
{
Debug.Log("Open failed: " + ComNativeWrapper.GetLastError());
return;
}
}
private static void ClosePort()
{
if (h > 0)
{
ComNativeWrapper.Serial_Close(h);
h = 0;
}
}
//Тут чтение, не трогаем(опционально)
private static string ReadBufferLine()
{
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buf, buf.Length);
if (read > 0)
{
var s = Encoding.UTF8.GetString(buf, 0, read);
//Debug.Log($"ReadLine len={read} data=[{s}]");
return s;
}
else if (read < 0)
{
Debug.Log($"ReadLine err={read} :: {ComNativeWrapper.GetLastError()}");
}
return "";
}
//просим прислать состояние цифровых входов. Приходит что то вроде "INPUT111111101"
private void requestInputs() => Send(REQUEST_INPUT);
private void pollPort()
{
//Тут все просто, читаем что нам приходит с порта и сравниваем.
string comInput = ReadBufferLine();
if (comInput != "")
{
DebugWindow.Message("COM: " + comInput);
if (comInput == "COIN") AddCredit();
if (comInput.StartsWith("INPUT")) onInputRecieved?.Invoke(comInput.Substring(5)); //инвокаю сразу без префикса "INPUT"
}
}
#endregion
}
public enum LightMode
{
Idle,
Orange,
Blue,
Win,
Lose,
Prize,
}

View File

@ -1,314 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
//Зависит от ComNativeWrapper и одноименного плагина
//рекомендуется соединение с таймаутом не меньше 10мс. Иначе вероятно busy loop и много алокаций.
public static class PrizeStandController
{
/// <summary>
/// По завершении инициализации.
/// bool - успех/неуспех.
/// string - сообщение в случае неуспеха
/// </summary>
public static event Action<bool, string> InitializationComplete;
/// <summary>
/// По завершении операции открытия.
/// bool - успех/неуспех.
/// string - сообщение в случае неуспеха
/// </summary>
public static event Action<bool, string> OpeningFinished;
private const string REQUEST_STAND_STATUS = "25";
private const string OPEN_BOX_PREFIX = "2501";
private const float STATUS_REQUEST_TIMEOUT = 3;
private const float WAIT_OPEN_STATUS_TIMEOUT = 5;
private static readonly IReadOnlyList<BoxStatus> READY_TO_OPEN_STATUSES = new[] { BoxStatus.OK, BoxStatus.OKplusCard };
private static int handle = -1;
private static int boxesCount;
private static readonly SemaphoreSlim _lock = new(1, 1);
private static bool initialized = false;
private static ILogger _logger;
#region PUBLIC API
/// <summary>
/// Инициализировать модуль витрины.
/// </summary>
/// <param name="handleId">Id хэндла полученное после ComNativeWrapper.Serial_Open</param>
/// <param name="countOfBoxes">Количество физических коробок витрины</param>
/// <param name="logger">(опционально) Логгер</param>
public static void Initialize(int handleId, int countOfBoxes, ILogger logger)
{
bool hasError = false;
StringBuilder sb = new StringBuilder();
//Проверка хэндла. h<=0 - ошибка открытия порта
if (handleId <= 0)
{
hasError = true;
sb.AppendLine($"[Модуль витрины] Переданный handle={handleId}. Ожидалось значение>0.");
}
else handle = handleId;
//Проверка количества коробок
if (countOfBoxes <= 0)
{
hasError = true;
sb.AppendLine($"[Модуль витрины] Попытка задать количество коробок={countOfBoxes}. Ожидалось значение>0.");
}
else boxesCount = countOfBoxes;
//Установка логгера
_logger = logger;
if (_logger == null)
sb.AppendLine($"[Модуль витрины] Получен логгер NULL. Если нужно логгирование, передайте дествующий логгер.");
//Завершение
if (hasError)
{
initialized = false;
handle = -1;
boxesCount = 0;
InitializationComplete?.Invoke(false, sb.ToString());
}
else
{
initialized = true;
InitializationComplete?.Invoke(true, sb.ToString());
}
}
/// <summary>
/// Открыть коробку по номеру
/// </summary>
/// <param name="boxNumber">Номер коробки (считаем с 1)</param>
public static async Task OpenBoxAsync(int boxNumber)
{
if(!await _lock.WaitAsync(0))
{
OpeningFinished?.Invoke(false, "Выдача уже в процессе");
return;
}
try
{
//Валидация
var validationResult = await CanExecuteOpenAsync(boxNumber);
if (!validationResult.OK)
{
OpeningFinished?.Invoke(false, validationResult.Message);
return;
}
SendOpenCommand(boxNumber);
//Обработка результатов
BoxStatus openStatus = await ReadBoxStatus(boxNumber, 1, WAIT_OPEN_STATUS_TIMEOUT);
if (openStatus == BoxStatus.OPENED)
{
OpeningFinished?.Invoke(true, $"Ячейка {boxNumber} успешно открыта");
}
else
{
_logger?.Log($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}");
OpeningFinished?.Invoke(false, $"Ошибка при открытии ячейки номер {boxNumber}");
}
}
catch (Exception ex)
{
_logger?.Log($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}: {ex.Message}");
OpeningFinished?.Invoke(false, ex.Message);
}
finally
{
_lock.Release();
}
}
#endregion
#region HELPERS
private static void SendOpenCommand(int boxNumber)
=> ComNativeWrapper.Serial_WriteStringUTF8(handleId: handle,
utf8: $"{OPEN_BOX_PREFIX}{boxNumber:D2}", //D2 приводит в формат XX (2 знака, ноль впереди)
appendNewline: 1);
private static void SendRequestStatusCommand()
=> ComNativeWrapper.Serial_WriteStringUTF8(handle, REQUEST_STAND_STATUS, 1);
/// <summary>
/// Проверка может ли модуль произвести выдачу
/// </summary>
private static async Task<(bool OK, string Message)> CanExecuteOpenAsync(int boxNumber)
{
bool isOk = true;
StringBuilder sb = new StringBuilder();
sb.AppendLine($"[Модуль витрины. Проверка перед открытием]");
//Проверка инициализации
if (!initialized)
{
isOk = false;
sb.AppendLine("- Модуль не инициализирован.");
}
bool boxNumberIsValid = boxNumber > 0 && boxNumber <= boxesCount;
if (!boxNumberIsValid)
{
isOk = false;
sb.AppendLine($"- Номер коробки={boxNumber} невалидный. Требуется от 1 до {boxesCount}.");
}
//Не лезем в порт с запросом статуса коробки, если то, что выше дало НеОК
if (isOk)
{
SendRequestStatusCommand();
BoxStatus boxStatus = await ReadBoxStatus(boxNumber, boxesCount, STATUS_REQUEST_TIMEOUT);
if (!READY_TO_OPEN_STATUSES.Contains(boxStatus))
{
isOk = false;
sb.AppendLine($"- Статус коробки {boxNumber}: {boxStatus} не позволяет провести открытие.");
}
}
else
{
sb.AppendLine($"- Невозможно проверить статус коробки {boxNumber} из-за ошибок выше.");
}
return (isOk, sb.ToString());
}
/// <summary>
/// Читать статус заданной ячейки витрины. Выход когда прочитали заданное количество вхождений, или по таймауту.
/// </summary>
/// <param name="expectedBoxNumber">Номер ячейки которую мы ждем</param>
/// <param name="expectedEntriesCount">Сколько ячеек мы ждем</param>
/// <param name="timeout">Таймаут операции(сек.)</param>
/// <returns></returns>
private static async Task<BoxStatus> ReadBoxStatus(int expectedBoxNumber, int expectedEntriesCount, float timeout)
{
byte[] buffer = new byte[1024];
//Изначально предполагаем что статус нужной коробки не пришел
BoxStatus status = BoxStatus.NotFound;
int boxesRecieved = 0;
var sw = Stopwatch.StartNew();
while (sw.Elapsed.TotalSeconds < timeout && boxesRecieved < expectedEntriesCount)
{
//ждать пока всё не считаем
int entryLength = await Task.Run(() => ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle, buffer, buffer.Length));
if (entryLength == 0) //ничего не пришло
{
continue;
}
else if (entryLength < 0) //ошибка
{
_logger?.Log($"Ошибка при считывании статуса ячеек={entryLength} :: {ComNativeWrapper.GetLastError()}");
continue;
}
string entry = Encoding.UTF8.GetString(buffer, 0, entryLength).Trim();
var ParseResult = ParseBoxStatus(entry);
if(ParseResult.BoxNumber == -1)
{
_logger?.Log($"[Модуль витрины. Ошибка получения статуса ячейки]\n{ParseResult.Message}");
}
else
{
boxesRecieved++;
if (ParseResult.BoxNumber == expectedBoxNumber)
status = ParseResult.Status;
}
}
if (status == BoxStatus.NotFound)
_logger?.Log($"[Модуль витрины. Не дождался ответа по статусу ячейки]\n" +
$"Ожидаемый номер: {expectedBoxNumber}\n" +
$"Получено статусов ячеек: {boxesRecieved}/{expectedEntriesCount}");
return status;
}
private static (int BoxNumber, BoxStatus Status, string Message) ParseBoxStatus(string entryString)
{
int number = -1;
BoxStatus status = BoxStatus.ParsingError;
string message = "";
if (entryString.StartsWith("BOX"))
{
var noPrefixEntry = entryString.Substring(3); //Убираем "BOX"
string[] parts = noPrefixEntry.Split('_'); //ожидаем массив [<номер_коробки>, <статус_коробки>]
if (parts.Length == 2)
{
//Парсим номер коробки
bool boxNumberParsed = (int.TryParse(parts[0], out int parsedNumber));
if (boxNumberParsed)
{
number = parsedNumber;
status = MapStatusFromString(parts[1]);
}
else message = $"Не удалось спарсить номер коробки: {entryString}";
}
else
{
message = $"Ошибка парсинга. Часть строки не пришла: {entryString}";
}
}
else message = $"Неожиданный префикс при парсинге ячеек: {entryString}";
return (number, status, message);
}
private static BoxStatus MapStatusFromString(string statusString) => statusString switch
{
"OK" => BoxStatus.OK,
"OK+CARD" => BoxStatus.OKplusCard,
"EMPTY" => BoxStatus.Empty,
"OPEN" => BoxStatus.Open,
"OPENED" => BoxStatus.OPENED,
"OPEN_ERROR" => BoxStatus.OPEN_ERROR,
"ERROR" => BoxStatus.Error,
_ => BoxStatus.ParsingError
};
#endregion
}
enum BoxStatus
{
//Статусы ДО открытия
OK, //Готов к выдаче
OKplusCard, //Готов к выдаче + готов выдать по карте
Empty, //Пусто, уже выдали
Open, //Дверка УЖЕ открыта. Приз предположительно пустой
//Статусы ПОСЛЕ открытия
OPENED, //Всё хорошо, открылся
OPEN_ERROR, //Ошибка открытия
//Служебные статусы
ParsingError, //При парсинге что то пошло не так, или пришла непредусмотренная фигня
NotFound, //Коробка с требуемым номером не найдена
Error //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок)
}

View File

@ -1,114 +0,0 @@
using System.Collections;
using UnityEngine;
/// <summary>
/// Система управления выдачей призов и ведения статистики игры.
/// Отвечает за процесс выдачи призов, подсчет запусков и наград.
/// </summary>
public class PrizeSystem : MonoBehaviour
{
[SerializeField] AudioClip prizeErrorSound;
public static string key_starts = "STARTS";
public static string key_startsNul = "STARTSNUL";
public static string key_prizesgiven = "PRIZESGIVEN";
public static string key_prizesgivenNul = "PRIZESGIVENNUL";
private static PrizeSystem instance;
private static Coroutine prizeCoroutine;
private static bool prizing = false;
//===========================start: PUBLIC API===========================//
/// <summary>
/// Инициализирует систему призов и счетчики статистики.
/// Должен быть вызван один раз при запуске приложения.
/// </summary>
/// <remarks>
/// Выполняет:
/// - Установку статического экземпляра (singleton)
/// - Инициализацию счетчиков в PlayerPrefs если они отсутствуют
/// - Подготовку системы к работе
/// </remarks>
public void Initialize()
{
instance = this;
InitCounters();
}
/// <summary>
/// Запускает процесс выдачи физического приза игроку.
/// Проверяет настройки и текущее состояние перед началом выдачи.
/// </summary>
/// <remarks>
/// Условия для выдачи приза:
/// - В настройках включена выдача призов при игре с ботом
/// - В данный момент не выполняется другая выдача приза
///
/// Процесс выдачи:
/// - Включает мотор хопера
/// - Ожидает подтверждения выдачи от датчиков (10 сек таймаут)
/// - Выключает мотор по завершению
/// - Обновляет счетчики выданных призов
/// - Воспроизводит звук ошибки при неудаче
/// </remarks>
public static void GivePrize()
{
bool settingEnabled = SettingsManager.instance.GetSettingValue("Выдача призов при игре с ботом") == "Выдавать";
if (!settingEnabled) { DebugWindow.Message("Выдача приза отключена в настройках"); return; }
if (prizing) { DebugWindow.Message("Уже идет выдача приза. Не спамь кнопку."); return; }
DebugWindow.Message("Выдаю приз");
if (prizeCoroutine != null) instance.StopCoroutine(prizeCoroutine);
prizeCoroutine = instance.StartCoroutine(instance.Prize());
}
//===========================end: PUBLIC API===========================//
#region Приватные методы
private IEnumerator Prize()
{
prizing = true;
DebugWindow.Message("Начинаю выдачу");
float timeout = 10; //сек
ComWrapper.Send("301");//включаем мотор
float startTime = Time.time;
bool success = false;
PlayerPrefs.SetInt("PrizeGivenBuffer", 0);//На всякий случай подготовка буфера
//проверить где он пишется
while (Time.time - startTime < timeout)
{
if (PlayerPrefs.GetInt("PrizeGivenBuffer", 0) == 1)
{
success = true;
IncreasePrizeCounters();
break;
}
yield return null;
}
//Выключаем мотор
ComWrapper.Send("300");
DebugWindow.Message("Закончил. Успех: " + success);
prizing = false;
if (!success) SoundManager.instance.playClip(prizeErrorSound);
}
void IncreasePrizeCounters()
{
PlayerPrefs.SetInt(key_prizesgiven, PlayerPrefs.GetInt(key_prizesgiven) + 1);
PlayerPrefs.SetInt(key_prizesgivenNul, PlayerPrefs.GetInt(key_prizesgivenNul) + 1);
}
void InitCounters()
{
if (!PlayerPrefs.HasKey(key_starts)) PlayerPrefs.SetInt(key_starts, 0);
if (!PlayerPrefs.HasKey(key_startsNul)) PlayerPrefs.SetInt(key_startsNul, 0);
if (!PlayerPrefs.HasKey(key_prizesgiven)) PlayerPrefs.SetInt(key_prizesgiven, 0);
if (!PlayerPrefs.HasKey(key_prizesgivenNul)) PlayerPrefs.SetInt(key_prizesgivenNul, 0);
}
#endregion
}

View File

@ -1,89 +0,0 @@
using UnityEngine;
/// <summary>
/// Статический фасад для унифицированного доступа к системе ввода.
/// Предоставляет глобальный интерфейс для работы с управлением независимо от выбранного источника.
/// </summary>
/// <remarks>
/// Этот класс является точкой входа для всей игровой логики, работающей с вводом.
/// Автоматически инициализирует выбранный источник управления (клавиатура или COM-порт)
/// и предоставляет статические методы для удобного доступа из любого места кода.
///
/// Использование:
/// 1. Настройте тип ввода в инспекторе
/// 2. Вызовите Initialize() при старте игры
/// 3. Используйте статические методы для проверки ввода
/// </remarks>
public class In : MonoBehaviour
{
[Header("Выбор источника управления")]
[SerializeField] private InputSourceType inputType;
static InputSource source;
/// <summary>
/// Инициализирует систему ввода на основе выбранного типа.
/// Должен быть вызван один раз при запуске приложения.
/// </summary>
/// <remarks>
/// Создает соответствующий экземпляр InputSource:
/// - KeyboardInput для тестирования на клавиатуре
/// - COMinput для работы с периферийной платой
/// </remarks>
public void Initialize()
{
source = inputType switch
{
InputSourceType.Keyboard => new KeyboardInput(),
InputSourceType.COM => new COMinput(),
_ => throw new System.NotImplementedException()
};
var success = source.Initialize();
if (!success) Debug.LogWarning("Не удалось проинициалзировать источник управления.");
}
// ================================================================================
// СТАТИЧЕСКИЕ МЕТОДЫ ДЛЯ РАБОТЫ С ФИЗИЧЕСКИМИ ПИНАМИ
// ================================================================================
/// <summary>Проверяет состояние конкретной кнопки по её физическому пину</summary>
public static bool GetButton(ButtonPinMapping button) => source.GetButton(button);
/// <summary>Проверяет нажатие кнопки по пину в текущем кадре</summary>
public static bool GetButtonDown(ButtonPinMapping button) => source.GetButtonDown(button);
/// <summary>Проверяет отпускание кнопки по пину в текущем кадре</summary>
public static bool GetButtonUp(ButtonPinMapping button) => source.GetButtonUp(button);
/// <summary>Проверяет состояние кнопки по числовому индексу пина</summary>
public static bool GetButton(int pinindex) => source.GetButton(pinindex);
/// <summary>Проверяет нажатие кнопки по индексу пина в текущем кадре</summary>
public static bool GetButtonDown(int pinindex) => source.GetButtonDown(pinindex);
/// <summary>Проверяет отпускание кнопки по индексу пина в текущем кадре</summary>
public static bool GetButtonUp(int pinindex) => source.GetButtonUp(pinindex);
// ================================================================================
// СТАТИЧЕСКИЕ МЕТОДЫ ДЛЯ РАБОТЫ С АБСТРАКТНЫМИ ДЕЙСТВИЯМИ ИГРОКОВ
// ================================================================================
/// <summary>Проверяет состояние действия для любого из игроков</summary>
public static bool GetButtonAnyPlayer(PlayerAction button) => source.GetButtonAnyPlayer(button);
/// <summary>Проверяет начало действия любым игроком в текущем кадре</summary>
public static bool GetButtonDownAnyPlayer(PlayerAction button) => source.GetButtonDownAnyPlayer(button);
/// <summary>Проверяет завершение действия любым игроком в текущем кадре</summary>
public static bool GetButtonUpAnyPlayer(PlayerAction button) => source.GetButtonUpAnyPlayer(button);
/// <summary>Проверяет состояние действия для текущего активного игрока</summary>
public static bool GetButtonCurrentPlayer(PlayerAction button) => source.GetButtonCurrentPlayer(button);
/// <summary>Проверяет начало действия текущим игроком в текущем кадре</summary>
public static bool GetButtonDownCurrentPlayer(PlayerAction button) => source.GetButtonDownCurrentPlayer(button);
/// <summary>Проверяет завершение действия текущим игроком в текущем кадре</summary>
public static bool GetButtonUpCurrentPlayer(PlayerAction button) => source.GetButtonUpCurrentPlayer(button);
}

View File

@ -1,117 +0,0 @@
/// <summary>
/// Абстрактный базовый класс для всех источников ввода в системе.
/// Определяет унифицированный интерфейс для работы с аппаратными кнопками и виртуальными действиями игроков.
/// </summary>
/// <remarks>
/// Наследники этого класса реализуют конкретные способы ввода:
/// - COMinput: работа с периферийной платой через COM-порт
/// - KeyboardInput: эмуляция ввода с клавиатуры для тестирования
///
/// Класс предоставляет три уровня абстракции:
/// 1. Низкоуровневый доступ к физическим пинам
/// 2. Доступ к абстрактным игровым действиям
/// 3. Мультиплеерные методы для работы с несколькими игроками
/// </remarks>
public abstract class InputSource
{
/// <summary>
/// Инициализирует источник ввода. Должен быть вызван перед использованием других методов.
/// </summary>
/// <returns>true если инициализация прошла успешно, false в случае ошибки</returns>
public abstract bool Initialize();
//----------------------------------------------------------------------------------
/// <summary>
/// Получает текущее состояние конкретной кнопки по её физическому пину.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>true если кнопка нажата в текущий момент</returns>
public abstract bool GetButton(ButtonPinMapping button);
/// <summary>
/// Проверяет, была ли кнопка нажата в начале текущего кадра.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>true только в первом кадре после нажатия кнопки</returns>
public abstract bool GetButtonDown(ButtonPinMapping button);
/// <summary>
/// Проверяет, была ли кнопка отпущена в начале текущего кадра.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>true только в первом кадре после отпускания кнопки</returns>
public abstract bool GetButtonUp(ButtonPinMapping button);
//----------------------------------------------------------------------------------
/// <summary>
/// Получает состояние кнопки по числовому индексу пина.
/// Перегрузка для работы с системами, где количество пинов превышает размер перечисления.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-based)</param>
/// <returns>true если кнопка нажата в текущий момент</returns>
public abstract bool GetButton(int pinIndex);
/// <summary>
/// Проверяет нажатие кнопки по числовому индексу пина в текущем кадре.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-based)</param>
/// <returns>true только в первом кадре после нажатия</returns>
public abstract bool GetButtonDown(int pinIndex);
/// <summary>
/// Проверяет отпускание кнопки по числовому индексу пина в текущем кадре.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-based)</param>
/// <returns>true только в первом кадре после отпускания</returns>
public abstract bool GetButtonUp(int pinIndex);
//----------------------------------------------------------------------------------
/// <summary>
/// Получает состояние игрового действия для любого из игроков.
/// Используется для мультиплеерных сценариев где важно действие любого игрока.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие активно у первого ИЛИ второго игрока</returns>
public abstract bool GetButtonAnyPlayer(PlayerAction button);
/// <summary>
/// Проверяет начало игрового действия любым игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие начато любым игроком в этом кадре</returns>
public abstract bool GetButtonDownAnyPlayer(PlayerAction button);
/// <summary>
/// Проверяет завершение игрового действия любым игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие завершено любым игроком в этом кадре</returns>
public abstract bool GetButtonUpAnyPlayer(PlayerAction button);
//----------------------------------------------------------------------------------
/// <summary>
/// Получает состояние игрового действия для текущего активного игрока.
/// Используется для одиночной игры или поочередного управления.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие активно у текущего игрока</returns>
public abstract bool GetButtonCurrentPlayer(PlayerAction button);
/// <summary>
/// Проверяет начало игрового действия текущим игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие начато текущим игроком в этом кадре</returns>
public abstract bool GetButtonDownCurrentPlayer(PlayerAction button);
/// <summary>
/// Проверяет завершение игрового действия текущим игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие завершено текущим игроком в этом кадре</returns>
public abstract bool GetButtonUpCurrentPlayer(PlayerAction button);
}

View File

@ -1,175 +0,0 @@
using UnityEngine;
/// <summary>
/// Реализация системы ввода с клавиатуры для тестирования и разработки.
/// Эмулирует работу периферийной платы через стандартные клавиши клавиатуры.
/// </summary>
/// <remarks>
/// Соответствие клавиш и пинов:
/// - Игрок 1: WASD + Space
/// - Игрок 2: Стрелки + RightControl
/// - Служебные: 1, 2, 3, F5
/// - Датчики хопера: O, P
/// </remarks>
public class KeyboardInput : InputSource
{
private int currentPlayer => gm.currentPlayer == CellState.Player1 ? 1 : 2;//ПЕРЕОПРЕДЕЛИТЕ ОПРЕДЕЛЕНИЕ ТЕКУЩЕГО ИГРОКА ПОД СВОЮ СИСТЕМУ.
private GameManager gm;//я получаю кто сейчас ходит через этого менеджера. Если у вас другое, допишите.
/// <summary>Проверяет состояние кнопки по её физическому пину через клавиатуру</summary>
public override bool GetButton(ButtonPinMapping button) => Input.GetKey(getKeyCode(button));
/// <summary>Проверяет нажатие кнопки по пину в текущем кадре через клавиатуру</summary>
public override bool GetButtonDown(ButtonPinMapping button) => Input.GetKeyDown(getKeyCode(button));
/// <summary>Проверяет отпускание кнопки по пину в текущем кадре через клавиатуру</summary>
public override bool GetButtonUp(ButtonPinMapping button) => Input.GetKeyUp(getKeyCode(button));
//----------------------------------------------------------------------------------
/// <summary>Проверяет состояние кнопки по числовому индексу пина через клавиатуру</summary>
public override bool GetButton(int pinIndex) => Input.GetKey(getKeyCode(pinIndex));
/// <summary>Проверяет нажатие кнопки по индексу пина в текущем кадре через клавиатуру</summary>
public override bool GetButtonDown(int pinIndex) => Input.GetKeyDown(getKeyCode(pinIndex));
/// <summary>Проверяет отпускание кнопки по индексу пина в текущем кадре через клавиатуру</summary>
public override bool GetButtonUp(int pinIndex) => Input.GetKeyUp(getKeyCode(pinIndex));
//----------------------------------------------------------------------------------
/// <summary>Проверяет состояние действия для текущего игрока (всегда игрок 1 для клавиатуры)</summary>
public override bool GetButtonCurrentPlayer(PlayerAction button) => GetButton(GetButtonForCurrentPlayer(button));
/// <summary>Проверяет начало действия текущим игроком в текущем кадре</summary>
public override bool GetButtonDownCurrentPlayer(PlayerAction button) => GetButtonDown(GetButtonForCurrentPlayer(button));
/// <summary>Проверяет завершение действия текущим игроком в текущем кадре</summary>
public override bool GetButtonUpCurrentPlayer(PlayerAction button) => GetButtonUp(GetButtonForCurrentPlayer(button));
//----------------------------------------------------------------------------------
/// <summary>Проверяет состояние действия для любого из игроков (игрок 1 ИЛИ игрок 2)</summary>
public override bool GetButtonAnyPlayer(PlayerAction button) => CheckBothPlayers(button, GetButton);
/// <summary>Проверяет начало действия любым игроком в текущем кадре</summary>
public override bool GetButtonDownAnyPlayer(PlayerAction button) => CheckBothPlayers(button, GetButtonDown);
/// <summary>Проверяет завершение действия любым игроком в текущем кадре</summary>
public override bool GetButtonUpAnyPlayer(PlayerAction button) => CheckBothPlayers(button, GetButtonUp);
//----------------------------------------------------------------------------------
/// <summary>
/// Инициализирует клавиатурный ввод. Всегда возвращает true так как клавиатура всегда доступна.
/// </summary>
/// <returns>Всегда true</returns>
public override bool Initialize() => gm = GameManager.instance;
// ================================================================================
// ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ
// ================================================================================
/// <summary>
/// Определяет кнопку текущего игрока для указанного действия.
/// Для клавиатуры всегда возвращает кнопку первого игрока.
/// </summary>
private ButtonPinMapping GetButtonForCurrentPlayer(PlayerAction button)
{
var buttons = _buttonByAction(button);
return currentPlayer == 1 ? buttons.player1button : buttons.player2button;
}
/// <summary>
/// Проверяет действие для обоих игроков используя указанную функцию проверки.
/// </summary>
private bool CheckBothPlayers(PlayerAction action, System.Func<ButtonPinMapping, bool> buttonFunc)
{
var buttons = _buttonByAction(action);
return buttonFunc(buttons.player1button) || buttonFunc(buttons.player2button);
}
/// <summary>
/// Возвращает соответствие между игровыми действиями и физическими кнопками игроков.
/// </summary>
private (ButtonPinMapping player1button, ButtonPinMapping player2button) _buttonByAction(PlayerAction playerAction) =>
playerAction switch
{
PlayerAction.up => (ButtonPinMapping.up1, ButtonPinMapping.up2),
PlayerAction.down => (ButtonPinMapping.down1, ButtonPinMapping.down2),
PlayerAction.left => (ButtonPinMapping.left1, ButtonPinMapping.left2),
PlayerAction.right => (ButtonPinMapping.right1, ButtonPinMapping.right2),
PlayerAction.action => (ButtonPinMapping.action1, ButtonPinMapping.action2),
PlayerAction.extra => (ButtonPinMapping.extra, ButtonPinMapping.extra),
_ => throw new System.ArgumentException($"Unknown player action: {playerAction}")
};
/// <summary>
/// Преобразует физический пин в соответствующую клавишу клавиатуры.
/// </summary>
private KeyCode getKeyCode(ButtonPinMapping button)
{
return button switch
{
ButtonPinMapping.up1 => KeyCode.W,
ButtonPinMapping.right1 => KeyCode.D,
ButtonPinMapping.down1 => KeyCode.S,
ButtonPinMapping.left1 => KeyCode.A,
ButtonPinMapping.action1 => KeyCode.Space,
ButtonPinMapping.up2 => KeyCode.UpArrow,
ButtonPinMapping.right2 => KeyCode.RightArrow,
ButtonPinMapping.down2 => KeyCode.DownArrow,
ButtonPinMapping.left2 => KeyCode.LeftArrow,
ButtonPinMapping.action2 => KeyCode.RightControl,
ButtonPinMapping.startOnePlayer => KeyCode.Alpha1,
ButtonPinMapping.startTwoPlayers => KeyCode.Alpha2,
ButtonPinMapping.secret => KeyCode.F5,
ButtonPinMapping.extra => KeyCode.Alpha3,
ButtonPinMapping.hasPrize => KeyCode.O,
ButtonPinMapping.prizeGiven => KeyCode.P,
_ => KeyCode.None,
};
}
/// <summary>
/// Преобразует числовой индекс пина в соответствующую клавишу клавиатуры.
/// Используется для методов с прямым доступом по индексу.
/// </summary>
private KeyCode getKeyCode(int pinIndex)
{
return pinIndex switch
{
0 => KeyCode.W, // up1
1 => KeyCode.D, // right1
2 => KeyCode.S, // down1
3 => KeyCode.A, // left1
4 => KeyCode.Space, // action1
5 => KeyCode.UpArrow, // up2
6 => KeyCode.RightArrow, // right2
7 => KeyCode.DownArrow, // down2
8 => KeyCode.LeftArrow, // left2
9 => KeyCode.RightControl, // action2
10 => KeyCode.Alpha1, // startOnePlayer
11 => KeyCode.Alpha2, // startTwoPlayers
12 => KeyCode.F5, // secret
13 => KeyCode.Alpha3, // extra
14 => KeyCode.O, // hasPrize
15 => KeyCode.P, // prizeGiven
_ => KeyCode.None, // невалидный индекс
};
}
}

View File

@ -1,19 +0,0 @@
/// <summary>
/// Абстрактные игровые действия для управления игроком.
/// Обеспечивает независимость от физического расположения кнопок.
/// </summary>
public enum PlayerAction
{
/// <summary>Вверх</summary>
up,
/// <summary>Вниз</summary>
down,
/// <summary>Влево</summary>
left,
/// <summary>Вправо</summary>
right,
/// <summary>Основное действие</summary>
action,
/// <summary>Дополнительное действие(ВНИМАНИЕ РАБОТАЕТ У ОБОИХ ИГРОКОВ ОДНОВРЕМЕННО)</summary>
extra
}

View File

@ -1 +0,0 @@
{"product":"RRLicensedApp","hwid_sha256":"9f67ba1daae5c0d237588a2f32dbba2a38ca763ce44898c2722e3edaa4a3ae6d","fingerprint":"d675d82f7d58bf105dd381d028ff66f776bdd7881d535e483bd1e80aaf6c3c97","expiry":0,"nonce":"I1onNRUkvn8TybL3h5Q7dAt1KIj/wJ7WSPy3LLgzAvU","issued":"2025-11-05T09:14:02.2416908Z","features":["full"],"sig":"XHEz/NOCSeIErht17gtSoYxPNyF2OeawtLHO3lqkARxf1r4rYOiIALA8tybqE/BE7w4/bB\u002B2AIHDwMyyAXVh6Q=="}