using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; namespace RR.Common.UI { public class UIPointer : MonoBehaviour { [Header("Настройки")] [SerializeField] private GameObject pointer; [SerializeField] private RectTransform content; // Content ScrollView, где находятся элементы [SerializeField] private float smoothSpeed = 10f; // Скорость перемещения указателя [SerializeField] private Vector2 offset = Vector2.zero; // Смещение указателя относительно элемента [Header("Поведение")] [Tooltip("Не скрывать указатель при отсутствии выбора (оставаться на последней позиции)")] [SerializeField] private bool keepVisibleWhenNoSelection = true; [Tooltip("Задержка перед скрытием указателя (в секундах)")] [SerializeField] private float hideDelay = 0.5f; [Header("Исключения")] [Tooltip("Элементы, при выборе которых курсор скрывается")] [SerializeField] private List hideOnSelectElements = new(); private RectTransform pointerRect; private float hideTimer; private bool hasEverSelected; // Был ли когда-либо выбран элемент private void Start() { if (pointer != null) { pointerRect = pointer.GetComponent(); if (pointerRect == null) { Debug.LogError("UIPointer: pointer не имеет RectTransform!"); } } else { Debug.LogError("UIPointer: pointer не назначен!"); } // Если content не задан, пытаемся найти его автоматически if (content == null) { content = GetComponent(); } hideTimer = hideDelay; } private void LateUpdate() { if (!pointerRect || !content) return; // Получаем текущий выбранный элемент из EventSystem GameObject selected = EventSystem.current?.currentSelectedGameObject; // Если ничего не выбрано if (selected == null) { HandleNoSelection(); return; } // Проверяем, находится ли выбранный элемент в списке исключений if (IsInHideList(selected)) { HidePointer(); return; } // Проверяем, что выбранный элемент находится внутри нашего content RectTransform selectedRect = selected.GetComponent(); if (!selectedRect || !selectedRect.IsChildOf(content)) { HandleNoSelection(); return; } // Сбрасываем таймер и отмечаем что был выбор hideTimer = hideDelay; hasEverSelected = true; // Показываем указатель, если он был скрыт if (!pointer.activeSelf) pointer.SetActive(true); // Вычисляем целевую позицию Vector2 targetPos = GetTargetPosition(selectedRect); // Плавно перемещаем указатель if (smoothSpeed > 0) { pointerRect.anchoredPosition = Vector2.Lerp( pointerRect.anchoredPosition, targetPos, Time.unscaledDeltaTime * smoothSpeed ); } else { pointerRect.anchoredPosition = targetPos; } } private void HandleNoSelection() { // Если включено сохранение видимости или ещё не было выбора - не скрываем if (keepVisibleWhenNoSelection || !hasEverSelected) return; // Уменьшаем таймер hideTimer -= Time.unscaledDeltaTime; if (hideTimer <= 0 && pointer.activeSelf) { pointer.SetActive(false); } } /// /// Проверяет, находится ли элемент в списке исключений или является его дочерним элементом /// private bool IsInHideList(GameObject obj) { if (hideOnSelectElements == null || hideOnSelectElements.Count == 0) return false; foreach (var hideElement in hideOnSelectElements) { if (hideElement == null) continue; // Проверяем сам элемент или его родителей if (obj == hideElement || obj.transform.IsChildOf(hideElement.transform)) return true; } return false; } /// /// Немедленно скрывает указатель /// private void HidePointer() { if (pointer != null && pointer.activeSelf) { pointer.SetActive(false); } } private Vector2 GetTargetPosition(RectTransform target) { // Получаем мировые координаты центра целевого элемента Vector3[] corners = new Vector3[4]; target.GetWorldCorners(corners); Vector3 worldCenter = (corners[0] + corners[2]) * 0.5f; // Конвертируем мировую позицию в локальную позицию родителя указателя RectTransform pointerParent = pointerRect.parent as RectTransform; if (pointerParent == null) { Debug.LogWarning("UIPointer: pointer не имеет RectTransform родителя!"); return pointerRect.anchoredPosition; } RectTransformUtility.ScreenPointToLocalPointInRectangle( pointerParent, RectTransformUtility.WorldToScreenPoint(null, worldCenter), null, out Vector2 localPos ); // X фиксирована, меняется только Y return new Vector2( pointerRect.anchoredPosition.x, // X остается как есть localPos.y + offset.y // Y от целевого элемента + смещение ); } // Метод для ручного обновления позиции указателя (если нужно) public void UpdatePointerPosition() { if (pointerRect == null || content == null) return; GameObject selected = EventSystem.current?.currentSelectedGameObject; if (selected == null) return; RectTransform selectedRect = selected.GetComponent(); if (selectedRect == null || !selectedRect.IsChildOf(content)) return; pointerRect.anchoredPosition = GetTargetPosition(selectedRect); } } }