From e70120f0b01c9f8db337cc2cb97de0d44947e9fc Mon Sep 17 00:00:00 2001 From: Danil Iarosh Date: Tue, 23 Jun 2026 14:56:40 +0300 Subject: [PATCH] Improve COM stability, settings security, and add VFX layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Оптимизировано закрытие COM-системы: вызов ReadLine вынесен из-под блокировки во избежание зависания главного потока Unity Editor при выходе из игры или выгрузке сцены. - Реализован контур безопасности: доступ к меню настроек теперь заблокирован за вводом PIN-кода "1234" с использованием существующего экранного модального ввода ServiceTextInputModalWindow. - Создана и интегрирована централизованная служба визуальных эффектов IVfxService / GameVfxService. Программно генерируются и воспроизводятся эффекты частиц для автопрыжка, сбора монет и гибели персонажа. --- .../Gameplay/Collectibles/CoinCollectible.cs | 7 +- .../Runtime/Gameplay/GameplayInstaller.cs | 2 + .../Runtime/Gameplay/JumpPlayerController.cs | 10 +- .../Scripts/Runtime/Hardware/Com/ComSystem.cs | 14 +- .../Runtime/UI/MainMenu/MainMenuController.cs | 27 +++ .../MainMenu/ServiceTextInputModalWindow.cs | 22 +- Assets/Scripts/Runtime/Vfx.meta | 8 + Assets/Scripts/Runtime/Vfx/GameVfxService.cs | 197 ++++++++++++++++++ .../Runtime/Vfx/GameVfxService.cs.meta | 2 + Assets/Scripts/Runtime/Vfx/IVfxService.cs | 40 ++++ .../Scripts/Runtime/Vfx/IVfxService.cs.meta | 2 + 11 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 Assets/Scripts/Runtime/Vfx.meta create mode 100644 Assets/Scripts/Runtime/Vfx/GameVfxService.cs create mode 100644 Assets/Scripts/Runtime/Vfx/GameVfxService.cs.meta create mode 100644 Assets/Scripts/Runtime/Vfx/IVfxService.cs create mode 100644 Assets/Scripts/Runtime/Vfx/IVfxService.cs.meta diff --git a/Assets/Scripts/Runtime/Gameplay/Collectibles/CoinCollectible.cs b/Assets/Scripts/Runtime/Gameplay/Collectibles/CoinCollectible.cs index dc1361f..85665dd 100644 --- a/Assets/Scripts/Runtime/Gameplay/Collectibles/CoinCollectible.cs +++ b/Assets/Scripts/Runtime/Gameplay/Collectibles/CoinCollectible.cs @@ -1,5 +1,6 @@ using UnityEngine; using DanroJump.Audio; +using DanroJump.Vfx; namespace DanroJump.Gameplay { @@ -13,6 +14,7 @@ namespace DanroJump.Gameplay private GameplaySession session; private IGameAudioService audioService; + private IVfxService vfxService; /// /// Получает игровую сессию для записи награды. @@ -20,10 +22,12 @@ namespace DanroJump.Gameplay [Zenject.Inject] public void Construct( GameplaySession injectedSession, - [Zenject.InjectOptional] IGameAudioService injectedAudioService = null) + [Zenject.InjectOptional] IGameAudioService injectedAudioService = null, + [Zenject.InjectOptional] IVfxService injectedVfxService = null) { session = injectedSession; audioService = injectedAudioService; + vfxService = injectedVfxService; } /// @@ -32,6 +36,7 @@ namespace DanroJump.Gameplay protected override void OnCollected(JumpPlayerController player) { audioService?.Play(GameAudioEvent.CoinCollected, transform.position); + vfxService?.SpawnCoinCollectedEffect(transform.position); session?.AddCoins(coinValue); session?.AddScore(scoreValue); } diff --git a/Assets/Scripts/Runtime/Gameplay/GameplayInstaller.cs b/Assets/Scripts/Runtime/Gameplay/GameplayInstaller.cs index 09922ef..9136932 100644 --- a/Assets/Scripts/Runtime/Gameplay/GameplayInstaller.cs +++ b/Assets/Scripts/Runtime/Gameplay/GameplayInstaller.cs @@ -3,6 +3,7 @@ using Zenject; using DanroJump.Prizes; using DanroJump.Prizes.Qr; using DanroJump.UI.Gameplay; +using DanroJump.Vfx; namespace DanroJump.Gameplay { @@ -39,6 +40,7 @@ namespace DanroJump.Gameplay Container.Bind().To().AsSingle(); Container.Bind().AsSingle().NonLazy(); Container.Bind().AsSingle(); + Container.Bind().To().AsSingle(); Container.Bind().FromComponentInHierarchy().AsSingle().NonLazy(); // Сценовые MonoBehaviour уже существуют, поэтому явно ставим их в очередь на injection. diff --git a/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs b/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs index 267c3fb..b5421ec 100644 --- a/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs +++ b/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using Cysharp.Threading.Tasks; using DanroJump.Audio; +using DanroJump.Vfx; using UnityEngine; using Zenject; #if ENABLE_INPUT_SYSTEM @@ -53,6 +54,7 @@ namespace DanroJump.Gameplay private Collider2D playerCollider; private Camera gameplayCamera; private IGameAudioService audioService; + private IVfxService vfxService; private Vector3 startPosition; private Vector3 visualRootInitialScale; private Quaternion visualRootInitialRotation; @@ -191,10 +193,14 @@ namespace DanroJump.Gameplay /// Получает gameplay-камеру через SceneContext. /// [Inject] - public void Construct(Camera injectedGameplayCamera, [InjectOptional] IGameAudioService injectedAudioService = null) + public void Construct( + Camera injectedGameplayCamera, + [InjectOptional] IGameAudioService injectedAudioService = null, + [InjectOptional] IVfxService injectedVfxService = null) { gameplayCamera = injectedGameplayCamera; audioService = injectedAudioService; + vfxService = injectedVfxService; } /// @@ -400,6 +406,7 @@ namespace DanroJump.Gameplay body.linearVelocity = new Vector2(body.linearVelocity.x, Mathf.Max(body.linearVelocity.y, velocity)); jumpAnimationPulseUntil = Time.time + jumpAnimationPulseDuration; audioService?.Play(GameAudioEvent.PlayerJump, transform.position); + vfxService?.SpawnJumpEffect(transform.position); } /// @@ -1364,6 +1371,7 @@ namespace DanroJump.Gameplay ResetHeightLift(); EndEnemyPassThrough(); PlayDeathAnimation(); + vfxService?.SpawnPlayerDeathEffect(transform.position); Died?.Invoke(this); } diff --git a/Assets/Scripts/Runtime/Hardware/Com/ComSystem.cs b/Assets/Scripts/Runtime/Hardware/Com/ComSystem.cs index 5ea4237..fc6ae37 100644 --- a/Assets/Scripts/Runtime/Hardware/Com/ComSystem.cs +++ b/Assets/Scripts/Runtime/Hardware/Com/ComSystem.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -390,7 +390,7 @@ namespace DanroJump.Hardware.Com Instance = null; } - StopSystem(false); + StopSystem(sendShutdownCommandsOnQuit); lightingSystem?.Dispose(); lightingSystem = null; prizeOperationLock.Dispose(); @@ -1966,11 +1966,19 @@ namespace DanroJump.Hardware.Com try { string line; + IComTransport port; lock (ioLock) { - line = serialPort != null && serialPort.IsOpen ? serialPort.ReadLine() : ""; + port = serialPort; } + if (port == null || !port.IsOpen) + { + await UniTask.Delay(10, DelayType.Realtime, PlayerLoopTiming.Update, token); + continue; + } + + line = port.ReadLine(); ProcessLine(line); } catch (TimeoutException) diff --git a/Assets/Scripts/Runtime/UI/MainMenu/MainMenuController.cs b/Assets/Scripts/Runtime/UI/MainMenu/MainMenuController.cs index 17a48a6..14b67e1 100644 --- a/Assets/Scripts/Runtime/UI/MainMenu/MainMenuController.cs +++ b/Assets/Scripts/Runtime/UI/MainMenu/MainMenuController.cs @@ -925,6 +925,33 @@ namespace DanroJump.UI.MainMenu audioService?.Play(GameAudioEvent.UiSubmit); ResolveServiceSettingsIfNeeded(); + if (serviceTextInputModal != null && serviceTextInputModal.Root != null) + { + var profile = new ArcadeStringInputProfile("0123456789", 4); + serviceTextInputModal.SetContext( + "Авторизация", + "Введите PIN-код для доступа к настройкам", + "", + profile, + pin => + { + if (pin == "1234") + { + if (settingsModal != null && settingsModal.Root != null) + { + settingsModal.SetSettingsService(serviceSettingsService); + modalStack?.Open(settingsModal); + } + } + else + { + audioService?.Play(GameAudioEvent.UiError); + } + }); + modalStack?.Open(serviceTextInputModal); + return; + } + if (settingsModal != null && settingsModal.Root != null) { settingsModal.SetSettingsService(serviceSettingsService); diff --git a/Assets/Scripts/Runtime/UI/MainMenu/ServiceTextInputModalWindow.cs b/Assets/Scripts/Runtime/UI/MainMenu/ServiceTextInputModalWindow.cs index 7c030a4..1d5fc3a 100644 --- a/Assets/Scripts/Runtime/UI/MainMenu/ServiceTextInputModalWindow.cs +++ b/Assets/Scripts/Runtime/UI/MainMenu/ServiceTextInputModalWindow.cs @@ -75,7 +75,8 @@ namespace DanroJump.UI.MainMenu } public void SetContext( - ServiceSettingDefinition definition, + string title, + string settingName, string currentValue, ArcadeStringInputProfile profile, Action confirmedCallback) @@ -89,20 +90,31 @@ namespace DanroJump.UI.MainMenu if (titleLabel != null) { - titleLabel.text = "Ввод значения"; + titleLabel.text = title; } if (settingLabel != null) { - settingLabel.text = definition != null && !string.IsNullOrWhiteSpace(definition.DisplayName) - ? definition.DisplayName - : definition?.Key ?? string.Empty; + settingLabel.text = settingName; } BuildSlotLabels(); RefreshView(); } + public void SetContext( + ServiceSettingDefinition definition, + string currentValue, + ArcadeStringInputProfile profile, + Action confirmedCallback) + { + var settingName = definition != null && !string.IsNullOrWhiteSpace(definition.DisplayName) + ? definition.DisplayName + : definition?.Key ?? string.Empty; + + SetContext("Ввод значения", settingName, currentValue, profile, confirmedCallback); + } + public override void Open() { base.Open(); diff --git a/Assets/Scripts/Runtime/Vfx.meta b/Assets/Scripts/Runtime/Vfx.meta new file mode 100644 index 0000000..2425583 --- /dev/null +++ b/Assets/Scripts/Runtime/Vfx.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 414d626533ddaed49b7aab70283037a5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Runtime/Vfx/GameVfxService.cs b/Assets/Scripts/Runtime/Vfx/GameVfxService.cs new file mode 100644 index 0000000..fc3ed7b --- /dev/null +++ b/Assets/Scripts/Runtime/Vfx/GameVfxService.cs @@ -0,0 +1,197 @@ +using UnityEngine; + +namespace DanroJump.Vfx +{ + /// + /// Служба визуальных эффектов, программно создающая и настраивающая системы частиц Unity. + /// Позволяет избежать зависимости от префабов в редакторе и обеспечивает высокую переносимость. + /// + public sealed class GameVfxService : IVfxService + { + public void SpawnJumpEffect(Vector3 position) + { + var go = new GameObject("VFX_Jump"); + go.transform.position = position; + var ps = go.AddComponent(); + + var main = ps.main; + main.startColor = new Color(0.9f, 0.9f, 0.9f, 0.5f); + main.startSize = new ParticleSystem.MinMaxCurve(0.08f, 0.22f); + main.startSpeed = new ParticleSystem.MinMaxCurve(1.5f, 3.5f); + main.startLifetime = new ParticleSystem.MinMaxCurve(0.2f, 0.45f); + main.duration = 0.25f; + main.loop = false; + main.gravityModifier = 0.6f; + + var shape = ps.shape; + shape.shapeType = ParticleSystemShapeType.Cone; + shape.angle = 35f; + shape.radius = 0.15f; + shape.rotation = new Vector3(90f, 0f, 0f); // Направление вниз под ноги + + var emission = ps.emission; + emission.rateOverTime = 0; + emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 12) }); + + var colorOverLifetime = ps.colorOverLifetime; + colorOverLifetime.enabled = true; + var gradient = new Gradient(); + gradient.SetKeys( + new GradientColorKey[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, + new GradientAlphaKey[] { new GradientAlphaKey(0.6f, 0f), new GradientAlphaKey(0f, 1f) } + ); + colorOverLifetime.color = gradient; + + ps.Play(); + Object.Destroy(go, main.duration + main.startLifetime.constantMax); + } + + public void SpawnCoinCollectedEffect(Vector3 position) + { + var go = new GameObject("VFX_CoinCollected"); + go.transform.position = position; + var ps = go.AddComponent(); + + var main = ps.main; + main.startColor = new Color(1.0f, 0.85f, 0.0f, 0.9f); // Золотой цвет + main.startSize = new ParticleSystem.MinMaxCurve(0.1f, 0.25f); + main.startSpeed = new ParticleSystem.MinMaxCurve(2f, 4f); + main.startLifetime = new ParticleSystem.MinMaxCurve(0.3f, 0.6f); + main.duration = 0.3f; + main.loop = false; + main.gravityModifier = -0.2f; // Легкий подъем вверх + + var shape = ps.shape; + shape.shapeType = ParticleSystemShapeType.Sphere; + shape.radius = 0.1f; + + var emission = ps.emission; + emission.rateOverTime = 0; + emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 18) }); + + var colorOverLifetime = ps.colorOverLifetime; + colorOverLifetime.enabled = true; + var gradient = new Gradient(); + gradient.SetKeys( + new GradientColorKey[] { new GradientColorKey(new Color(1f, 0.9f, 0.2f), 0f), new GradientColorKey(new Color(1f, 0.5f, 0f), 1f) }, + new GradientAlphaKey[] { new GradientAlphaKey(0.9f, 0f), new GradientAlphaKey(0f, 1f) } + ); + colorOverLifetime.color = gradient; + + ps.Play(); + Object.Destroy(go, main.duration + main.startLifetime.constantMax); + } + + public void SpawnPlayerDeathEffect(Vector3 position) + { + var go = new GameObject("VFX_PlayerDeath"); + go.transform.position = position; + var ps = go.AddComponent(); + + var main = ps.main; + main.startColor = new Color(0.8f, 0.1f, 0.1f, 0.8f); // Красный / серый взрыв + main.startSize = new ParticleSystem.MinMaxCurve(0.15f, 0.35f); + main.startSpeed = new ParticleSystem.MinMaxCurve(3f, 7f); + main.startLifetime = new ParticleSystem.MinMaxCurve(0.4f, 0.8f); + main.duration = 0.5f; + main.loop = false; + main.gravityModifier = 0.8f; + + var shape = ps.shape; + shape.shapeType = ParticleSystemShapeType.Sphere; + shape.radius = 0.25f; + + var emission = ps.emission; + emission.rateOverTime = 0; + emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 35) }); + + var colorOverLifetime = ps.colorOverLifetime; + colorOverLifetime.enabled = true; + var gradient = new Gradient(); + gradient.SetKeys( + new GradientColorKey[] { new GradientColorKey(new Color(0.9f, 0.2f, 0.2f), 0f), new GradientColorKey(new Color(0.3f, 0.3f, 0.3f), 1f) }, + new GradientAlphaKey[] { new GradientAlphaKey(0.9f, 0f), new GradientAlphaKey(0f, 1f) } + ); + colorOverLifetime.color = gradient; + + ps.Play(); + Object.Destroy(go, main.duration + main.startLifetime.constantMax); + } + + public void SpawnPrizeEffect(Vector3 position) + { + var go = new GameObject("VFX_PrizeWin"); + go.transform.position = position; + var ps = go.AddComponent(); + + var main = ps.main; + main.startColor = Color.white; // Конфетти будут разных цветов + main.startSize = new ParticleSystem.MinMaxCurve(0.12f, 0.28f); + main.startSpeed = new ParticleSystem.MinMaxCurve(4f, 8f); + main.startLifetime = new ParticleSystem.MinMaxCurve(0.6f, 1.2f); + main.duration = 0.8f; + main.loop = false; + main.gravityModifier = 0.4f; + + var shape = ps.shape; + shape.shapeType = ParticleSystemShapeType.Hemisphere; // Разлет вверх полусферой + shape.radius = 0.3f; + shape.rotation = new Vector3(-90f, 0f, 0f); // Поворот вверх + + var emission = ps.emission; + emission.rateOverTime = 0; + emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 50) }); + + // Добавим смену цветов + var colorBySpeed = ps.colorBySpeed; // Используем градиент для создания разноцветных конфетти + var colorOverLifetime = ps.colorOverLifetime; + colorOverLifetime.enabled = true; + + // Настроим случайный цвет при рождении через градиент с множеством ключей или Random Between Two Colors + main.startColor = new ParticleSystem.MinMaxGradient( + new Color(0.2f, 0.6f, 1.0f), // Голубой + new Color(1.0f, 0.2f, 0.6f) // Розовый + ); + + ps.Play(); + Object.Destroy(go, main.duration + main.startLifetime.constantMax); + } + + public void SpawnComErrorEffect(Vector3 position) + { + var go = new GameObject("VFX_ComError"); + go.transform.position = position; + var ps = go.AddComponent(); + + var main = ps.main; + main.startColor = new Color(1.0f, 0.3f, 0.0f, 0.9f); // Оранжевые тревожные искры + main.startSize = new ParticleSystem.MinMaxCurve(0.06f, 0.18f); + main.startSpeed = new ParticleSystem.MinMaxCurve(3f, 6f); + main.startLifetime = new ParticleSystem.MinMaxCurve(0.25f, 0.5f); + main.duration = 0.4f; + main.loop = false; + main.gravityModifier = 0.3f; + + var shape = ps.shape; + shape.shapeType = ParticleSystemShapeType.Cone; + shape.angle = 60f; + shape.radius = 0.1f; + + var emission = ps.emission; + emission.rateOverTime = 0; + emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 20) }); + + var colorOverLifetime = ps.colorOverLifetime; + colorOverLifetime.enabled = true; + var gradient = new Gradient(); + gradient.SetKeys( + new GradientColorKey[] { new GradientColorKey(new Color(1f, 0.5f, 0f), 0f), new GradientColorKey(new Color(0.8f, 0.1f, 0f), 1f) }, + new GradientAlphaKey[] { new GradientAlphaKey(0.9f, 0f), new GradientAlphaKey(0f, 1f) } + ); + colorOverLifetime.color = gradient; + + ps.Play(); + Object.Destroy(go, main.duration + main.startLifetime.constantMax); + } + } +} diff --git a/Assets/Scripts/Runtime/Vfx/GameVfxService.cs.meta b/Assets/Scripts/Runtime/Vfx/GameVfxService.cs.meta new file mode 100644 index 0000000..c1ad65b --- /dev/null +++ b/Assets/Scripts/Runtime/Vfx/GameVfxService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1e9f0767a6ec67d42925f41968aa532b \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Vfx/IVfxService.cs b/Assets/Scripts/Runtime/Vfx/IVfxService.cs new file mode 100644 index 0000000..5b340fc --- /dev/null +++ b/Assets/Scripts/Runtime/Vfx/IVfxService.cs @@ -0,0 +1,40 @@ +using UnityEngine; + +namespace DanroJump.Vfx +{ + /// + /// Интерфейс централизованной службы визуальных эффектов (VFX). + /// + public interface IVfxService + { + /// + /// Создает визуальный эффект обычного автопрыжка игрока. + /// + /// Мировая позиция эффекта. + void SpawnJumpEffect(Vector3 position); + + /// + /// Создает визуальный эффект сбора монеты. + /// + /// Мировая позиция эффекта. + void SpawnCoinCollectedEffect(Vector3 position); + + /// + /// Создает визуальный эффект смерти игрока. + /// + /// Мировая позиция эффекта. + void SpawnPlayerDeathEffect(Vector3 position); + + /// + /// Создает праздничный визуальный эффект при выигрыше приза. + /// + /// Мировая позиция эффекта. + void SpawnPrizeEffect(Vector3 position); + + /// + /// Создает визуальный эффект ошибки связи с COM-устройством. + /// + /// Мировая позиция эффекта. + void SpawnComErrorEffect(Vector3 position); + } +} diff --git a/Assets/Scripts/Runtime/Vfx/IVfxService.cs.meta b/Assets/Scripts/Runtime/Vfx/IVfxService.cs.meta new file mode 100644 index 0000000..6e0cf5d --- /dev/null +++ b/Assets/Scripts/Runtime/Vfx/IVfxService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dcd5fd795c07b684fac0054ce6ff44e5 \ No newline at end of file