Danro/Assets/Scripts/UI/UIElementAnimator.cs
Danil Iarosh c1d8cb89c9
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat: add UI animator, game price display, improve lighting reliability
Добавлены новые UI-компоненты и исправлена надёжность подсветки

- Добавлен UIElementAnimator — универсальный аниматор UI-элементов (покачивание, движение вверх-вниз, пульсация масштаба)
- Добавлен GamePriceDisplay — отображение стоимости игры из настроек (payment.startGameCost, payment.currency)
- Добавлен метод StartIdleEffects() в GameManager для гарантированного запуска мигания шапки и последовательности эффектов
- Исправлена подсветка, не включавшаяся при переходе в главное меню: OnSceneLoaded теперь вызывает StartIdleEffects()
- Исправлено восстановление подсветки при переподключении платы: ComSystemManager использует StartIdleEffects() для MainMenu
- Добавлены LightController.IsHatOn и ResendHatState() для корректного отслеживания состояния шапки
- Исправлено зависание спецэффектов при мигании шапки: LightSequence теперь переотправляет команду эффекта каждые 2 сек
- Убраны дублирующиеся вызовы Blink/SetLight из LoadScene (теперь обрабатывается в OnSceneLoaded)
- Обновлена сцена MainMenu, ассеты шрифтов, удалён старый префаб Coin
2026-03-05 17:03:03 +03:00

278 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using UnityEngine;
namespace RR.Common.UI
{
/// <summary>
/// Универсальный аниматор UI-элементов.
/// Поддерживает покачивание (rotation), вертикальное движение (bob) и пульсацию масштаба (pulse).
/// Каждый эффект включается/отключается и настраивается независимо.
/// </summary>
[AddComponentMenu("RR/UI/UI Element Animator")]
public class UIElementAnimator : MonoBehaviour
{
// ───────────────────────── Enums ─────────────────────────
/// <summary>Функция плавности для анимации.</summary>
public enum EaseType
{
Sine,
Smooth, // SmoothStep
Linear,
Bounce
}
// ───────────────────────── Swing (покачивание) ─────────────────────────
[Serializable]
public sealed class SwingSettings
{
[Tooltip("Включить покачивание")]
public bool enabled = true;
[Tooltip("Максимальный угол отклонения (градусы)")]
[Range(0f, 90f)]
public float amplitude = 15f;
[Tooltip("Длительность одного полного цикла (туда‑обратно), сек")]
[Min(0.01f)]
public float duration = 1f;
[Tooltip("Сдвиг фазы (0–1)")]
[Range(0f, 1f)]
public float phaseOffset;
[Tooltip("Функция плавности")]
public EaseType ease = EaseType.Sine;
}
// ───────────────────────── Bob (движение вверх‑вниз) ─────────────────────────
[Serializable]
public sealed class BobSettings
{
[Tooltip("Включить движение вверх‑вниз")]
public bool enabled = true;
[Tooltip("Амплитуда движения (пикселей / юнитов)")]
[Min(0f)]
public float amplitude = 10f;
[Tooltip("Длительность одного полного цикла (вверх‑вниз), сек")]
[Min(0.01f)]
public float duration = 1.2f;
[Tooltip("Сдвиг фазы (0–1)")]
[Range(0f, 1f)]
public float phaseOffset;
[Tooltip("Функция плавности")]
public EaseType ease = EaseType.Sine;
}
// ───────────────────────── Pulse (пульсация масштаба) ─────────────────────────
[Serializable]
public sealed class PulseSettings
{
[Tooltip("Включить пульсацию масштаба")]
public bool enabled = true;
[Tooltip("Минимальный множитель масштаба")]
[Min(0f)]
public float minScale = 0.9f;
[Tooltip("Максимальный множитель масштаба")]
[Min(0f)]
public float maxScale = 1.1f;
[Tooltip("Длительность одного полного цикла (увеличение‑уменьшение), сек")]
[Min(0.01f)]
public float duration = 1f;
[Tooltip("Сдвиг фазы (0–1)")]
[Range(0f, 1f)]
public float phaseOffset;
[Tooltip("Функция плавности")]
public EaseType ease = EaseType.Smooth;
[Tooltip("Пульсировать равномерно по обеим осям")]
public bool uniform = true;
}
// ───────────────────────── Inspector fields ─────────────────────────
[Header("Покачивание (Rotation Z)")]
[SerializeField] private SwingSettings swing = new();
[Header("Движение вверх‑вниз")]
[SerializeField] private BobSettings bob = new();
[Header("Пульсация масштаба")]
[SerializeField] private PulseSettings pulse = new();
[Header("Общие")]
[Tooltip("Использовать unscaledTime (работает на паузе)")]
[SerializeField] private bool useUnscaledTime = true;
// ───────────────────────── Runtime state ─────────────────────────
private RectTransform _rect;
private Transform _transform;
private Vector3 _initialLocalPosition;
private Quaternion _initialLocalRotation;
private Vector3 _initialLocalScale;
// ───────────────────────── Public API ─────────────────────────
public SwingSettings Swing => swing;
public BobSettings Bob => bob;
public PulseSettings Pulse => pulse;
/// <summary>Сбросить все анимации к исходному состоянию.</summary>
public void ResetToInitial()
{
Transform t = GetTransform();
t.localPosition = _initialLocalPosition;
t.localRotation = _initialLocalRotation;
t.localScale = _initialLocalScale;
}
/// <summary>Запомнить текущее состояние как начальное.</summary>
public void CaptureInitialState()
{
Transform t = GetTransform();
_initialLocalPosition = t.localPosition;
_initialLocalRotation = t.localRotation;
_initialLocalScale = t.localScale;
}
// ───────────────────────── Unity callbacks ─────────────────────────
private void Awake()
{
CaptureInitialState();
}
private void Update()
{
float time = useUnscaledTime ? Time.unscaledTime : Time.time;
Transform t = GetTransform();
// --- Swing ---
if (swing.enabled)
{
float phase = time / swing.duration + swing.phaseOffset;
float wave = EvaluateWave(phase, swing.ease);
float angle = wave * swing.amplitude;
t.localRotation = _initialLocalRotation * Quaternion.Euler(0f, 0f, angle);
}
else
{
t.localRotation = _initialLocalRotation;
}
// --- Bob ---
if (bob.enabled)
{
float phase = time / bob.duration + bob.phaseOffset;
float wave = EvaluateWave(phase, bob.ease);
float offset = wave * bob.amplitude;
Vector3 pos = _initialLocalPosition;
pos.y += offset;
t.localPosition = pos;
}
else
{
t.localPosition = _initialLocalPosition;
}
// --- Pulse ---
if (pulse.enabled)
{
float phase = time / pulse.duration + pulse.phaseOffset;
float wave = EvaluateWave01(phase, pulse.ease); // 0..1
float scale = Mathf.Lerp(pulse.minScale, pulse.maxScale, wave);
if (pulse.uniform)
{
t.localScale = _initialLocalScale * scale;
}
else
{
t.localScale = new Vector3(
_initialLocalScale.x * scale,
_initialLocalScale.y * scale,
_initialLocalScale.z);
}
}
else
{
t.localScale = _initialLocalScale;
}
}
// ───────────────────────── Helpers ─────────────────────────
private Transform GetTransform()
{
if (_transform == null)
{
_rect = GetComponent<RectTransform>();
_transform = _rect != null ? _rect : transform;
}
return _transform;
}
/// <summary>
/// Возвращает значение от −1 до 1 (симметричная волна).
/// Используется для swing и bob.
/// </summary>
private static float EvaluateWave(float phase, EaseType ease)
{
float t = Mathf.Repeat(phase, 1f); // 0..1
return ease switch
{
EaseType.Sine => Mathf.Sin(t * Mathf.PI * 2f),
EaseType.Linear => 1f - 4f * Mathf.Abs(t - 0.5f), // triangle wave −1..1
EaseType.Smooth => SmoothTriangle(t),
EaseType.Bounce => Mathf.Sin(t * Mathf.PI * 2f) * BounceDecay(t),
_ => Mathf.Sin(t * Mathf.PI * 2f)
};
}
/// <summary>
/// Возвращает значение от 0 до 1 (однополярная волна).
/// Используется для pulse.
/// </summary>
private static float EvaluateWave01(float phase, EaseType ease)
{
return (EvaluateWave(phase, ease) + 1f) * 0.5f;
}
/// <summary>SmoothStep‑версия треугольной волны (−1..1).</summary>
private static float SmoothTriangle(float t)
{
// Треугольная волна 0..1..0
float tri = 1f - 2f * Mathf.Abs(t - 0.5f); // 0..1
tri = tri * tri * (3f - 2f * tri); // SmoothStep
return tri * 2f - 1f; // remap to −1..1
}
/// <summary>Затухание для bounce‑волны.</summary>
private static float BounceDecay(float t)
{
// Полный цикл: сильный толчок → затухание → возврат
float bounce = 1f - Mathf.Abs(2f * t - 1f);
return bounce * bounce;
}
}
}