Commit as of 2026-03-05 11:30:58 by
This commit is contained in:
parent
e57c0d4b1d
commit
833ec4ca6f
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup />
|
||||
</Project>
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN // Исключите редко используемые компоненты из заголовков Windows
|
||||
// Файлы заголовков Windows
|
||||
#include <windows.h>
|
||||
@ -1,5 +0,0 @@
|
||||
// pch.cpp: файл исходного кода, соответствующий предварительно скомпилированному заголовочному файлу
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
// При использовании предварительно скомпилированных заголовочных файлов необходим следующий файл исходного кода для выполнения сборки.
|
||||
@ -1,13 +0,0 @@
|
||||
// pch.h: это предварительно скомпилированный заголовочный файл.
|
||||
// Перечисленные ниже файлы компилируются только один раз, что ускоряет последующие сборки.
|
||||
// Это также влияет на работу IntelliSense, включая многие функции просмотра и завершения кода.
|
||||
// Однако изменение любого из приведенных здесь файлов между операциями сборки приведет к повторной компиляции всех(!) этих файлов.
|
||||
// Не добавляйте сюда файлы, которые планируете часто изменять, так как в этом случае выигрыша в производительности не будет.
|
||||
|
||||
#ifndef PCH_H
|
||||
#define PCH_H
|
||||
|
||||
// Добавьте сюда заголовочные файлы для предварительной компиляции
|
||||
#include "framework.h"
|
||||
|
||||
#endif //PCH_H
|
||||
@ -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>
|
||||
Binary file not shown.
Binary file not shown.
@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
@ -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\|
|
||||
Binary file not shown.
Binary file not shown.
@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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(';');
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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 //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
public enum InputSourceType
|
||||
{
|
||||
COM,
|
||||
Keyboard
|
||||
}
|
||||
@ -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, // невалидный индекс
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
Binary file not shown.
@ -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=="}
|
||||
Loading…
x
Reference in New Issue
Block a user