using System.Collections; using System.Collections.Generic; using UnityEngine; using Random = UnityEngine.Random; namespace RR.Common.Controls { /// /// Управляет подсветкой периферийной платы по протоколу 85XXYY: /// /// XX — код эффекта () /// YY — время в секундах (00–99) /// /// Команда 85 (без параметров) отключает рендеринг и освобождает память. /// Поддерживает цепочки эффектов через : /// /// SetLight(LightMode.VerticalWave, 3).Then(LightMode.DiagonalWave, 3).Loop(); /// /// public class LightController : MonoBehaviour { // ───────────────────────── Инспектор ───────────────────────── [Header("Команды шляпы")] [Tooltip("COM-команда для включения шляпы")] [SerializeField] private string hatOnCommand = "2131"; [Tooltip("COM-команда для выключения шляпы")] [SerializeField] private string hatOffCommand = "2130"; [Header("Мигание")] [Tooltip("Интервал между переключениями при мигании (секунды)")] [SerializeField] [Range(0.05f, 5f)] private float blinkInterval = 0.5f; [Tooltip("Случайный разброс интервала (±секунды). 0 = строго равномерно")] [SerializeField] [Range(0f, 2f)] private float blinkIntervalVariance; [Header("Световые эффекты")] [Tooltip("Эффект по умолчанию при вызове SetLight без аргументов")] [SerializeField] private LightMode defaultLightMode = LightMode.Rainbow; [Tooltip("Время эффекта по умолчанию (секунды, 0–99)")] [SerializeField] [Range(0, 99)] private int defaultDuration = 3; // ───────────────────────── Приватное состояние ───────────────────────── private Coroutine _blinkingCoroutine; private LightSequence _activeSequence; private const string EffectPrefix = "85"; // ───────────────────────── Свойства ───────────────────────── /// Время эффекта по умолчанию (используется в ). public int DefaultDuration => defaultDuration; // ===========================start: PUBLIC API=========================== /// /// Включает эффект по умолчанию и возвращает для построения цепочки. /// public LightSequence SetLight() => SetLight(defaultLightMode, defaultDuration); /// /// Включает эффект на время по умолчанию и возвращает для построения цепочки. /// public LightSequence SetLight(LightMode mode) => SetLight(mode, defaultDuration); /// /// Включает световой эффект на указанное время и возвращает для построения цепочки. /// Если вызвать без .Then() / .Loop() — эффект запустится немедленно как одиночный. /// /// Эффект подсветки /// Время эффекта (0–99 секунд) public LightSequence SetLight(LightMode mode, int durationSeconds) { StopSequence(); _activeSequence = new LightSequence(this, mode, durationSeconds); _activeSequence.Play(); return _activeSequence; } /// /// Останавливает текущую активную последовательность эффектов и отключает рендеринг. /// public void StopSequence() { _activeSequence?.Stop(); _activeSequence = null; } /// /// Останавливает все эффекты и отключает рендеринг платы (85). /// public void StopAllEffects() { StopSequence(); ComWrapper.Send(EffectPrefix); } /// /// Запускает мигание шляпы с настроенным интервалом. /// public void Blink() => Blink(blinkInterval); /// /// Запускает мигание шляпы с явно указанным интервалом. /// public void Blink(float interval) { StopBlink(enableHat: false); _blinkingCoroutine = StartCoroutine(BlinkingCoroutine(interval)); } /// /// Меняет интервал мигания на лету; перезапускает мигание, если оно активно. /// public void SetBlinkInterval(float interval) { blinkInterval = Mathf.Max(0.05f, interval); if (_blinkingCoroutine != null) Blink(blinkInterval); } /// /// Останавливает мигание шляпы. /// /// Оставить шляпу включённой после остановки public void StopBlink(bool enableHat = true) { if (_blinkingCoroutine != null) { StopCoroutine(_blinkingCoroutine); _blinkingCoroutine = null; } ComWrapper.Send(enableHat ? hatOnCommand : hatOffCommand); } /// Возвращает true, если мигание шляпы сейчас активно. public bool IsBlinking => _blinkingCoroutine != null; /// Возвращает true, если последовательность эффектов сейчас активна. public bool IsSequencePlaying => _activeSequence?.IsPlaying ?? false; // ===========================end: PUBLIC API=========================== /// /// Отправляет COM-команду для одного шага последовательности. /// Используется из . /// public void SendCommand(LightMode mode, int durationSeconds) { ComWrapper.Send(BuildCommand(mode, durationSeconds)); } // ReSharper disable once IteratorNeverReturns — корутина намеренно бесконечная private IEnumerator BlinkingCoroutine(float interval) { while (true) { ComWrapper.Send(Random.Range(0f, 1f) < 0.5f ? hatOnCommand : hatOffCommand); float wait = blinkIntervalVariance > 0f ? Mathf.Max(0.05f, interval + Random.Range(-blinkIntervalVariance, blinkIntervalVariance)) : interval; yield return new WaitForSeconds(wait); } } private static string BuildCommand(LightMode mode, int durationSeconds) { if (mode == LightMode.Off) return EffectPrefix; int xx = (int)mode; int yy = Mathf.Clamp(durationSeconds, 0, 99); return $"{EffectPrefix}{xx:D2}{yy:D2}"; } /// /// Строитель последовательности световых эффектов. /// Создаётся через . /// /// SetLight(LightMode.VerticalWave, 3).Then(LightMode.DiagonalWave, 3).Loop(); /// SetLight(LightMode.VerticalWave, 3).Then(LightMode.Off); /// SetLight(LightMode.Rainbow, 5); /// /// public class LightSequence { private readonly struct Step { public readonly LightMode Mode; public readonly int DurationSeconds; public Step(LightMode mode, int durationSeconds) { Mode = mode; DurationSeconds = durationSeconds; } } private readonly List _steps = new(); private readonly LightController _controller; private bool _looping; private Coroutine _coroutine; internal LightSequence(LightController controller, LightMode firstMode, int firstDuration) { _controller = controller; _steps.Add(new Step(firstMode, firstDuration)); } /// Добавляет следующий эффект с временем по умолчанию. public LightSequence Then(LightMode mode) => Then(mode, _controller.DefaultDuration); /// Добавляет следующий эффект с указанным временем. public LightSequence Then(LightMode mode, int durationSeconds) { _steps.Add(new Step(mode, durationSeconds)); return this; } /// /// Зацикливает последовательность. Для остановки — . /// public LightSequence Loop() { _looping = true; Play(); return this; } /// Запускает последовательность один раз. public LightSequence Play() { Stop(); _coroutine = _controller.StartCoroutine(RunSequence()); return this; } /// Останавливает выполнение последовательности. public void Stop() { if (_coroutine == null) return; _controller.StopCoroutine(_coroutine); _coroutine = null; } /// Возвращает true, если последовательность сейчас выполняется. public bool IsPlaying => _coroutine != null; // ReSharper disable once IteratorNeverReturns — цикл намеренно бесконечный при _looping=true private IEnumerator RunSequence() { do { foreach (Step step in _steps) { _controller.SendCommand(step.Mode, step.DurationSeconds); if (step.Mode != LightMode.Off && step.DurationSeconds > 0) yield return new WaitForSeconds(step.DurationSeconds); } } while (_looping); _coroutine = null; } } } }