using UnityEngine; namespace RR.Common.UI { /// /// Указатель-стрелка, который плавно перемещается к произвольному UI-элементу. /// Управляется извне через или . /// public class UIRowPointer : MonoBehaviour { [Header("Указатель")] [Tooltip("RectTransform указателя (стрелка, иконка и т.д.).")] [SerializeField] private RectTransform pointer; [Header("Цели (опционально)")] [Tooltip("Фиксированные цели для SetTarget(int). Можно не заполнять, если используется SetTarget(RectTransform).")] [SerializeField] private RectTransform[] targets; [Header("Движение")] [Tooltip("Скорость плавного перемещения. 0 — мгновенно.")] [SerializeField] private float smoothSpeed = 12f; [Tooltip("Смещение указателя относительно центра цели (в локальных координатах родителя указателя).")] [SerializeField] private Vector2 offset; [Header("Покачивание")] [Tooltip("Включить покачивание указателя по горизонтали.")] [SerializeField] private bool wobble = true; [Tooltip("Амплитуда покачивания (пикселей).")] [SerializeField] private float wobbleAmplitude = 4f; [Tooltip("Скорость покачивания (циклов в секунду).")] [SerializeField] private float wobbleSpeed = 2f; // ── Состояние ── private RectTransform _currentTarget; private Vector2 _targetPos; private Vector2 _baseX; private bool _ready; // ────────────── Публичный API ────────────── /// /// Перемещает указатель к произвольному . /// public void SetTarget(RectTransform target) { if (target == null) return; _currentTarget = target; _targetPos = CalculatePosition(target); if (!_ready) { if (pointer) pointer.anchoredPosition = _targetPos; _ready = true; } ShowPointer(); } /// /// Перемещает указатель к цели по индексу из массива . /// public void SetTarget(int index) { if (targets == null || index < 0 || index >= targets.Length) return; SetTarget(targets[index]); } /// /// Скрывает указатель. /// public void Hide() { if (pointer) pointer.gameObject.SetActive(false); _currentTarget = null; } /// /// Задаёт массив целей из кода (если не задан через инспектор). /// public void SetTargets(RectTransform[] newTargets) { targets = newTargets; } // ────────────── Lifecycle ────────────── private void Awake() { if (pointer) _baseX = new Vector2(pointer.anchoredPosition.x, 0f); } private void LateUpdate() { if (!pointer || _currentTarget == null) return; // Пересчитываем позицию (цель может двигаться из-за layout) _targetPos = CalculatePosition(_currentTarget); // Плавное перемещение Vector2 pos = smoothSpeed > 0 ? Vector2.Lerp(pointer.anchoredPosition, _targetPos, Time.unscaledDeltaTime * smoothSpeed) : _targetPos; // Покачивание if (wobble) pos.x += Mathf.Sin(Time.unscaledTime * wobbleSpeed * Mathf.PI * 2f) * wobbleAmplitude; pointer.anchoredPosition = pos; } // ────────────── Внутреннее ────────────── private Vector2 CalculatePosition(RectTransform target) { RectTransform parent = pointer.parent as RectTransform; if (parent == null) return pointer.anchoredPosition; Vector3[] corners = new Vector3[4]; target.GetWorldCorners(corners); Vector3 worldCenter = (corners[0] + corners[2]) * 0.5f; RectTransformUtility.ScreenPointToLocalPointInRectangle( parent, RectTransformUtility.WorldToScreenPoint(null, worldCenter), null, out Vector2 localPos ); return new Vector2( _baseX.x + offset.x, localPos.y + offset.y ); } private void ShowPointer() { if (pointer && !pointer.gameObject.activeSelf) pointer.gameObject.SetActive(true); } } }