commit f895d27a7c8e5465151d1adf0eb0a1c307726fa0 Author: Danil Iarosh Date: Wed Nov 5 09:15:15 2025 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1036bd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.obsidian/ \ No newline at end of file diff --git a/GreatGamedevVault/CHANGELOG.md b/GreatGamedevVault/CHANGELOG.md new file mode 100644 index 0000000..13a013b --- /dev/null +++ b/GreatGamedevVault/CHANGELOG.md @@ -0,0 +1,38 @@ +Парни, если что то поменяли/добавили/удалили - отмечаемся {Дата} - {Изменение} + +## Сентябрь 2025 +- 09.09.25 \_sega + - Хранилище создано + - Добавлена папка с организационными инструкциями + - [[Организационная структура]] + - [[Стандарты]] + - Добавил папку [Приколы](file:///B:/!ФайлОбменник!/Юнити_Разработка/GreatGamedevVault/Приколы(фичи)) (для инструкций и приколов) + - Добавил инструкцию [[Ссылка на папку вне хранилища]] +- 11.09.2025 + - \_sega + - Добавлена папка [Библиотека](file:///B:/!ФайлОбменник!/Юнити_Разработка/Библиотека) + - Инструкция [[Добавление файла в библиотеку]] + - Добавил в карантин [[AudioManager]] + - [[AudioManager|Док]] + - [Файл](file:///"B:/!ФайлОбменник!/Юнити_Разработка/Библиотека/Карантин/AudioManager.cs") + - Dis_Bro + - Добавил файл [[README]] +- 12.09.2025 Dis_Bro + - Дописал начальную версию файла [[README]]. При измениий инфы, свзанной с ним, вносите изменеия и туда + - Сделал [[README]] домашней страницей хранилища. Открывается в режиме чтения +- 15.09.2025 Dis_Bro + - Добавил в карантин [[GameManager]] + - [[GameManager|Док]] + - [Файл](file:///"B:/!ФайлОбменник!/Юнити_Разработка/Библиотека/Карантин/GameManager.cs") +- 16.09.2025 \_sega + - Добавил папку 2. Периферия + скрипт ComWrapper + - Соответственно юзаем его когда надо обрабатывать монетоприемник, хопер или инпуты с периферийной платы + +## Октябрь 2025 +- 23.10.2025 Dis_Bro + - Добавил файл [[COMInput (бывш. ComEnjoyer 🍆)]] by \_sega + - Обновил инфу по внешним ссылкам в файле [[Ссылка на папку вне хранилища]] + - Переписал все ссылки под работающий формат + + + diff --git a/GreatGamedevVault/README.md b/GreatGamedevVault/README.md new file mode 100644 index 0000000..ccbeaee --- /dev/null +++ b/GreatGamedevVault/README.md @@ -0,0 +1,48 @@ +### Общая информация +[[CHANGELOG]] - дата изменения: `$= moment(dv.page("CHANGELOG").file.mtime.ts).format("hh:mm, DD MMM YYYY")` + +>[!info]- Внесение изменений в CHANGELOG +>Для внесения Изменений по новой дате в CHANGELOG можно пользоваться шаблоном: `changelog_template` при нажатии `Alt + T`. +>В CHANGELOG вносятся все изменения, связанные с этим хранилищем + +Для переключения режимов (чтение/редактирование) используйте комбинацию клавиш `Ctrl + E` + +### Ссылки хранилища +#### Билды +[Ссылка на билды фишки](file:///B:/!ФайлОбменник!/Юнити_Разработка/Builds/Conect4_builds) +[Ссылка на билды промика](file:///B:/!ФайлОбменник!/Юнити_Разработка/Builds/Promo_builds) +[Ссылка на билды морского боя](file:///B:/!ФайлОбменник!/Юнити_Разработка/Builds/Sea_battle_Cviat) +### Организация +Сведения об организационной деятельностьи [[Организационная структура|в общем]] и в частности. +```dataview +LIST WITHOUT ID FROM "Организация" SORT file.ctime ASC +``` + +### Фичи +Полезные гайды для этого хранилища и Obsidian в целом. +```dataview +LIST WITHOUT ID FROM "Приколы(фичи)" SORT file.ctime ASC +``` + +### Библиотека скриптов +Общая библиотека с reusable-скриптами. +Состоит из нескольких разделов: +- [[#Карантин|Карантин]] +- [[#Менеджеры|Менеджеры]] +- [[#Периферия]] + +##### Карантин +```dataview +LIST WITHOUT ID FROM "Библиотека скриптов/0. Карантин" SORT file.ctime ASC +``` + +##### Менеджеры +```dataview +LIST WITHOUT ID FROM "Библиотека скриптов/1. Менеджеры" SORT file.ctime ASC +``` + +##### Периферия + +```dataview +LIST WITHOUT ID FROM "Библиотека скриптов/2. Периферия" SORT file.ctime ASC +``` \ No newline at end of file diff --git a/GreatGamedevVault/attachments/Thumbs.db b/GreatGamedevVault/attachments/Thumbs.db new file mode 100644 index 0000000..cb5f7ba Binary files /dev/null and b/GreatGamedevVault/attachments/Thumbs.db differ diff --git a/GreatGamedevVault/attachments/Компонент ComWrapper.cs.png b/GreatGamedevVault/attachments/Компонент ComWrapper.cs.png new file mode 100644 index 0000000..bcf5e6e Binary files /dev/null and b/GreatGamedevVault/attachments/Компонент ComWrapper.cs.png differ diff --git a/GreatGamedevVault/attachments/Компонент In.cs.png b/GreatGamedevVault/attachments/Компонент In.cs.png new file mode 100644 index 0000000..b970c8f Binary files /dev/null and b/GreatGamedevVault/attachments/Компонент In.cs.png differ diff --git a/GreatGamedevVault/attachments/Компонент PrizeSystem.png b/GreatGamedevVault/attachments/Компонент PrizeSystem.png new file mode 100644 index 0000000..f6a63f3 Binary files /dev/null and b/GreatGamedevVault/attachments/Компонент PrizeSystem.png differ diff --git a/GreatGamedevVault/attachments/Схема кнопок в системе инпута.png b/GreatGamedevVault/attachments/Схема кнопок в системе инпута.png new file mode 100644 index 0000000..7d2cd6a Binary files /dev/null and b/GreatGamedevVault/attachments/Схема кнопок в системе инпута.png differ diff --git a/GreatGamedevVault/obs_templates/changelog_template.md b/GreatGamedevVault/obs_templates/changelog_template.md new file mode 100644 index 0000000..fc9c2be --- /dev/null +++ b/GreatGamedevVault/obs_templates/changelog_template.md @@ -0,0 +1,3 @@ +- {{date:DD.MM.YYYY}} + - Changes... + \ No newline at end of file diff --git a/GreatGamedevVault/Библиотека скриптов/0. Карантин/AudioManager.md b/GreatGamedevVault/Библиотека скриптов/0. Карантин/AudioManager.md new file mode 100644 index 0000000..96bd1f9 --- /dev/null +++ b/GreatGamedevVault/Библиотека скриптов/0. Карантин/AudioManager.md @@ -0,0 +1,273 @@ +by: \_sega + +[Карантин линк](file:///"B:/!ФайлОбменник!/Юнити_Разработка/Библиотека/Карантин/AudioManager.cs") + +Минималистичный аудио-менеджер для 2D/3D-проектов на Unity: один синглтон-объект, не уничтожается при смене сцен, умеет мгновенно запускать музыку, кроссфейдить треки, мягко ставить музыку на паузу/снимать её, а также воспроизводить SFX через пул аудиоисточников (2D и 3D). + +--- +## 🔧 Возможности + +- Синглтон `AudioManager.Instance` c `DontDestroyOnLoad` + +- Музыка: + + - мгновенный старт трека + + - плавная смена треков (кроссфейд A→B) + + - мягкая пауза/возврат с фейдами (на **unscaled** времени) + +- SFX: + + - пул источников SFX фиксированного размера + + - воспроизведение 2D SFX + + - воспроизведение 3D SFX в мировой позиции (spatialBlend, min/maxDistance) + + - round-robin выбор свободного источника, «steal», если все заняты + +- Интеграция с `AudioMixerGroup` для музыки и SFX (по желанию) + +- Автосоздание недостающих дочерних объектов: + + - `MusicSource`, `TempMusicSource`, `SFX Pool` + + +--- + +## 📦 Требования и зависимости + +- Unity (любой современный LTS) +- Пространства имён: + - `UnityEngine`, `UnityEngine.Audio` +- Скрипт живёт в `namespace Managers` + + +--- + +## 🧱 Рекомендуемая структура в иерархии + +``` +AudioManager (root, с этим скриптом) [DontDestroyOnLoad] +├── MusicSource (AudioSource, loop=true, 2D) +├── TempMusicSource (AudioSource, loop=true, 2D, volume=0) +└── SFX Pool (empty transform) + ├── SFX_00 (AudioSource, loop=false) + ├── SFX_01 + └── ... (размер пула настраивается) +``` + +> Если этих объектов нет, менеджер создаст их сам. + +--- + +## 🚀 Быстрый старт + +### 1) Добавьте объект с компонентом `Managers.AudioManager` в какую-нибудь сцену (например, стартовую). + +### 2) Вызовите инициализацию **один раз** на старте приложения + +> В скрипте `Awake()` закомментирован. Нужно вызвать `Initialize()` вручную. + +```csharp +using Managers; +using UnityEngine; + +public class Bootstrap : MonoBehaviour +{ + [SerializeField] private AudioManager audioManager; // перетащите сюда объект из сцены + + void Awake() + { + if (audioManager != null) + audioManager.Initialize(); // создаст пул и подготовит источники + } +} +``` + +### 3) Воспроизведение музыки и звуков + +```csharp +// Мгновенно запустить музыку +AudioManager.Instance.PlayMusicImmediate(menuClip); + +// Плавно сменить трек (кроссфейд) +AudioManager.Instance.CrossfadeMusic(gameplayClip, duration: 1.2f); + +// Поставить музыку на мягкую паузу и затем вернуть +AudioManager.Instance.SmoothPauseMusic(true, 0.5f); // fade-out+Pause +AudioManager.Instance.SmoothPauseMusic(false, 0.5f); // ПЛАВНО ПРОДОЛЖАЕМ ИГРАТЬ+fade-in + +// 2D SFX +AudioManager.Instance.PlaySfx(clickClip, volume: 0.9f, pitch: 1.0f); + +// 3D SFX в мире (например, взрыв) +AudioManager.Instance.PlaySfxAt(explosionClip, position: hitPoint, volume: 1f, pitch: 1f, spatialBlend: 1f); +``` + +--- + +## ⚙️ Настройки компонента + +|Поле|Назначение|Примечание| +|---|---|---| +|**musicSource**|Основной источник музыки|Найдётся/создастся автоматически| +|**tempMusicSource**|Временный источник для кроссфейда|Найдётся/создастся автоматически| +|**musicMixerGroup**|Группа микшера для музыки|Необязательно| +|**musicTargetVolume**|Целевая громкость музыки после фейдов (0–1.5)|По умолчанию `1.0`| +|**defaultFadeDuration**|Длительность фейда по умолчанию (сек)|По умолчанию `0.8`| +|**sfxPoolSize**|Размер пула SFX источников|По умолчанию `8`| +|**sfxPoolParent**|Родительский объект для SFX источников|Создаётся как `SFX Pool`, если отсутствует| +|**sfxMixerGroup**|Группа микшера для SFX|Необязательно| +|**sfxDefaultVolume**|Базовая громкость SFX (0–1.5)|Умножается на `volume` вызова| + +--- + +## 🧩 Публичное API + +### `PlayMusicImmediate(AudioClip clip, bool loop = true, float targetVolume = -1f)` + +Мгновенно запускает музыку без фейда. + +- `targetVolume < 0` → используется `musicTargetVolume`. + + +### `CrossfadeMusic(AudioClip newClip, float duration = -1f, bool loop = true, float newTargetVolume = -1f)` + +Плавно сменяет текущий трек на `newClip` за `duration` секунд. + +- `duration < 0` → `defaultFadeDuration` + +- `newTargetVolume < 0` → `musicTargetVolume` + +- Работает на `Time.unscaledDeltaTime` — фейды не завязаны на `Time.timeScale`. + + +### `SmoothPauseMusic(bool pause, float duration = -1f)` + +Мягкая пауза/возврат: + +- `pause == true` → fade-out к 0 + `AudioSource.Pause()` + +- `pause == false` → `UnPause()` + fade-in к `musicTargetVolume` + `duration < 0` → `defaultFadeDuration`. + + +### `PlaySfx(AudioClip clip, float volume = 1f, float pitch = 1f)` + +Проигрывает 2D SFX из пула. +Фактическая громкость: `Clamp(volume * sfxDefaultVolume, 0..1.5)`. + +### `PlaySfxAt(AudioClip clip, Vector3 position, float volume = 1f, float pitch = 1f, float spatialBlend = 1f)` + +Проигрывает 3D SFX в мировой позиции: + +- `spatialBlend` клампится в `[0..1]` + +- Для 3D проставляются `rolloffMode=Linear`, `minDistance=1`, `maxDistance=20` (можно поменять в коде) + + +--- + +## 🗂️ Как работает пул SFX + +- При инициализации: + - ищутся существующие `AudioSource` внутри `SFX Pool` и добавляются в пул; + - недостающее количество создаётся до `sfxPoolSize`. +- При воспроизведении: + - сначала ищется **свободный** источник, проход начинается с `sfxRoundRobinIndex`; + - если все заняты — берётся следующий по кругу (**steal**) и прерывает его текущий звук; + - `sfxRoundRobinIndex` сдвигается, балансируя нагрузку. + +--- + +## 🎛️ Работа с AudioMixer + +Установите `musicMixerGroup` и/или `sfxMixerGroup`, чтобы: +- управлять громкостью/эквализацией группой, а не по источникам; +- использовать экспонированные параметры (в дБ). + +**Пример управления экспонированным параметром громкости (дБ):** + +```csharp +[SerializeField] private AudioMixer masterMixer; +// параметр должен быть Exposed, например "MusicVolume" + +public void SetMusicDb(float db) // например, -80..0 +{ + masterMixer.SetFloat("MusicVolume", db); +} + +// утилита пересчёта линейного значения (0..1) в dB +public static float LinearToDb01(float x) +{ + return Mathf.Log10(Mathf.Clamp(x, 0.0001f, 1f)) * 20f; +} +``` + +> **Важно:** Внутренние фейды `AudioManager` меняют `volume` источников, а не параметры микшера. Для «мастер-громкости» используйте микшер. + +--- + +## 🧠 Поведение и тонкости + +- **Синглтон:** при наличии дубликата — лишний объект удалится; основной переживёт смену сцен. + +- **Инициализация:** требуется **однократный** вызов `Initialize()` (например, из `Awake` в вашем Bootstrap). + +- **Unscaled время:** кроссфейды и фейды на паузу/возврат используют `Time.unscaledDeltaTime`, чтобы работать даже при `Time.timeScale = 0`. + +- **Loop:** музыкальные источники настроены `loop = true` по умолчанию. + +- **2D по умолчанию:** музыка и SFX создаются как 2D (`spatialBlend=0`), 3D включается в `PlaySfxAt`. + +- **Остановка корутин:** перед новым фейдом/кроссфейдом старые корутины корректно останавливаются. + + +--- + +## 🧪 Частые сценарии (шаблоны) + +### Пауза игры (заморозить время + мягко приглушить музыку) + +```csharp +Time.timeScale = 0f; +AudioManager.Instance.SmoothPauseMusic(true, 0.4f); // fade-out & Pause +``` + +### Возврат из паузы + +```csharp +AudioManager.Instance.SmoothPauseMusic(false, 0.4f); // UnPause & fade-in +Time.timeScale = 1f; +``` + +### Переход из меню в бой с кроссфейдом + +```csharp +AudioManager.Instance.CrossfadeMusic(battleClip, duration: 1.0f); +``` + +### Проигрывание UI-клика + +```csharp +AudioManager.Instance.PlaySfx(uiClickClip, volume: 0.8f); +``` + +### Взрыв в мире + +```csharp +AudioManager.Instance.PlaySfxAt(explosionClip, hitPoint, volume: 1f, spatialBlend: 1f); +``` + +--- + +## 📚 Краткая шпаргалка + +- Инициализация: `audioManager.Initialize();` +- Музыка сразу: `PlayMusicImmediate(clip);` +- Кроссфейд: `CrossfadeMusic(clip, 1.0f);` +- Пауза/возврат: `SmoothPauseMusic(true/false, 0.5f);` +- 2D звук: `PlaySfx(clip);` +- 3D звук: `PlaySfxAt(clip, position);` diff --git a/GreatGamedevVault/Библиотека скриптов/0. Карантин/GameManager.md b/GreatGamedevVault/Библиотека скриптов/0. Карантин/GameManager.md new file mode 100644 index 0000000..5d32302 --- /dev/null +++ b/GreatGamedevVault/Библиотека скриптов/0. Карантин/GameManager.md @@ -0,0 +1,175 @@ +by: Dis_Bro + +[Карантин линк](file:///"B:/!ФайлОбменник!/Юнити_Разработка/Библиотека/Карантин/GameManager.cs") + +Глобальный менеджер игры для Unity 2D: синглтон-объект, сохраняется между сценами, управляет жизнями и монетами, загружает уровни и сцены, спавнит игрока, ставит игру на паузу и генерирует события для UI. + +--- +## 🔧 Возможности + +- Синглтон `GameManager.Instance` с `DontDestroyOnLoad` +- Игровой прогресс: + - миры, уровни (`World`, `Stage`, `StagesByWorld`) + - количество жизней и монет + - авто-добавление жизни за монеты +- Управление игроком: + - поиск и спавн из `PlayerSpawn` + - загрузка префаба через `Resources` или инспектор + - событие `PlayerSpawned` +- Сцены: + - загрузка сцен по имени + - уровни в формате `"World-Stage"` + - загрузка UI-слоя `"Level UI"` поверх уровня + - проверка наличия сцены в билд-настройках +- Пауза: + - `SetPause` и `TogglePause` + - остановка времени и `AudioListener` + - событие `PauseStateChanged` +- События для UI: + - изменение количества монет (`CoinsChanged`) + - изменение паузы (`PauseStateChanged`) + +--- +## 📦 Требования и зависимости + +- Unity (любой современный LTS) +- Пространства имён: + - `UnityEngine`, `UnityEngine.SceneManagement` +- Скрипт живёт в `namespace RR.Game2D.Core` + +--- +## 🧱 Рекомендуемая структура в иерархии + +``` +[GameManager] (создаётся автоматически, DontDestroyOnLoad) +└── (ничего не требуется — префабы игрока ищутся через инспектор/Resources) +``` + +> Если объекта `GameManager` нет — он будет создан автоматически через `Bootstrap`. + +--- +## 🚀 Быстрый старт + +### 1) Подключите GameManager +`GameManager` создастся сам при старте приложения. + +### 2) Подпишитесь на события +```csharp +GameManager.Instance.CoinsChanged += c => uiCoins.text = c.ToString(); +GameManager.Instance.PauseStateChanged += p => pauseMenu.SetActive(p); +``` + +### 3) Управляйте игрой +```csharp +// Начать новую игру +GameManager.Instance.NewGame(); + +// Добавить монету +GameManager.Instance.AddCoin(); + +// Поставить на паузу +GameManager.Instance.TogglePause(); +``` + +--- +## ⚙️ Настройки компонента + +|Поле|Назначение|Примечание| +|---|---|---| +|**playerPrefab**|Префаб игрока|Если не указан, ищется в `Resources`| +|**playerPrefabResourcePath**|Путь к префабу в `Resources`|По умолчанию `"Prefabs/Player"`| + +--- +## 🧩 Публичное API + +### `NewGame()` +Начинает игру заново: жизни = 3, монеты = 0, загрузка уровня `1-1`. + +### `GameOver()` +Окончание игры. +**По умолчанию:** загрузка `"MainMenu"`. +Можно заменить, например, на вызов экрана результатов или рестарт. + +### `LoadLevel(int world, int stage)` +Загружает сцену `"{world}-{stage}"`. +Если сцены нет: +```csharp +// Можно заменить на другое поведение +print("WIN"); +LoadScene("MainMenu", false); +``` + +### `LoadScene(string sceneName, bool useUI = true)` +Загружает сцену по имени. При `useUI=true` добавляет `"Level UI"`. + +### `NextLevel()` +Переходит к следующему уровню/миру. + +### `ResetLevel()` +Перезапускает уровень, отнимая жизнь и сбрасывая монеты. + +### `AddCoin()` +Добавляет монету. По умолчанию: +1 жизнь за 100 монет. +Пример кастомизации: +1 жизнь каждые 50 монет или обмен монет на очки. + +### `AddLife()` +Добавляет жизнь. +Пример кастомизации: ограничение по максимуму или вызов VFX/SFX. + +### `SetPause(bool pause)` +Ставит/снимает паузу (`Time.timeScale` и `AudioListener.pause`). + +### `TogglePause()` +Переключает состояние паузы. Можно расширить вызовом UI. + +### `PlayerSpawned` (event) +Событие: игрок заспавнен. + +### `CoinsChanged` (event) +Событие: монеты изменились. + +--- +## 🧠 Поведение и тонкости + +- `GameManager` создаётся автоматически при старте (через `Bootstrap`). +- Если в сцене есть объект с тегом `"Player"`, он удаляется и заменяется новым. +- Игрок спавнится в точке `PlayerSpawn`, если она есть. +- Методы с пометкой *«Можно заменить на другое поведение»* легко кастомизируются. + +--- +## 🧪 Частые сценарии + +### Запуск новой игры +```csharp +GameManager.Instance.NewGame(); +``` + +### Переход на следующий уровень +```csharp +GameManager.Instance.NextLevel(); +``` + +### Добавление монет +```csharp +GameManager.Instance.AddCoin(); +``` + +### Сброс уровня +```csharp +GameManager.Instance.ResetLevel(); +``` + +### Пауза/возврат +```csharp +GameManager.Instance.TogglePause(); +``` + +--- +## 📚 Краткая шпаргалка + +- Начало игры: `NewGame()` +- Сброс уровня: `ResetLevel()` +- Следующий уровень: `NextLevel()` +- Пауза/возврат: `TogglePause()` / `SetPause(bool)` +- Монеты: `AddCoin()` +- Жизни: `AddLife()` diff --git a/GreatGamedevVault/Библиотека скриптов/2. Периферия/COMInput (бывш. ComEnjoyer 🍆).md b/GreatGamedevVault/Библиотека скриптов/2. Периферия/COMInput (бывш. ComEnjoyer 🍆).md new file mode 100644 index 0000000..89f3973 --- /dev/null +++ b/GreatGamedevVault/Библиотека скриптов/2. Периферия/COMInput (бывш. ComEnjoyer 🍆).md @@ -0,0 +1,244 @@ +by: \_sega + +> [!info]- 🚷Зачем оно? +>Чтобы билдить IL2CPP и не терять контакт с периферийной платой потому что IL2CPP не имеет SerialPort. То ли он его отрезает то ли разрабы все предусмотрели🤡 + +[Линки в библиотеке](file:///B:/!ФайлОбменник!/Юнити_Разработка/Библиотека/Периферия/UnityToEsp): +1. [Плагин](file:///B:/!ФайлОбменник!/Юнити_Разработка/Библиотека/Периферия/UnityToEsp/Assets/Plugins/x86_64) +2. [Ассеты плагина](file:///B:/!ФайлОбменник!/Юнити_Разработка/Библиотека/Периферия/UnityToEsp/Assets/Scripts) +# Иструкция по внедрению + +## Шаг I. Добавление файлов в проект + +Для работы с этим набором скриптов нужно +1. Добавить все скрипты папки Inputs в свой проект. В идеале добавить папку целиком. +2. В папку `Assets\Plugins\x86_64` поместить ComNative.dll + +## Шаг II. Необходимые доработки скриптов под свой проект + +Везде где используется `DebugWindow.Message` можно заменить на `Debug.Log` или кастомный логописец. Или выпилить + +### ComNativeWrapper.cs +==Не трогаем.== +_Это bridge между Unity и c++._ +Он импортирует нам функции из dll + +### ComWrapper.cs +метод ==Send(LightMode lightMode)== +Содержит вложенную функцию _getLightString_. Ее нужно доработать под свои световые сигналы. +Так же внести коррективы в `enum LightMode`. Он в этом же файле + +метод ==Pay== +Здесь есть моя проверка на то, включена ли бесплатная игра. Нужно добавить свою, либо если не предполагается игры без оплаты, то убрать этот блок + +В строке +```cs +h = ComNativeWrapper.Serial_Open(port, 115200, 8, 0, 1, 5, 10); +``` +Можно заменить 5 на большее/меньшее число. +Этот параметр - ==readTimeout==. +Сколько программа ждет пока придет хоть какой-то инпут перед тем как пойти дальше. +Сейчас все работает синхронно, то есть этот мув буквально добавляет каждому кадру задержку 5мс. +Для 60fps нужно 16.6мс. +То есть при задержке 5мс нужно чтобы ваша игра успевала рендерить кадр за 11.6мс. Если лагает, можно поставить 3-4мс. +==Слишком маленький таймаут => плохо обрабатывается множественный приход монет== например с вендисты. + +### PrizeSystem.cs +метод ==GivePrize== +```cs +bool settingEnabled = SettingsManager.instance.GetSettingValue("Выдача призов при игре с ботом") == "Выдавать"; +``` +Проверка настройки выдачи приза. +Тут надо или кастомную или выпилить + +### COMInput +```cs +private int currentPlayer => gm.currentPlayer == CellState.Player1 ? 1 : 2; +private GameManager gm; +``` +В этом свойстве __нужно дать понять как определять первый или второй игрок сейчас управляет__. Нужно возвращать 1(первый игрок) или 2(второй игрок) +Это нужно чтобы корректно работали методы с пометкой CurrentPlayer +Если игрок 1, можно всегда возвращать 1. +GameManager можно выпилить если у вас другой способ определения активного игрока. + +## Шаг III. Добавление скриптов на сцену +Для работы нужно добавить на сцену +- ComWrapper.cs +- In.cs +- PrizeSystem.cs + +Я добавил их на тот же объект, где скрипт-entry point, чтобы проще получить их и проинициализировать. + +## Шаг IV. Настройка в инспекторе + +### ComWrapper.cs +![[Компонент ComWrapper.cs.png]] +SaveMoney - Сохранять монеты после перезапуска игры. +Если выключить каждый перезапуск сбрасывает монеты. + +### In.cs +![[Компонент In.cs.png]] +Тут выбираем с чего получать ввод. +В рантайме не переключается. +Перед билдом на аппарат ставим "COM" +Для тестов "Keyboard" + +### PrizeSystem.cs +![[Компонент PrizeSystem.png]] +Тут просто звук ошибки выдачи вставить. + +## Шаг V. Инициализация +Чтобы все стартануло надо проинициализировать систему. Да не просто, а в правильном порядке: +```cs +GetComponent().Initialize(); +GetComponent().Initialize(); +GetComponent().Initialize(); +``` + +## Шаг VI. Применение + +### Получение ввода +```cs +//Проверка конкретной кнопки +In.GetButton(ButtonPinMapping.right1) //и другие значение перечисления +In.GetButtonDown(ButtonPinMapping.right1) //и другие значение перечисления +In.GetButtonUp(ButtonPinMapping.right1) //и другие значение перечисления + +//Проверка действия с любого джойстика +GetButtonAnyPlayer(PlayerAction.right) +GetButtonDownAnyPlayer(PlayerAction.right) +GetButtonUpAnyPlayer(PlayerAction.right) + +//Проверка действия с джойстика текущего игрока +GetButtonCurrentPlayer(PlayerAction.right) +GetButtonDownCurrentPlayer(PlayerAction.right) +GetButtonUpCurrentPlayer(PlayerAction.right) +``` +Вот общая схема подключения кнопок +![[Схема кнопок в системе инпута.png]] + +Дополнительно можно подписаться на событие получения ввода. Откликается на любое изменение состояния кнопок и датчиков хоппера + +Метод-подписчик дожен иметь параметр string, в него передается строка инпутов. +```cs +ComWrapper.onInputRecieved.AddListener(MYMETHOD); +``` +не забываем отписывать метод когда объект не работает. + +### Работа с выдачей приза +Чтобы отдать приз надо вызвать +```cs +PrizeSystem.GivePrize(); +``` +Метод статический, вызовется из любого места. + +### Работа с монетами +#### Приход монеты +Система автоматически считывает приход монеты и пишет себе в приватную переменную +При приходе монеты вызывается событие на которое можно подписаться +```cs +ComWrapper.onCoinAdded.AddListener(MYMETHOD) +``` + +метод-подписчик должен иметь параметр int. В него приходит количество монет после пополнения. +Можно обновлять текст-счетчик с помощью этой темы. + +Так же система пишет в PlayerPrefs сколько монет у игрока +```cs +CreditsCount++; +PlayerPrefs.SetInt("CoinsCount", CreditsCount); +``` +По ключу "CoinsCount" можно вытащить актуальное количество монет + +Если очень хочется добавить монету откуда то, можно вызвать +```cs +ComWrapper.AddCredit() +``` + +Для удобства тестов можно завести такой скрипт +```cs +public class PaymentEmulator : MonoBehaviour +{ + public KeyCode key; + void Update() + { + if (Input.GetKeyDown(key)) ComWrapper.AddCredit(); + } +} +``` + +#### Списание монеты/Оплата +Чтобы проверить оплату и писать монету есть вот этот метод. +```cs +ComWrapper.Pay(); +``` +Он проверяет можем ли мы списать монету (хватает или нет) +- Если хватает -> Списываем возвращаем `true` +- Не хватает -> Возвращаем false + +Это можно использовать в проверке типа +```cs +if (ComWrapper.Pay()) +{ + LoadNextLevel();//условно +} +else +{ + PlayErrorSound();//условно +} +``` + +При вызове Pay() происходит событие списания монеты, в котором передается int - количество монет после списания. +Пример применения +```cs +void Start() => + ComWrapper.onCoinRemoved.AddListener(Refresh_Counter); +void OnDisable() => + ComWrapper.onCoinRemoved.RemoveListener(Refresh_Counter); +void OnApplicationQuit() => + ComWrapper.onCoinRemoved.RemoveListener(Refresh_Counter); + +[SerializeField] TextMeshProUGUI Counter; //текст-счетчик монет +void Refresh_Counter(int newCount) => + Counter.text = newCount.ToString(); +``` + +### Работа со статистикой +в PrizeSystem есть набор строк +```cs +public static string key_starts = "STARTS"; +public static string key_startsNul = "STARTSNUL"; +public static string key_prizesgiven = "PRIZESGIVEN"; +public static string key_prizesgivenNul = "PRIZESGIVENNUL"; +``` + +Это есть ключи для PlayerPrefs. +1. Количество запусков +2. Количество запусков обнуляемое +3. Выданных призов +4. Выданных призов обнуляемое + +Эти данные система сама пишет. +Можно по этим ключам получать сколько призов выдано и сколько списаний монеток произошло. +```cs +PlayerPrefs.GetInt(PrizeSystem.key_starts, 0);//0 если еще не записывался ключ +``` + +Для обнуляемых если попросят сделать обнуление обнуляйте ключи с пометкой Nul. + + + + +### Работа с подсветкой + +```cs +ComWrapper.Send(LightMode.Idle); +``` + +если нужны другие сигналы, лезем в ComWrapper, дописываем в перечисление нужный сигнал, в функции getLightString добавляем в switchCase строку которую Тимур скажет + +### Отправка строкового сигнала +Фиг знает зачем оно вам надо вообще но если надо +```cs +ComWrapper.Send("ТокарьБанановый"); +``` \ No newline at end of file diff --git a/GreatGamedevVault/Организация/Добавление файла в библиотеку.md b/GreatGamedevVault/Организация/Добавление файла в библиотеку.md new file mode 100644 index 0000000..8d8bc21 --- /dev/null +++ b/GreatGamedevVault/Организация/Добавление файла в библиотеку.md @@ -0,0 +1,48 @@ +1. Сначала добавляемый файл отправляется в папку [Карантин](file:///B:/!ФайлОбменник!/Юнити_Разработка/Библиотека/Карантин) +2. В эту базу пишется ридмишка со ссылкой на файл (что умеет, как работает, микро документация) +3. Добавленный файл подвергается жестокой критике. +4. Если все норм, файл едет в папку [RR](file:///B:/!ФайлОбменник!/Юнити_Разработка/Библиотека/RR) +5. Файл распределяется в подходящую папку (По неймспейсу) + +==Убедительная просьба.== +Давайте распределять файлы сразу по неймспейсам. +Это поможет избежать ошибок и переписывания кода если имена пересекутся с основным проектом. + +--- + +``` +[временный текст пока документ в разработке] + +нужна общая примерная структура неймспейсов, предлагаю разработать. + +sega: Сейчас хочу добавить хорошего аудио менеджера (AudioManager.cs) +Далее могут быть разработаны другие полезные менеджеры типа менеджер партикл систем. Пусть будет namespace Managers для этого. Да, придется подключать его там, где нужно использовать. Если кому то это не понравится можно в самом скрипте снести неймспейс. + +Dis_Bro: Пока набросал минимальную структуру, от неё можно отталкиваться + + + + +``` + +```mermaid +flowchart LR + +A[RR] --> AA[Game2D] +A --> AB[Game3D] +A --> AC[Common] + +AA --> AAA[Core] +AA --> AAB[Behavoiur] +AA --> AAC[Utilities] + +AB --> ABA[Core] +AB --> ABB[Behavoiur] +AB --> ABC[Utilities] + +AC --> ACA[Mangers] +AC --> ACB[...] +AC --> ACC[...] + + +``` \ No newline at end of file diff --git a/GreatGamedevVault/Организация/Организационная структура.md b/GreatGamedevVault/Организация/Организационная структура.md new file mode 100644 index 0000000..3299db1 --- /dev/null +++ b/GreatGamedevVault/Организация/Организационная структура.md @@ -0,0 +1,42 @@ +# Ораганизация +- Наличие тимлида + - Распределение приоритетов + - Отслеживание сроков + - Код-ревью понеобходимости + - Связь с начальством +- Гит + - Заливать и хранить на гите объемные ассеты долго и неудобно. + - Пока разработка у каждого индивидуальная вопрос не слишком актуален +- Папка на пандоре + - Хороший переиспользуемый код. Обязательно README. + - Билды по своим папкам + - Закупленные или стыренные ассеты + README. +- Запланированные на неделю мероприятия +- Использование Obsidian для отслеживания выполненных задач (упрощает вспоминание "что делали" когда приходит время отчитываться) + +___ +# Стандарты и правила +- Общий [[Стандарты]] +- Общая [[Добавление файла в библиотеку|библиотека]] для складирования полезного переиспользуемого кода (менеджеры, паттерны, префабы, реюзабл механики) +- Архитектура программы + - Для индивидуальной разработки полагаемся на мнение чата, делаем как удобно + - Коллективная разработка: Сначала проектируем на схемах, вместе мозгуем, потом следуем плану. + +___ +# Недельный цикл работы + +==Понедельник== +- Планерка + - Разбираем задачи с пятницы + - Расставляем приоритеты + +==Вторник-Четверг== +- Фокус на разработку +- При завершении задачи добавляем отработанную задачу в пул "на проверку" +- Внеплановые собрания для помощи по необходимости + +==Пятница== +- Собираем билды +- Проводим плейтесты +- Собрать информацию о текущем состоянии проектов +- Составляем обратную связь -> генерируем задачи на следующую неделю diff --git a/GreatGamedevVault/Организация/Стандарты.md b/GreatGamedevVault/Организация/Стандарты.md new file mode 100644 index 0000000..6059fe0 --- /dev/null +++ b/GreatGamedevVault/Организация/Стандарты.md @@ -0,0 +1,81 @@ +# 1. Именование и стиль C-sharp + +- **PascalCase**: классы, интерфейсы, методы, свойства, события. +- **camelCase**: локальные переменные и параметры (spawnPoint, speed). +- **\_camelCase**: приватные поля. +- **UPPER_SNAKE_CASE**: const и static readonly. +- **Namespaces**: Company.Product.Module → BobotailX.Gameplay.Combat. +- **Один класс = один файл**. Публичные типы в отдельных файлах. +- **var** — для очевидных типов, иначе явный тип. + +- Для автоформатирования периодически юзаем Ctrl+KD в VS + +# 2. Unity-специфичные штуки + +- Поля в инспекторе: \[SerializeField] private + public get (инкапсуляция лучше, чем public). +- Компоненты кешируем в Awake/OnEnable (не дергать `GetComponent` каждый кадр). +- Минимизируем работу в `Update()`; используем события, корутины. +- В Input/Physics использовать `Time.[fixed]deltaTime`, не «магические» коэффициенты. +- Не использовать `FindObjectOfType`/`Find` в рантайме + - Использовать DI, ссылки через инспектор, фабрики, бутстрапы. +- Конфиги — **ScriptableObject** (баланс, таблицы параметров). +- Сцены: + - **MainBootstrap** сцена с уникальным Bootstrap скриптом для загрузки менеджеров-синглтонов, существующих всю игру (GameManager, ContextManager, AudioManager) + - Локальные Bootstrap'ы для сцен по необходимости + +>Bootstrap'ы - реализация паттерна Entry Point. Удобно управлять порядком загрузки, чтобы не долбаться с настройкой Execution Order'а когда менеджеров >1 и зависимости усложняются + +# 3. Структура скрипта (порядок элементов) + +1. const/static readonly +2. serialized поля +3. приватные поля (неконфигурируемые) +4. свойства/события +5. **Unity Messages** (Awake, OnEnable, Start, Update, …) +6. публичные методы API +7. приватные методы/вспомогательные +8. вложенные типы + +# 4. Организация папок проекта +Assets/ + Scripts/ + Art/ + Models/ + Textures/ + Materials/ + Sprites/ + Animations/ + object1/ + animator + clips + object2/ + ... + Audio/ + Music/ + SFX/ + Prefabs/ + Scenes/ + UI/ + ThirdParty/ + Все что накачали/ + +# 5. Аннотации инспектора +Да, лишний прописывать их тратит время, но инспектор становитсяя красивым, удобным и читаемым. +- Header +- Tooltip +- Range +- Min +- Max +- Delayed +- RequireComponent + +# 6. Документация и комментарии +- XML-комментарии для публичного API. + ```cs + /// + /// Воспроизводит звуковой эффект. + /// + /// Аудио-клип для воспроизведения. + /// Мировая позиция источника звука. + ``` +- Комментарии «зачем», а не «что». \ No newline at end of file diff --git a/GreatGamedevVault/Приколы(фичи)/Ссылка на папку вне хранилища.md b/GreatGamedevVault/Приколы(фичи)/Ссылка на папку вне хранилища.md new file mode 100644 index 0000000..ec70abc --- /dev/null +++ b/GreatGamedevVault/Приколы(фичи)/Ссылка на папку вне хранилища.md @@ -0,0 +1,14 @@ +``` +[Ссылка на билды фишки](file:///B:/!ФайлОбменник!/Юнити_Разработка/Builds/Conect4_builds) +``` + +Живой пример: +[Ссылка на билды фишки](file:///B:/!ФайлОбменник!/Юнити_Разработка/Builds/Conect4_builds) + +Вот так делается ссылыка на папку. Можно юзать, когда добавляем какие-либо ассеты на диск. + +>[!warning] Пробелы в пути обязательно заменяем на %20 + +> [!BUG]- Лучше переделывать все ссылки под формат из примера! +> Это связано с тем, что в ссылках формата `B:\!ФайлОбменник!\Юнити_Разработка\Builds\Conect4_builds` (Стандартные для винды) Obsidian по умолчанию распознаёт бэкслеш `\` как начало escape-последовательности, и из-за этого в режиме чтения ссылка просто не сможет открыться +> ==Поэтому усиленно меняем бэкслеш `\` в ссылочках на слеш `/`== diff --git a/Библиотека/Карантин/AudioManager.cs b/Библиотека/Карантин/AudioManager.cs new file mode 100644 index 0000000..c84c7ee --- /dev/null +++ b/Библиотека/Карантин/AudioManager.cs @@ -0,0 +1,345 @@ +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 sfxPool = new List(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(); + } + if (tempMusicSource == null) + { + var t = transform.Find("TempMusicSource"); + if (t != null) tempMusicSource = t.GetComponent(); + } + + 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(); + 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(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(); + 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 ==== + + /// + /// Мгновенно запустить музыку (без фейда). + /// + 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; + } + + /// + /// 2) Плавно сменить музыку (кроссфейд A->B). + /// + 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)); + } + + /// + /// 1) Плавно приостановить музыку (для паузы). + /// pause=true: плавный фейд к 0 и Pause(). pause=false: UnPause() и плавный фейд к musicTargetVolume. + /// + 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; + } + } + + /// + /// 3) Воспроизвести SFX из пула (2D). + /// + 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); + } + + /// + /// 3) Воспроизвести SFX из пула в мире (3D). + /// + 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(); + } + } +} \ No newline at end of file diff --git a/Библиотека/Карантин/GameManager.cs b/Библиотека/Карантин/GameManager.cs new file mode 100644 index 0000000..d344430 --- /dev/null +++ b/Библиотека/Карантин/GameManager.cs @@ -0,0 +1,306 @@ +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 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 PlayerSpawned; + + public event Action CoinsChanged; + + //--------------------------- + // Singleton + //--------------------------- + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Bootstrap() + { + if (Instance == null) + { + var go = new GameObject(nameof(GameManager)); + Instance = go.AddComponent(); + 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(); + // } + //} + + /// + /// Поиск префаба игрока. + /// Если не найдет префаб из инспектора, ищет в "Resources" + /// + /// Префаб игрока + private GameObject ResolvePlayerPrefab() + { + if (playerPrefab != null) return playerPrefab; + + var loaded = Resources.Load(playerPrefabResourcePath); + if (loaded == null) + { + Debug.LogError( + $"GameManager: playerPrefab is null. Resources.Load('{playerPrefabResourcePath}') вернул null. " + + $"Ожидается файл: Assets/Resources/{playerPrefabResourcePath}.prefab (точное совпадение регистра)."); + } + return loaded; + } + + /// + /// Начало новой игры. + /// Загружает первый уровень. + /// Устанавливает жизни на 3, монеты на 0 + /// + public void NewGame() + { + Lives = 3; + Coins = 0; + CoinsChanged?.Invoke(Coins); + LoadLevel(1, 1); + } + + // Можно заменить на другое поведение + /// + /// Окончание игры. + /// Загружает сцену главного меню + /// + public void GameOver() => LoadScene("MainMenu", false); + // -- + + /// + /// Загрузка уровня по данным. + /// Загружает уровень по номерам мира и уровня + /// + /// Номер мира + /// Номер уровня + 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); + // -- + } + } + + /// + /// Загрузка сцены по названию. + /// Вызывает одноименный метод в SceneManager. + /// Может загружать сцены с UI и без. + /// В сценах без UI игрок не спавнится + /// + /// Строковое название сцены + /// Использовать ли UI + 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]); + } + } + + /// + /// Метод для спавна игрока. + /// Ищет первый спавнпоинт, спавнит игрока на его позиции. + /// Если на карте есть объект игрока, удаляет его и спавнит свой + /// + private void TrySpawnPlayerForActiveScene() + { + if (!_shouldSpawnPlayer) return; + PlayerSpawn spawn = FindFirstObjectByType(); + 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); + } + + + /// + /// Метод переход на следующий уровень. + /// Может загружать уровни и миры в прямой зависимости от + /// Количества уровней на мир(StagesByWorld) + /// + public void NextLevel() + { + if (Stage >= StagesByWorld) + { + World++; + Stage = 1; + } + else Stage++; + LoadLevel(World, Stage); + } + + /// + /// Метод сброса уровня. + /// При сбросе также сбрасывает монеты и отнимает жизни + /// + public void ResetLevel() + { + Lives--; + + Coins = 0; + CoinsChanged?.Invoke(Coins); + + //print($"Lives: {lives}"); + if (Lives > 0) LoadLevel(World, Stage); + else GameOver(); + } + + /// + /// Метод добавления монеты + /// + public void AddCoin() + { + Coins++; + // Можно заменить на другое поведение + if (Coins >= 100) + { + Coins = 0; + AddLife(); + } + // -- + CoinsChanged?.Invoke(Coins); + } + + /// + /// Метод паузы игры. + /// Ставит на паузу все SFX и саму игру + /// + /// Ставить ли на паузу + public void SetPause(bool pause) + { + if (IsPaused == pause) return; + IsPaused = pause; + + Time.timeScale = pause ? 0f : 1f; + AudioListener.pause = pause; + + PauseStateChanged?.Invoke(IsPaused); + } + + // Можно заменить на другое поведение + /// + /// Метод переключателя паузы + /// + public void TogglePause() => SetPause(!IsPaused); + // -- + + // Можно заменить на другое поведение + /// + /// Метод для добавления жизни. + /// + public void AddLife() => Lives++; + // -- + + /// + /// Метод проверки сцены на существование в настройках билда + /// + /// Строковое название сцены + /// + 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; + } + } +} \ No newline at end of file diff --git a/Библиотека/Периферия/UnityToEsp/Assets/Plugins/x86_64/ComNative.dll b/Библиотека/Периферия/UnityToEsp/Assets/Plugins/x86_64/ComNative.dll new file mode 100644 index 0000000..e0f6d6b Binary files /dev/null and b/Библиотека/Периферия/UnityToEsp/Assets/Plugins/x86_64/ComNative.dll differ diff --git a/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ButtonPinMapping.cs b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ButtonPinMapping.cs new file mode 100644 index 0000000..9b5e564 --- /dev/null +++ b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ButtonPinMapping.cs @@ -0,0 +1,43 @@ +/// +/// Соответствие между кнопками игрового ввода и цифровыми выходами периферийной платы (пины 0-15). +/// Содержит mapping для управления двух игроков, служебных кнопок и датчиков хопера. +/// +public enum ButtonPinMapping +{ + /// Кнопка движения вверх для первого игрока + up1 = 0, + /// Кнопка движения вправо для первого игрока + right1 = 1, + /// Кнопка движения вниз для первого игрока + down1 = 2, + /// Кнопка движения влево для первого игрока + left1 = 3, + /// Действие/огонь для первого игрока + action1 = 4, + + /// Кнопка движения вверх для второго игрока + up2 = 5, + /// Кнопка движения вправо для второго игрока + right2 = 6, + /// Кнопка движения вниз для второго игрока + down2 = 7, + /// Кнопка движения влево для второго игрока + left2 = 8, + /// Действие/огонь для второго игрока + action2 = 9, + + /// Запуск игры для одного игрока + startOnePlayer = 10, + /// Запуск игры для двух игроков + startTwoPlayers = 11, + /// Секретная/служебная кнопка для захода в настройки + secret = 12, + + /// Дополнительная/резервная кнопка + extra = 13, + + /// Датчик наличия приза в хопере + hasPrize = 14, + /// Датчик выдачи приза из хопера + prizeGiven = 15 +} diff --git a/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/COMinput.cs b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/COMinput.cs new file mode 100644 index 0000000..d4e3694 --- /dev/null +++ b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/COMinput.cs @@ -0,0 +1,253 @@ +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) + + /// + /// Получает состояние конкретной кнопки по её физическому пину. + /// Возвращает true если кнопка в данный момент нажата. + /// + /// Пин кнопки на периферийной плате + /// True если кнопка нажата в текущем кадре + public override bool GetButton(ButtonPinMapping button) => buttonState[(int)button]; + + /// + /// Проверяет, была ли кнопка нажата в начале текущего кадра. + /// Удерживает true до тех пор, пока кто то не проверит, затем сбрасывает. + /// + /// Пин кнопки на периферийной плате + /// True если кнопка была нажата в этом кадре + public override bool GetButtonDown(ButtonPinMapping button) => ConsumeButtonEvent(buttonDown, button); + + /// + /// Проверяет, была ли кнопка отпущена в начале текущего кадра. + /// Удерживает true до тех пор, пока кто то не проверит, затем сбрасывает. + /// + /// Пин кнопки на периферийной плате + /// True если кнопка была отпущена в этом кадре + public override bool GetButtonUp(ButtonPinMapping button) => ConsumeButtonEvent(buttonUp, button); + + + /// + /// Получает состояние кнопки по числовому индексу пина напрямую из буфера состояний. + /// Обходит перечисление ButtonPinMapping для прямого доступа к пинам по индексу. + /// + /// Числовой индекс пина (0-15) + /// true если кнопка нажата в текущий момент + /// + /// Используется когда необходим прямой доступ к пинам без приведения к перечислению. + /// Подходит для сценариев где количество пинов может превышать размер перечисления. + /// + public override bool GetButton(int pinIndex) => buttonState[pinIndex]; + + /// + /// Проверяет, была ли кнопка нажата в начале текущего кадра по числовому индексу пина. + /// Возвращает true только в первом кадре после нажатия и сбрасывает флаг события. + /// + /// Числовой индекс пина (0-15) + /// true если кнопка была нажата в этом кадре + /// + /// Метод "потребляет" событие - после чтения флаг сбрасывается. + /// Использует общий механизм ConsumeButtonEvent для обработки одноразовых событий. + /// + public override bool GetButtonDown(int pinIndex) => ConsumeButtonEvent(buttonDown, pinIndex); + + /// + /// Проверяет, была ли кнопка отпущена в начале текущего кадра по числовому индексу пина. + /// Возвращает true только в первом кадре после отпускания и сбрасывает флаг события. + /// + /// Числовой индекс пина (0-15) + /// true если кнопка была отпущена в этом кадре + /// + /// Метод "потребляет" событие - после чтения флаг сбрасывается. + /// Использует общий механизм ConsumeButtonEvent для обработки одноразовых событий. + /// + public override bool GetButtonUp(int pinIndex) => ConsumeButtonEvent(buttonUp, pinIndex); + // ------------------------------------------------------------------------------------ + //ЭТА ГРУППА МЕТОДОВ ПОЗВОЛЯЕТ УЗНАТЬ СОСТОЯНИЯ КНОПОК АКТИВНОГО ИГРОКА (проверка по enum PlayerAction) + //Служебные кнопки типа startOnePlayer, startTwoPlayers, secret, extra, hasPrize, prizeGiven здесь не проверяются + + /// + /// Получает состояние действия для текущего игрока. + /// Возвращает true если действие активно (кнопка нажата) у текущего игрока. + /// + /// Действие игрока (up, down, left, right, action, extra) + /// True если действие активно у текущего игрока + public override bool GetButtonCurrentPlayer(PlayerAction button) + => GetButton(GetButtonForCurrentPlayer(button)); + + /// + /// Проверяет, было ли действие начато текущим игроком в этом кадре. + /// Возвращает true только в первом кадре после начала действия. + /// + /// Действие игрока + /// True если действие было начато в этом кадре + public override bool GetButtonDownCurrentPlayer(PlayerAction button) + => GetButtonDown(GetButtonForCurrentPlayer(button)); + + /// + /// Проверяет, было ли действие завершено текущим игроком в этом кадре. + /// Возвращает true только в первом кадре после завершения действия. + /// + /// Действие игрока + /// True если действие было завершено в этом кадре + public override bool GetButtonUpCurrentPlayer(PlayerAction button) + => GetButtonUp(GetButtonForCurrentPlayer(button)); + + // ------------------------------------------------------------------------------------ + //ЭТА ГРУППА МЕТОДОВ ПОЗВОЛЯЕТ УЗНАТЬ СОСТОЯНИЯ КНОПОК ЛЮБОГО ИГРОКА (проверка по enum PlayerAction) + //Служебные кнопки типа startOnePlayer, startTwoPlayers, secret, extra, hasPrize, prizeGiven здесь не проверяются + + /// + /// Получает состояние действия для любого из игроков. + /// Возвращает true если действие активно у первого ИЛИ второго игрока. + /// + /// Действие игрока + /// True если действие активно у любого игрока + public override bool GetButtonAnyPlayer(PlayerAction button) + => CheckBothPlayers(button, GetButton); + + /// + /// Проверяет, было ли действие начато любым игроком в этом кадре. + /// Возвращает true если первый ИЛИ второй игрок начали действие в этом кадре. + /// + /// Действие игрока + /// True если действие было начато любым игроком в этом кадре + public override bool GetButtonDownAnyPlayer(PlayerAction button) + => CheckBothPlayers(button, GetButtonDown); + + /// + /// Проверяет, было ли действие завершено любым игроком в этом кадре. + /// Возвращает true если первый ИЛИ второй игрок завершили действие в этом кадре. + /// + /// Действие игрока + /// True если действие было завершено любым игроком в этом кадре + public override bool GetButtonUpAnyPlayer(PlayerAction button) + => CheckBothPlayers(button, GetButtonUp); + #endregion + + + //==========ХЕЛПЕРЫ========== + // Проверка нажатия с обоих контроллеров игроков + private bool CheckBothPlayers(PlayerAction action, Func 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; + } + + /// + /// Вспомогательный метод для обработки одноразовых событий кнопок по числовому индексу. + /// Возвращает текущее значение флага и сбрасывает его для предотвращения повторной обработки. + /// + /// Буфер событий (buttonDown или buttonUp) + /// Числовой индекс пина + /// Состояние флага события до сброса + private bool ConsumeButtonEvent(bool[] buffer, int pinIndex) + { + bool result = buffer[pinIndex]; + buffer[pinIndex] = false; + return result; + } +} diff --git a/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ComSystem/ComNativeWrapper.cs b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ComSystem/ComNativeWrapper.cs new file mode 100644 index 0000000..eea135e --- /dev/null +++ b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ComSystem/ComNativeWrapper.cs @@ -0,0 +1,57 @@ +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(); + var sb = new StringBuilder(need); + int r = Serial_ListPorts(sb, sb.Capacity); + if (r < 0) return Array.Empty(); + string joined = sb.ToString(); + return string.IsNullOrWhiteSpace(joined) ? Array.Empty() : joined.Split(';'); + } +} diff --git a/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ComSystem/ComWrapper.cs b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ComSystem/ComWrapper.cs new file mode 100644 index 0000000..316df6c --- /dev/null +++ b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ComSystem/ComWrapper.cs @@ -0,0 +1,262 @@ +using System.Text; +using UnityEngine; +using UnityEngine.Events; + +/// +/// Основной класс-обертка для работы с COM-портом и периферийной платой. +/// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов. +/// +public class ComWrapper : MonoBehaviour +{ + //Настраиваем из инспектора + [SerializeField] bool saveMoneyBetweenSessions; + [SerializeField] bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true + + + public static UnityEvent onCoinAdded; + public static UnityEvent onCoinRemoved; + public static UnityEvent 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===========================// + + /// + /// Инициализирует систему работы с COM-портом. + /// Открывает соединение, настраивает события и запускает периодические запросы состояния. + /// + /// + /// Обязателен к вызову при старте приложения. Выполняет: + /// - Создание UnityEvent'ов для уведомлений + /// - Загрузку количества кредитов из PlayerPrefs + /// - Открытие COM-порта + /// - Запуск периодического опроса пинов (если включена проверка призов) + /// + public void Initialize() + { + //Создаем ивенты + onCoinAdded = new UnityEvent(); + onCoinRemoved = new UnityEvent(); + + //Начальная настройка + if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0); + CreditsCount = PlayerPrefs.GetInt("CoinsCount", 0); + initPort(); //вот это важный метод который собственно порт открывает. + + //Каждые 5 сек запрашиваем инпуты у платы. Надо потому что при заходе в игру с пинов еще не пришла инфа + if (enablePrizeChecking) InvokeRepeating("requestInputs", 0, 5); + } + + /// + /// Добавляет один кредит в систему и сохраняет обновленное количество. + /// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта. + /// + /// + /// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков. + /// Автоматически сохраняет состояние в PlayerPrefs. + /// + public static void AddCredit() + { + CreditsCount++; + PlayerPrefs.SetInt("CoinsCount", CreditsCount); + onCoinAdded?.Invoke(CreditsCount); + } + + /// + /// Отправляет произвольную строковую команду на периферийную плату через COM-порт. + /// + /// Команда для отправки + /// + /// Используется для отправки пользовательских команд на плату. + /// Команда должна быть в формате, ожидаемом прошивкой платы. + /// В случае ошибки отправки выводит предупреждение в DebugWindow. + /// + public static void Send(string message) + { + try + { + int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1); //w-количество записаных символов в порт. можно обработать + //Debug.Log("Wrote: " + w); + + } + catch { DebugWindow.Message("Ошибка отправки строкового сигнала."); } + } + + /// + /// Отправляет команду управления подсветкой на периферийную плату. + /// + /// Режим подсветки из перечисления LightMode + /// + /// Формирует команду вида "80XXX" где XXX - код режима подсветки. + /// Для добавления новых режимов необходимо обновить вложенный метод getLightString + добавить режим в перечисление. + /// + 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("Ошибка отправки светового сигнала."); } + } + + /// + /// Выполняет списание платы за запуск игры. + /// Проверяет наличие доступных кредитов или режим бесплатной игры. + /// При успешном списании увеличивает счетчики запусков. + /// + /// + /// true - если списание прошло успешно (бесплатный режим или есть кредиты), + /// false - если кредитов недостаточно и платный режим + /// + /// + /// В случае успеха: + /// - Увеличивает общие счетчики запусков (key_starts и key_startsNul) + /// - При платном режиме уменьшает количество кредитов на 1 + /// - Вызывает событие onCoinRemoved при списании кредита + /// + 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, +} \ No newline at end of file diff --git a/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ComSystem/PrizeSystem.cs b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ComSystem/PrizeSystem.cs new file mode 100644 index 0000000..d4d3ab4 --- /dev/null +++ b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/ComSystem/PrizeSystem.cs @@ -0,0 +1,114 @@ +using System.Collections; +using UnityEngine; + +/// +/// Система управления выдачей призов и ведения статистики игры. +/// Отвечает за процесс выдачи призов, подсчет запусков и наград. +/// +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===========================// + /// + /// Инициализирует систему призов и счетчики статистики. + /// Должен быть вызван один раз при запуске приложения. + /// + /// + /// Выполняет: + /// - Установку статического экземпляра (singleton) + /// - Инициализацию счетчиков в PlayerPrefs если они отсутствуют + /// - Подготовку системы к работе + /// + public void Initialize() + { + instance = this; + InitCounters(); + } + + /// + /// Запускает процесс выдачи физического приза игроку. + /// Проверяет настройки и текущее состояние перед началом выдачи. + /// + /// + /// Условия для выдачи приза: + /// - В настройках включена выдача призов при игре с ботом + /// - В данный момент не выполняется другая выдача приза + /// + /// Процесс выдачи: + /// - Включает мотор хопера + /// - Ожидает подтверждения выдачи от датчиков (10 сек таймаут) + /// - Выключает мотор по завершению + /// - Обновляет счетчики выданных призов + /// - Воспроизводит звук ошибки при неудаче + /// + 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 +} diff --git a/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/In.cs b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/In.cs new file mode 100644 index 0000000..db3c29a --- /dev/null +++ b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/In.cs @@ -0,0 +1,89 @@ +using UnityEngine; + +/// +/// Статический фасад для унифицированного доступа к системе ввода. +/// Предоставляет глобальный интерфейс для работы с управлением независимо от выбранного источника. +/// +/// +/// Этот класс является точкой входа для всей игровой логики, работающей с вводом. +/// Автоматически инициализирует выбранный источник управления (клавиатура или COM-порт) +/// и предоставляет статические методы для удобного доступа из любого места кода. +/// +/// Использование: +/// 1. Настройте тип ввода в инспекторе +/// 2. Вызовите Initialize() при старте игры +/// 3. Используйте статические методы для проверки ввода +/// +public class In : MonoBehaviour +{ + [Header("Выбор источника управления")] + [SerializeField] private InputSourceType inputType; + + static InputSource source; + + /// + /// Инициализирует систему ввода на основе выбранного типа. + /// Должен быть вызван один раз при запуске приложения. + /// + /// + /// Создает соответствующий экземпляр InputSource: + /// - KeyboardInput для тестирования на клавиатуре + /// - COMinput для работы с периферийной платой + /// + 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("Не удалось проинициалзировать источник управления."); + } + + // ================================================================================ + // СТАТИЧЕСКИЕ МЕТОДЫ ДЛЯ РАБОТЫ С ФИЗИЧЕСКИМИ ПИНАМИ + // ================================================================================ + + /// Проверяет состояние конкретной кнопки по её физическому пину + public static bool GetButton(ButtonPinMapping button) => source.GetButton(button); + + /// Проверяет нажатие кнопки по пину в текущем кадре + public static bool GetButtonDown(ButtonPinMapping button) => source.GetButtonDown(button); + + /// Проверяет отпускание кнопки по пину в текущем кадре + public static bool GetButtonUp(ButtonPinMapping button) => source.GetButtonUp(button); + + /// Проверяет состояние кнопки по числовому индексу пина + public static bool GetButton(int pinindex) => source.GetButton(pinindex); + + /// Проверяет нажатие кнопки по индексу пина в текущем кадре + public static bool GetButtonDown(int pinindex) => source.GetButtonDown(pinindex); + + /// Проверяет отпускание кнопки по индексу пина в текущем кадре + public static bool GetButtonUp(int pinindex) => source.GetButtonUp(pinindex); + + // ================================================================================ + // СТАТИЧЕСКИЕ МЕТОДЫ ДЛЯ РАБОТЫ С АБСТРАКТНЫМИ ДЕЙСТВИЯМИ ИГРОКОВ + // ================================================================================ + + /// Проверяет состояние действия для любого из игроков + public static bool GetButtonAnyPlayer(PlayerAction button) => source.GetButtonAnyPlayer(button); + + /// Проверяет начало действия любым игроком в текущем кадре + public static bool GetButtonDownAnyPlayer(PlayerAction button) => source.GetButtonDownAnyPlayer(button); + + /// Проверяет завершение действия любым игроком в текущем кадре + public static bool GetButtonUpAnyPlayer(PlayerAction button) => source.GetButtonUpAnyPlayer(button); + + /// Проверяет состояние действия для текущего активного игрока + public static bool GetButtonCurrentPlayer(PlayerAction button) => source.GetButtonCurrentPlayer(button); + + /// Проверяет начало действия текущим игроком в текущем кадре + public static bool GetButtonDownCurrentPlayer(PlayerAction button) => source.GetButtonDownCurrentPlayer(button); + + /// Проверяет завершение действия текущим игроком в текущем кадре + public static bool GetButtonUpCurrentPlayer(PlayerAction button) => source.GetButtonUpCurrentPlayer(button); +} \ No newline at end of file diff --git a/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/InputSource.cs b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/InputSource.cs new file mode 100644 index 0000000..d8d9323 --- /dev/null +++ b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/InputSource.cs @@ -0,0 +1,117 @@ +/// +/// Абстрактный базовый класс для всех источников ввода в системе. +/// Определяет унифицированный интерфейс для работы с аппаратными кнопками и виртуальными действиями игроков. +/// +/// +/// Наследники этого класса реализуют конкретные способы ввода: +/// - COMinput: работа с периферийной платой через COM-порт +/// - KeyboardInput: эмуляция ввода с клавиатуры для тестирования +/// +/// Класс предоставляет три уровня абстракции: +/// 1. Низкоуровневый доступ к физическим пинам +/// 2. Доступ к абстрактным игровым действиям +/// 3. Мультиплеерные методы для работы с несколькими игроками +/// +public abstract class InputSource +{ + /// + /// Инициализирует источник ввода. Должен быть вызван перед использованием других методов. + /// + /// true если инициализация прошла успешно, false в случае ошибки + public abstract bool Initialize(); + + //---------------------------------------------------------------------------------- + + /// + /// Получает текущее состояние конкретной кнопки по её физическому пину. + /// + /// Пин кнопки на периферийной плате + /// true если кнопка нажата в текущий момент + public abstract bool GetButton(ButtonPinMapping button); + + /// + /// Проверяет, была ли кнопка нажата в начале текущего кадра. + /// + /// Пин кнопки на периферийной плате + /// true только в первом кадре после нажатия кнопки + public abstract bool GetButtonDown(ButtonPinMapping button); + + /// + /// Проверяет, была ли кнопка отпущена в начале текущего кадра. + /// + /// Пин кнопки на периферийной плате + /// true только в первом кадре после отпускания кнопки + public abstract bool GetButtonUp(ButtonPinMapping button); + + //---------------------------------------------------------------------------------- + + /// + /// Получает состояние кнопки по числовому индексу пина. + /// Перегрузка для работы с системами, где количество пинов превышает размер перечисления. + /// + /// Числовой индекс пина (0-based) + /// true если кнопка нажата в текущий момент + public abstract bool GetButton(int pinIndex); + + /// + /// Проверяет нажатие кнопки по числовому индексу пина в текущем кадре. + /// + /// Числовой индекс пина (0-based) + /// true только в первом кадре после нажатия + public abstract bool GetButtonDown(int pinIndex); + + /// + /// Проверяет отпускание кнопки по числовому индексу пина в текущем кадре. + /// + /// Числовой индекс пина (0-based) + /// true только в первом кадре после отпускания + public abstract bool GetButtonUp(int pinIndex); + + //---------------------------------------------------------------------------------- + + /// + /// Получает состояние игрового действия для любого из игроков. + /// Используется для мультиплеерных сценариев где важно действие любого игрока. + /// + /// Абстрактное игровое действие + /// true если действие активно у первого ИЛИ второго игрока + public abstract bool GetButtonAnyPlayer(PlayerAction button); + + /// + /// Проверяет начало игрового действия любым игроком в текущем кадре. + /// + /// Абстрактное игровое действие + /// true если действие начато любым игроком в этом кадре + public abstract bool GetButtonDownAnyPlayer(PlayerAction button); + + /// + /// Проверяет завершение игрового действия любым игроком в текущем кадре. + /// + /// Абстрактное игровое действие + /// true если действие завершено любым игроком в этом кадре + public abstract bool GetButtonUpAnyPlayer(PlayerAction button); + + //---------------------------------------------------------------------------------- + + /// + /// Получает состояние игрового действия для текущего активного игрока. + /// Используется для одиночной игры или поочередного управления. + /// + /// Абстрактное игровое действие + /// true если действие активно у текущего игрока + public abstract bool GetButtonCurrentPlayer(PlayerAction button); + + /// + /// Проверяет начало игрового действия текущим игроком в текущем кадре. + /// + /// Абстрактное игровое действие + /// true если действие начато текущим игроком в этом кадре + public abstract bool GetButtonDownCurrentPlayer(PlayerAction button); + + /// + /// Проверяет завершение игрового действия текущим игроком в текущем кадре. + /// + /// Абстрактное игровое действие + /// true если действие завершено текущим игроком в этом кадре + public abstract bool GetButtonUpCurrentPlayer(PlayerAction button); +} \ No newline at end of file diff --git a/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/InputSourceType.cs b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/InputSourceType.cs new file mode 100644 index 0000000..ad891ed --- /dev/null +++ b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/InputSourceType.cs @@ -0,0 +1,5 @@ +public enum InputSourceType +{ + COM, + Keyboard +} diff --git a/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/KeyboardInput.cs b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/KeyboardInput.cs new file mode 100644 index 0000000..94db5f2 --- /dev/null +++ b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/KeyboardInput.cs @@ -0,0 +1,175 @@ +using UnityEngine; + +/// +/// Реализация системы ввода с клавиатуры для тестирования и разработки. +/// Эмулирует работу периферийной платы через стандартные клавиши клавиатуры. +/// +/// +/// Соответствие клавиш и пинов: +/// - Игрок 1: WASD + Space +/// - Игрок 2: Стрелки + RightControl +/// - Служебные: 1, 2, 3, F5 +/// - Датчики хопера: O, P +/// +public class KeyboardInput : InputSource +{ + private int currentPlayer => gm.currentPlayer == CellState.Player1 ? 1 : 2;//ПЕРЕОПРЕДЕЛИТЕ ОПРЕДЕЛЕНИЕ ТЕКУЩЕГО ИГРОКА ПОД СВОЮ СИСТЕМУ. + private GameManager gm;//я получаю кто сейчас ходит через этого менеджера. Если у вас другое, допишите. + + + + /// Проверяет состояние кнопки по её физическому пину через клавиатуру + public override bool GetButton(ButtonPinMapping button) => Input.GetKey(getKeyCode(button)); + + /// Проверяет нажатие кнопки по пину в текущем кадре через клавиатуру + public override bool GetButtonDown(ButtonPinMapping button) => Input.GetKeyDown(getKeyCode(button)); + + /// Проверяет отпускание кнопки по пину в текущем кадре через клавиатуру + public override bool GetButtonUp(ButtonPinMapping button) => Input.GetKeyUp(getKeyCode(button)); + + //---------------------------------------------------------------------------------- + + /// Проверяет состояние кнопки по числовому индексу пина через клавиатуру + public override bool GetButton(int pinIndex) => Input.GetKey(getKeyCode(pinIndex)); + + /// Проверяет нажатие кнопки по индексу пина в текущем кадре через клавиатуру + public override bool GetButtonDown(int pinIndex) => Input.GetKeyDown(getKeyCode(pinIndex)); + + /// Проверяет отпускание кнопки по индексу пина в текущем кадре через клавиатуру + public override bool GetButtonUp(int pinIndex) => Input.GetKeyUp(getKeyCode(pinIndex)); + + //---------------------------------------------------------------------------------- + + /// Проверяет состояние действия для текущего игрока (всегда игрок 1 для клавиатуры) + public override bool GetButtonCurrentPlayer(PlayerAction button) => GetButton(GetButtonForCurrentPlayer(button)); + + /// Проверяет начало действия текущим игроком в текущем кадре + public override bool GetButtonDownCurrentPlayer(PlayerAction button) => GetButtonDown(GetButtonForCurrentPlayer(button)); + + /// Проверяет завершение действия текущим игроком в текущем кадре + public override bool GetButtonUpCurrentPlayer(PlayerAction button) => GetButtonUp(GetButtonForCurrentPlayer(button)); + + //---------------------------------------------------------------------------------- + + /// Проверяет состояние действия для любого из игроков (игрок 1 ИЛИ игрок 2) + public override bool GetButtonAnyPlayer(PlayerAction button) => CheckBothPlayers(button, GetButton); + + /// Проверяет начало действия любым игроком в текущем кадре + public override bool GetButtonDownAnyPlayer(PlayerAction button) => CheckBothPlayers(button, GetButtonDown); + + /// Проверяет завершение действия любым игроком в текущем кадре + public override bool GetButtonUpAnyPlayer(PlayerAction button) => CheckBothPlayers(button, GetButtonUp); + + //---------------------------------------------------------------------------------- + + /// + /// Инициализирует клавиатурный ввод. Всегда возвращает true так как клавиатура всегда доступна. + /// + /// Всегда true + public override bool Initialize() => gm = GameManager.instance; + + // ================================================================================ + // ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ + // ================================================================================ + + /// + /// Определяет кнопку текущего игрока для указанного действия. + /// Для клавиатуры всегда возвращает кнопку первого игрока. + /// + private ButtonPinMapping GetButtonForCurrentPlayer(PlayerAction button) + { + var buttons = _buttonByAction(button); + return currentPlayer == 1 ? buttons.player1button : buttons.player2button; + } + + /// + /// Проверяет действие для обоих игроков используя указанную функцию проверки. + /// + private bool CheckBothPlayers(PlayerAction action, System.Func buttonFunc) + { + var buttons = _buttonByAction(action); + return buttonFunc(buttons.player1button) || buttonFunc(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 System.ArgumentException($"Unknown player action: {playerAction}") + }; + + /// + /// Преобразует физический пин в соответствующую клавишу клавиатуры. + /// + 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, + }; + } + + /// + /// Преобразует числовой индекс пина в соответствующую клавишу клавиатуры. + /// Используется для методов с прямым доступом по индексу. + /// + 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, // невалидный индекс + }; + } +} \ No newline at end of file diff --git a/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/PlayerAction.cs b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/PlayerAction.cs new file mode 100644 index 0000000..db8f348 --- /dev/null +++ b/Библиотека/Периферия/UnityToEsp/Assets/Scripts/Inputs/PlayerAction.cs @@ -0,0 +1,19 @@ +/// +/// Абстрактные игровые действия для управления игроком. +/// Обеспечивает независимость от физического расположения кнопок. +/// +public enum PlayerAction +{ + /// Вверх + up, + /// Вниз + down, + /// Влево + left, + /// Вправо + right, + /// Основное действие + action, + /// Дополнительное действие(ВНИМАНИЕ РАБОТАЕТ У ОБОИХ ИГРОКОВ ОДНОВРЕМЕННО) + extra +}