using UnityEngine; using UnityEngine.EventSystems; 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; [Header("Selection Tracking")] [Tooltip("Задержка перед скрытием указателя, если выбранный элемент временно не найден.")] [SerializeField] private float unmatchedHideDelay = 0.15f; // ── Состояние ── private RectTransform _currentTarget; private Vector2 _targetPos; private Vector2 _baseX; private bool _ready; private bool _followSelection; private Transform _selectionScope; private bool _hideWhenSelectionUnmatched; private readonly System.Collections.Generic.List _selectionBindings = new(); private float _unmatchedSelectionTime; private sealed class SelectionBinding { public Transform SelectionRoot; public RectTransform PointerTarget; public bool HidePointer; } // ────────────── Публичный 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; } /// /// Переключает указатель в режим следования за выбранным UI-элементом. /// public void FollowSelection(Transform scope, bool hideWhenSelectionUnmatched = false) { _selectionScope = scope; _hideWhenSelectionUnmatched = hideWhenSelectionUnmatched; _followSelection = true; _unmatchedSelectionTime = 0f; } /// /// Отключает режим следования за выбранным UI-элементом. /// public void StopFollowingSelection() { _followSelection = false; _selectionScope = null; _hideWhenSelectionUnmatched = false; _unmatchedSelectionTime = 0f; } /// /// Очищает runtime-привязки выбранных элементов к строкам указателя. /// public void ClearSelectionBindings() { _selectionBindings.Clear(); } /// /// Привязывает выбранный UI-элемент или его дочерние элементы к строке указателя. /// public void RegisterSelectionBinding(Transform selectionRoot, RectTransform pointerTarget) { if (selectionRoot == null) { return; } _selectionBindings.Add(new SelectionBinding { SelectionRoot = selectionRoot, PointerTarget = pointerTarget, HidePointer = false }); } /// /// Привязывает выбранный UI-элемент к скрытию указателя. /// public void RegisterHideBinding(Transform selectionRoot) { if (selectionRoot == null) { return; } _selectionBindings.Add(new SelectionBinding { SelectionRoot = selectionRoot, PointerTarget = null, HidePointer = true }); } // ────────────── Lifecycle ────────────── private void Awake() { if (pointer) _baseX = new Vector2(pointer.anchoredPosition.x, 0f); } private void LateUpdate() { UpdateSelectionTracking(); 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 void UpdateSelectionTracking() { if (!_followSelection) { return; } GameObject selected = EventSystem.current != null ? EventSystem.current.currentSelectedGameObject : null; if (selected == null) { HandleUnmatchedSelection(); return; } Transform selectedTransform = selected.transform; if (_selectionScope != null && !selectedTransform.IsChildOf(_selectionScope)) { HandleUnmatchedSelection(); return; } SelectionBinding binding = FindSelectionBinding(selectedTransform); if (binding != null) { _unmatchedSelectionTime = 0f; if (binding.HidePointer || binding.PointerTarget == null) { Hide(); return; } SetTarget(binding.PointerTarget); return; } RectTransform trackedTarget = FindTrackedTarget(selectedTransform); if (trackedTarget != null) { _unmatchedSelectionTime = 0f; SetTarget(trackedTarget); return; } HandleUnmatchedSelection(); } private SelectionBinding FindSelectionBinding(Transform selectedTransform) { for (int i = 0; i < _selectionBindings.Count; i++) { SelectionBinding binding = _selectionBindings[i]; if (binding?.SelectionRoot == null) { continue; } if (selectedTransform == binding.SelectionRoot || selectedTransform.IsChildOf(binding.SelectionRoot)) { return binding; } } return null; } private RectTransform FindTrackedTarget(Transform selectedTransform) { if (targets == null) { return null; } for (int i = 0; i < targets.Length; i++) { RectTransform target = targets[i]; if (target == null) { continue; } if (selectedTransform == target || selectedTransform.IsChildOf(target)) { return target; } } return null; } private void HandleUnmatchedSelection() { if (_hideWhenSelectionUnmatched) { _unmatchedSelectionTime += Time.unscaledDeltaTime; if (_unmatchedSelectionTime >= unmatchedHideDelay) { Hide(); } return; } _unmatchedSelectionTime = 0f; } 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); } } }