Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Интегрирована система кастомных визуальных эффектов (VFX): - Добавлена новая утилита MvpVFXUtility и настройки MvpVFXSpawnSettings для спавна эффектов. - Реализован пул эффектов VFXPool с поддержкой отслеживания версий спавна, сброса оверрайдов и анимации масштаба по времени. - Добавлены эффекты прыжка и приземления в PlayerAnimationEvents. - Интегрирован эффект DeathVignetteController для плавного затемнения экрана при смерти игрока. - Добавлены частицы stompImpactVFX при уничтожении врагов прыжком сверху (EnemyController). - Реализован эффект конфетти finishVFX при успешном завершении уровня (LevelFinish). - Интегрированы эффекты искр при сборе монеток, кристаллов и других собираемых предметов (Collectible, EnvironmentalCollectibleBase). - Улучшен и визуально доработан пользовательский интерфейс (UI): - Добавлен скрипт QRCodeTierHighlight для цветовой подсветки QR-кодов в зависимости от тира приза. - Реализована пульсация блока приза в баннере победы (WinBannerController). - Обновлено обозначение валюты в BalanceDisplay с "₽" на "руб.". - Добавлены новые префабы, сцены, ассеты тайлсетов и шейдеров для настройки визуального стиля. - Обновлён протокол и инициализация подсветки COM-платы: - Изменен префикс команд подсветки с 85/86 на 8550, а диапазон эффектов обновлен с 40...52 на 10...22 (LightController, LightMode, LightCycleDurations). - Исправлена хардкод-команда сброса подсветки при инициализации в ComDeviceClient с устаревшей 8600 на новую 8550 для предотвращения рассинхронизации буфера обмена платы.
326 lines
11 KiB
C#
326 lines
11 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.UI;
|
||
|
||
namespace RR.Common.UI.FX
|
||
{
|
||
/// <summary>
|
||
/// Мягкое свечение для кнопок при фокусе с клавиатуры/джойстика или наведении мышью.
|
||
/// </summary>
|
||
[DisallowMultipleComponent]
|
||
[AddComponentMenu("RR/UI/FX/UI Selectable Focus Glow")]
|
||
public sealed class UISelectableFocusGlow : MonoBehaviour, ISelectHandler, IDeselectHandler, IPointerEnterHandler, IPointerExitHandler
|
||
{
|
||
private const int TextureSize = 96;
|
||
|
||
private static Sprite _glowSprite;
|
||
|
||
[SerializeField] private Color glowColor = new(1f, 0.92f, 0.38f, 1f);
|
||
[SerializeField] private bool useButtonColor = true;
|
||
[SerializeField, Range(0f, 1f)] private float colorLighten = 0.35f;
|
||
[SerializeField, Range(0f, 1f)] private float maxAlpha = 0.55f;
|
||
[SerializeField, Min(0f)] private float outlineDistance = 3f;
|
||
[SerializeField] private bool useTargetSpriteShape = true;
|
||
[SerializeField, Range(4, 16)] private int outlineSamples = 8;
|
||
[SerializeField, Range(1, 4)] private int blurLayers = 2;
|
||
[SerializeField, Min(0f)] private float blurSpread = 1.5f;
|
||
[SerializeField, Min(0.01f)] private float fadeSpeed = 12f;
|
||
[SerializeField, Min(0f)] private float pulseScale = 0.025f;
|
||
[SerializeField, Min(0.01f)] private float pulseDuration = 1.1f;
|
||
|
||
private Selectable _selectable;
|
||
private Image _targetImage;
|
||
private Image _glowImage;
|
||
private RectTransform _glowRect;
|
||
private GameObject _glowObject;
|
||
private readonly List<Image> _glowLayers = new();
|
||
private bool _isPointerInside;
|
||
private float _currentAlpha;
|
||
|
||
private void Awake()
|
||
{
|
||
ResolveReferences();
|
||
EnsureGlowView();
|
||
SetGlowAlpha(0f);
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
ResolveReferences();
|
||
EnsureGlowView();
|
||
SyncGlowRect(1f);
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
_isPointerInside = false;
|
||
SetGlowAlpha(0f);
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
EnsureGlowView();
|
||
|
||
float targetAlpha = IsFocusedNow() ? maxAlpha : 0f;
|
||
_currentAlpha = Mathf.MoveTowards(_currentAlpha, targetAlpha, fadeSpeed * Time.unscaledDeltaTime);
|
||
SetGlowAlpha(_currentAlpha);
|
||
|
||
if (_glowRect != null)
|
||
{
|
||
float wave = (Mathf.Sin(Time.unscaledTime / pulseDuration * Mathf.PI * 2f) + 1f) * 0.5f;
|
||
float scale = 1f + wave * pulseScale * Mathf.Clamp01(_currentAlpha / Mathf.Max(0.001f, maxAlpha));
|
||
SyncGlowRect(scale);
|
||
}
|
||
}
|
||
|
||
public void OnSelect(BaseEventData eventData)
|
||
{
|
||
_currentAlpha = Mathf.Max(_currentAlpha, maxAlpha * 0.5f);
|
||
}
|
||
|
||
public void OnDeselect(BaseEventData eventData)
|
||
{
|
||
}
|
||
|
||
public void OnPointerEnter(PointerEventData eventData)
|
||
{
|
||
_isPointerInside = true;
|
||
_currentAlpha = Mathf.Max(_currentAlpha, maxAlpha * 0.5f);
|
||
}
|
||
|
||
public void OnPointerExit(PointerEventData eventData)
|
||
{
|
||
_isPointerInside = false;
|
||
}
|
||
|
||
private void ResolveReferences()
|
||
{
|
||
if (_selectable == null)
|
||
{
|
||
_selectable = GetComponent<Selectable>();
|
||
}
|
||
|
||
if (_targetImage == null)
|
||
{
|
||
_targetImage = GetComponent<Image>();
|
||
}
|
||
}
|
||
|
||
private void EnsureGlowView()
|
||
{
|
||
if (_glowImage != null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Transform glowParent = transform.parent != null ? transform.parent : transform;
|
||
_glowObject = new GameObject($"{name} Focus Glow", typeof(RectTransform), typeof(LayoutElement));
|
||
_glowObject.transform.SetParent(glowParent, false);
|
||
_glowObject.GetComponent<LayoutElement>().ignoreLayout = true;
|
||
_glowRect = _glowObject.GetComponent<RectTransform>();
|
||
_glowImage = null;
|
||
|
||
_glowObject.transform.SetSiblingIndex(transform.GetSiblingIndex());
|
||
SyncGlowRect(1f);
|
||
|
||
RebuildGlowLayers();
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (_glowObject != null)
|
||
{
|
||
Destroy(_glowObject);
|
||
}
|
||
}
|
||
|
||
private void SyncGlowRect(float scale)
|
||
{
|
||
if (_glowRect == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
RectTransform targetRect = transform as RectTransform;
|
||
if (targetRect == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_glowRect.anchorMin = targetRect.anchorMin;
|
||
_glowRect.anchorMax = targetRect.anchorMax;
|
||
_glowRect.pivot = targetRect.pivot;
|
||
_glowRect.anchoredPosition = targetRect.anchoredPosition;
|
||
_glowRect.sizeDelta = targetRect.sizeDelta;
|
||
_glowRect.localRotation = targetRect.localRotation;
|
||
_glowRect.localScale = targetRect.localScale * scale;
|
||
|
||
if (_glowRect.parent == targetRect.parent)
|
||
{
|
||
_glowRect.SetSiblingIndex(Mathf.Max(0, targetRect.GetSiblingIndex() - 1));
|
||
}
|
||
}
|
||
|
||
private void RebuildGlowLayers()
|
||
{
|
||
_glowLayers.Clear();
|
||
int samples = Mathf.Max(4, outlineSamples);
|
||
int layerIndex = 0;
|
||
|
||
for (int blur = 0; blur < Mathf.Max(1, blurLayers); blur++)
|
||
{
|
||
float distance = outlineDistance + blur * blurSpread;
|
||
for (int sample = 0; sample < samples; sample++)
|
||
{
|
||
string layerName = $"Focus Outline {layerIndex + 1}";
|
||
Transform existingLayer = _glowRect.Find(layerName);
|
||
GameObject layerObject = existingLayer != null
|
||
? existingLayer.gameObject
|
||
: new GameObject(layerName, typeof(RectTransform), typeof(Image));
|
||
|
||
layerObject.transform.SetParent(_glowRect, false);
|
||
layerObject.transform.SetSiblingIndex(layerIndex);
|
||
|
||
float angle = sample / (float)samples * Mathf.PI * 2f;
|
||
Vector2 offset = new(Mathf.Cos(angle) * distance, Mathf.Sin(angle) * distance);
|
||
|
||
RectTransform layerRect = layerObject.GetComponent<RectTransform>();
|
||
layerRect.anchorMin = Vector2.zero;
|
||
layerRect.anchorMax = Vector2.one;
|
||
layerRect.offsetMin = offset;
|
||
layerRect.offsetMax = offset;
|
||
|
||
Image layerImage = layerObject.GetComponent<Image>();
|
||
ApplyGlowSprite(layerImage);
|
||
layerImage.raycastTarget = false;
|
||
_glowLayers.Add(layerImage);
|
||
|
||
if (layerIndex == 0)
|
||
{
|
||
_glowImage = layerImage;
|
||
}
|
||
|
||
layerIndex++;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ApplyGlowSprite(Image image)
|
||
{
|
||
if (image == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_targetImage != null && useTargetSpriteShape && _targetImage.sprite != null)
|
||
{
|
||
image.sprite = _targetImage.sprite;
|
||
image.type = _targetImage.type;
|
||
image.preserveAspect = _targetImage.preserveAspect;
|
||
image.fillCenter = true;
|
||
image.fillMethod = _targetImage.fillMethod;
|
||
image.fillOrigin = _targetImage.fillOrigin;
|
||
image.fillAmount = _targetImage.fillAmount;
|
||
image.fillClockwise = _targetImage.fillClockwise;
|
||
image.pixelsPerUnitMultiplier = _targetImage.pixelsPerUnitMultiplier;
|
||
return;
|
||
}
|
||
|
||
image.sprite = GetGlowSprite();
|
||
image.type = Image.Type.Sliced;
|
||
image.preserveAspect = false;
|
||
}
|
||
|
||
private bool IsFocusedNow()
|
||
{
|
||
if (!isActiveAndEnabled || _selectable == null || !_selectable.IsActive() || !_selectable.IsInteractable())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
bool isSelected = EventSystem.current != null
|
||
&& EventSystem.current.currentSelectedGameObject == gameObject;
|
||
|
||
return isSelected || _isPointerInside;
|
||
}
|
||
|
||
private void SetGlowAlpha(float alpha)
|
||
{
|
||
if (_glowLayers.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
for (int i = 0; i < _glowLayers.Count; i++)
|
||
{
|
||
Image layer = _glowLayers[i];
|
||
if (layer == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
int samples = Mathf.Max(4, outlineSamples);
|
||
int blurIndex = i / samples;
|
||
float layerT = Mathf.Max(1, blurLayers) <= 1 ? 0f : blurIndex / (float)(Mathf.Max(1, blurLayers) - 1);
|
||
Color color = ResolveGlowColor();
|
||
color.a = alpha * Mathf.Lerp(0.36f, 0.12f, layerT);
|
||
layer.color = color;
|
||
layer.enabled = alpha > 0.001f;
|
||
}
|
||
}
|
||
|
||
private Color ResolveGlowColor()
|
||
{
|
||
if (!useButtonColor || _targetImage == null)
|
||
{
|
||
return glowColor;
|
||
}
|
||
|
||
Color color = Color.Lerp(_targetImage.color, Color.white, colorLighten);
|
||
color.a = 1f;
|
||
return color;
|
||
}
|
||
|
||
private static Sprite GetGlowSprite()
|
||
{
|
||
if (_glowSprite != null)
|
||
{
|
||
return _glowSprite;
|
||
}
|
||
|
||
Texture2D texture = new(TextureSize, TextureSize, TextureFormat.RGBA32, false)
|
||
{
|
||
name = "Runtime UI Focus Glow",
|
||
wrapMode = TextureWrapMode.Clamp,
|
||
filterMode = FilterMode.Bilinear
|
||
};
|
||
|
||
Vector2 center = new(0.5f, 0.5f);
|
||
for (int y = 0; y < TextureSize; y++)
|
||
{
|
||
for (int x = 0; x < TextureSize; x++)
|
||
{
|
||
Vector2 uv = new(x / (TextureSize - 1f), y / (TextureSize - 1f));
|
||
float edgeDistance = Mathf.Max(Mathf.Abs(uv.x - center.x), Mathf.Abs(uv.y - center.y));
|
||
float alpha = Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(0.25f, 0.5f, edgeDistance));
|
||
alpha *= 1f - Mathf.SmoothStep(0.48f, 0.5f, edgeDistance);
|
||
texture.SetPixel(x, y, new Color(1f, 1f, 1f, alpha));
|
||
}
|
||
}
|
||
|
||
texture.Apply();
|
||
_glowSprite = Sprite.Create(
|
||
texture,
|
||
new Rect(0f, 0f, TextureSize, TextureSize),
|
||
new Vector2(0.5f, 0.5f),
|
||
100f,
|
||
0,
|
||
SpriteMeshType.FullRect,
|
||
new Vector4(24f, 24f, 24f, 24f));
|
||
|
||
return _glowSprite;
|
||
}
|
||
}
|
||
}
|