using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; namespace AlicizaX.UI.Extension { public class UXSelectable : UIBehaviour, IMoveHandler, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler, IPointerExitHandler, ISelectHandler, IDeselectHandler { protected static UXSelectable[] s_Selectables = new UXSelectable[10]; protected static int s_SelectableCount = 0; protected int m_CurrentIndex = -1; protected bool m_EnableCalled = false; protected bool isPointerInside { get; set; } protected bool isPointerDown { get; set; } protected bool hasSelection { get; set; } protected bool m_GroupsAllowInteraction = true; private readonly List m_CanvasGroupCache = new List(); [SerializeField] private UXNavigation m_Navigation = UXNavigation.defaultNavigation; public UXNavigation navigation { get { return m_Navigation; } set { if (SetPropertyUtility.SetStruct(ref m_Navigation, value)) OnSetProperty(); } } public static UXSelectable[] allSelectablesArray { get { UXSelectable[] temp = new UXSelectable[s_SelectableCount]; Array.Copy(s_Selectables, temp, s_SelectableCount); return temp; } } protected virtual void OnSetProperty() { // override if needed } protected override void OnEnable() { if (m_EnableCalled) return; base.OnEnable(); if (s_SelectableCount == s_Selectables.Length) { UXSelectable[] temp = new UXSelectable[s_Selectables.Length * 2]; Array.Copy(s_Selectables, temp, s_Selectables.Length); s_Selectables = temp; } if (EventSystem.current && EventSystem.current.currentSelectedGameObject == gameObject) { hasSelection = true; } m_CurrentIndex = s_SelectableCount; s_Selectables[m_CurrentIndex] = this; s_SelectableCount++; isPointerDown = false; m_GroupsAllowInteraction = ParentGroupAllowsInteraction(); m_EnableCalled = true; } protected override void OnDisable() { if (!m_EnableCalled) return; s_SelectableCount--; s_Selectables[s_SelectableCount].m_CurrentIndex = m_CurrentIndex; s_Selectables[m_CurrentIndex] = s_Selectables[s_SelectableCount]; s_Selectables[s_SelectableCount] = null; base.OnDisable(); m_EnableCalled = false; } protected void OnCanvasGroupChanged() { var parentGroupAllows = ParentGroupAllowsInteraction(); if (parentGroupAllows != m_GroupsAllowInteraction) { m_GroupsAllowInteraction = parentGroupAllows; OnSetProperty(); } } protected bool ParentGroupAllowsInteraction() { Transform t = transform; while (t != null) { t.GetComponents(m_CanvasGroupCache); for (var i = 0; i < m_CanvasGroupCache.Count; i++) { if (m_CanvasGroupCache[i].enabled && !m_CanvasGroupCache[i].interactable) return false; if (m_CanvasGroupCache[i].ignoreParentGroups) return true; } t = t.parent; } return true; } public virtual bool IsInteractable() { return m_GroupsAllowInteraction; } public UXSelectable FindSelectable(Vector3 dir) { dir = dir.normalized; Vector3 localDir = Quaternion.Inverse(transform.rotation) * dir; Vector3 pos = transform.TransformPoint(GetPointOnRectEdge(transform as RectTransform, localDir)); float maxScore = Mathf.NegativeInfinity; float maxFurthestScore = Mathf.NegativeInfinity; float score = 0; bool wantsWrapAround = navigation.wrapAround && (m_Navigation.mode == UXNavigation.Mode.Vertical || m_Navigation.mode == UXNavigation.Mode.Horizontal); UXSelectable bestPick = null; UXSelectable bestFurthestPick = null; for (int i = 0; i < s_SelectableCount; ++i) { UXSelectable sel = s_Selectables[i]; if (sel == this) continue; if (!sel.IsInteractable() || sel.navigation.mode == UXNavigation.Mode.None) continue; #if UNITY_EDITOR if (Camera.current != null && !UnityEditor.SceneManagement.StageUtility.IsGameObjectRenderedByCamera(sel.gameObject, Camera.current)) continue; #endif var selRect = sel.transform as RectTransform; Vector3 selCenter = selRect != null ? (Vector3)selRect.rect.center : Vector3.zero; Vector3 myVector = sel.transform.TransformPoint(selCenter) - pos; float dot = Vector3.Dot(dir, myVector); if (wantsWrapAround && dot < 0) { score = -dot * myVector.sqrMagnitude; if (score > maxFurthestScore) { maxFurthestScore = score; bestFurthestPick = sel; } continue; } if (dot <= 0) continue; score = dot / myVector.sqrMagnitude; if (score > maxScore) { maxScore = score; bestPick = sel; } } if (wantsWrapAround && null == bestPick) return bestFurthestPick; return bestPick; } private static Vector3 GetPointOnRectEdge(RectTransform rect, Vector2 dir) { if (rect == null) return Vector3.zero; if (dir != Vector2.zero) dir /= Mathf.Max(Mathf.Abs(dir.x), Mathf.Abs(dir.y)); dir = rect.rect.center + Vector2.Scale(rect.rect.size, dir * 0.5f); return dir; } void Navigate(AxisEventData eventData, UXSelectable sel) { if (sel != null && sel.IsActive()) eventData.selectedObject = sel.gameObject; } public virtual UXSelectable FindSelectableOnLeft() { if (m_Navigation.mode == UXNavigation.Mode.Explicit) return m_Navigation.selectOnLeft; if ((m_Navigation.mode & UXNavigation.Mode.Horizontal) != 0) return FindSelectable(transform.rotation * Vector3.left); return null; } public virtual UXSelectable FindSelectableOnRight() { if (m_Navigation.mode == UXNavigation.Mode.Explicit) return m_Navigation.selectOnRight; if ((m_Navigation.mode & UXNavigation.Mode.Horizontal) != 0) return FindSelectable(transform.rotation * Vector3.right); return null; } public virtual UXSelectable FindSelectableOnUp() { if (m_Navigation.mode == UXNavigation.Mode.Explicit) return m_Navigation.selectOnUp; if ((m_Navigation.mode & UXNavigation.Mode.Vertical) != 0) return FindSelectable(transform.rotation * Vector3.up); return null; } public virtual UXSelectable FindSelectableOnDown() { if (m_Navigation.mode == UXNavigation.Mode.Explicit) return m_Navigation.selectOnDown; if ((m_Navigation.mode & UXNavigation.Mode.Vertical) != 0) return FindSelectable(transform.rotation * Vector3.down); return null; } public virtual void OnMove(AxisEventData eventData) { switch (eventData.moveDir) { case MoveDirection.Right: Navigate(eventData, FindSelectableOnRight()); break; case MoveDirection.Up: Navigate(eventData, FindSelectableOnUp()); break; case MoveDirection.Left: Navigate(eventData, FindSelectableOnLeft()); break; case MoveDirection.Down: Navigate(eventData, FindSelectableOnDown()); break; } } public virtual void OnPointerDown(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) return; if (IsInteractable() && navigation.mode != UXNavigation.Mode.None && EventSystem.current != null) EventSystem.current.SetSelectedGameObject(gameObject, eventData); } public virtual void OnPointerUp(PointerEventData eventData) { } public virtual void OnPointerEnter(PointerEventData eventData) { } public virtual void OnPointerExit(PointerEventData eventData) { } public virtual void OnSelect(BaseEventData eventData) { hasSelection = true; } public virtual void OnDeselect(BaseEventData eventData) { hasSelection = false; } } }