#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION using AlicizaX.UI.Runtime; using UnityEngine.EventSystems; namespace UnityEngine.UI { [DisallowMultipleComponent] [AddComponentMenu("UI/UX Navigation Scope")] public sealed class UXNavigationScope : MonoBehaviour, ISelectHandler { private const int InvalidIndex = -1; private const int DefaultRuntimeSelectableCapacity = 16; [SerializeField, Header("默认选中控件")] private Selectable _defaultSelectable; [SerializeField, Header("编辑器烘焙导航控件")] private Selectable[] _bakedSelectables = System.Array.Empty(); [SerializeField, Header("动态控件容量")] private int _runtimeSelectableCapacity = DefaultRuntimeSelectableCapacity; [SerializeField, Header("记住上次选中")] private bool _rememberLastSelection = true; [SerializeField, Header("手柄/键盘模式必须有选中")] private bool _requireSelectionWhenGamepad = true; [SerializeField, Header("阻断下层导航域")] private bool _blockLowerScopes = true; [SerializeField, Header("自动选中首个可用控件")] private bool _autoSelectFirstAvailable = true; private Selectable[] _runtimeSelectables; private Navigation[] _bakedBaselineNavigation; private Navigation[] _runtimeBaselineNavigation; private Canvas _cachedCanvas; private UIHolderObjectBase _cachedHolder; private Selectable _lastSelected; private bool _navigationSuppressed; private int _cachedHierarchyDepth = -1; private bool _cachedIsSkipped; private bool _isSkippedCacheValid; private int _runtimeSelectableCount; private int[] _bakedSelectableHashIds = System.Array.Empty(); private int[] _bakedSelectableHashIndices = System.Array.Empty(); private int[] _runtimeSelectableHashIds = System.Array.Empty(); private int[] _runtimeSelectableHashIndices = System.Array.Empty(); private bool _selectableSetDirty = true; private bool _selectableAvailabilityDirty = true; private int _availableSelectableCount; private Selectable _firstAvailableSelectable; internal int RuntimeIndex { get; set; } = InvalidIndex; internal ulong ActivationSerial { get; set; } internal bool IsAvailable { get; set; } internal bool WasAvailable { get; set; } public bool NavigationSuppressed => _navigationSuppressed; internal int BakedSelectableCount => _bakedSelectables != null ? _bakedSelectables.Length : 0; public int RuntimeSelectableCount => _runtimeSelectableCount; public Selectable DefaultSelectable { get => _defaultSelectable; set { _defaultSelectable = value; MarkSelectableAvailabilityDirty(); MarkRuntimeStateDirty(); } } public bool RememberLastSelection => _rememberLastSelection; public bool RequireSelectionWhenGamepad => _requireSelectionWhenGamepad; public bool BlockLowerScopes => _blockLowerScopes; internal bool IsNavigationSkipped { get { if (!_isSkippedCacheValid) { _cachedIsSkipped = HasSkipInParents(); _isSkippedCacheValid = true; } return _cachedIsSkipped; } } private bool HasSkipInParents() { Transform current = transform; while (current != null) { if (current.TryGetComponent(out UXNavigationSkip skip)) { return true; } current = current.parent; } return false; } internal Canvas Canvas { get { if (_cachedCanvas == null) { _cachedCanvas = GetComponentInParent(true); } return _cachedCanvas; } } internal UIHolderObjectBase Holder { get { if (_cachedHolder == null) { _cachedHolder = GetComponent(); if (_cachedHolder == null) { _cachedHolder = GetComponentInParent(true); } } return _cachedHolder; } } private void Awake() { EnsureRuntimeBuffers(false); RefreshBaselineWhenUnsuppressed(); } private void OnEnable() { EnsureRuntimeBuffers(false); RefreshBaselineWhenUnsuppressed(); UXNavigationRuntime.EnsureInstance().RegisterScope(this); } private void OnDisable() { SetNavigationSuppressed(false); if (UXNavigationRuntime.TryGetInstance(out var runtime)) { runtime.UnregisterScope(this); } } private void OnDestroy() { SetNavigationSuppressed(false); } private void OnTransformChildrenChanged() { MarkSelectableAvailabilityDirty(); MarkRuntimeStateDirty(); } private void OnTransformParentChanged() { _cachedCanvas = null; _cachedHolder = null; _cachedHierarchyDepth = -1; _isSkippedCacheValid = false; MarkRuntimeStateDirty(); } #if UNITY_EDITOR private void OnValidate() { if (_runtimeSelectableCapacity < 0) { _runtimeSelectableCapacity = 0; } } #endif public bool RegisterSelectable(Selectable selectable) { if (selectable == null || !Owns(selectable.gameObject) || ContainsSelectable(selectable)) { return false; } EnsureRuntimeBuffers(true); if (_runtimeSelectableCount >= _runtimeSelectables.Length) { ReportCapacityExceeded(); return false; } int index = _runtimeSelectableCount++; _runtimeSelectables[index] = selectable; _runtimeBaselineNavigation[index] = selectable.navigation; MarkSelectableSetDirty(); if (_navigationSuppressed) { SetSelectableSuppressed(selectable); } MarkRuntimeStateDirty(); return true; } public bool UnregisterSelectable(Selectable selectable) { if (selectable == null || _runtimeSelectables == null) { return false; } int index = FindRuntimeIndex(selectable); if (index < 0) { return false; } if (_navigationSuppressed) { selectable.navigation = _runtimeBaselineNavigation[index]; } int last = --_runtimeSelectableCount; Selectable movedSelectable = _runtimeSelectables[last]; Navigation movedNavigation = _runtimeBaselineNavigation[last]; _runtimeSelectables[last] = null; _runtimeBaselineNavigation[last] = default(Navigation); if (index != last) { _runtimeSelectables[index] = movedSelectable; _runtimeBaselineNavigation[index] = movedNavigation; } if (_lastSelected == selectable) { _lastSelected = null; } MarkSelectableSetDirty(); MarkRuntimeStateDirty(); return true; } public void InvalidateSelectableCache() { if (_navigationSuppressed) { ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, false); ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, false); CaptureBaselineBeforeSuppress(); ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, true); ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, true); } else { RefreshBaselineWhenUnsuppressed(); } MarkRuntimeStateDirty(); } public void OnSelect(BaseEventData eventData) { GameObject selectedObject = eventData != null ? eventData.selectedObject : null; UXNavigationRuntime.NotifySelection(selectedObject); } internal void InvalidateSkipCacheOnly() { _isSkippedCacheValid = false; } internal int GetHierarchyDepth() { if (_cachedHierarchyDepth >= 0) { return _cachedHierarchyDepth; } int depth = 0; Transform current = transform; while (current != null) { depth++; current = current.parent; } _cachedHierarchyDepth = depth; return _cachedHierarchyDepth; } internal bool Owns(GameObject target) { if (target == null) { return false; } UXNavigationScope nearestScope = target.GetComponentInParent(true); return nearestScope == this; } internal Selectable GetPreferredSelectable() { RefreshSelectableAvailabilityIfDirty(); if (_rememberLastSelection && IsSelectableValid(_lastSelected)) { return _lastSelected; } if (IsSelectableValid(_defaultSelectable)) { return _defaultSelectable; } return _autoSelectFirstAvailable ? _firstAvailableSelectable : null; } internal bool HasAvailableSelectable() { RefreshSelectableAvailabilityIfDirty(); if (IsSelectableValid(_defaultSelectable)) { return true; } return _autoSelectFirstAvailable && _availableSelectableCount > 0; } internal void RecordSelection(GameObject selectedObject) { if (!_rememberLastSelection || selectedObject == null) { return; } Selectable selectable = GetSelectableFromObject(selectedObject); if (IsSelectableValid(selectable)) { _lastSelected = selectable; } } internal bool IsSelectableOwnedAndValid(GameObject selectedObject) { return IsSelectableValid(GetSelectableFromObject(selectedObject)); } internal void SetNavigationSuppressed(bool suppressed) { if (_navigationSuppressed == suppressed) { return; } if (suppressed) { CaptureBaselineBeforeSuppress(); } _navigationSuppressed = suppressed; ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, suppressed); ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, suppressed); } private void EnsureRuntimeBuffers(bool preserveRuntimeSelectables) { int capacity = _runtimeSelectableCapacity > 0 ? _runtimeSelectableCapacity : 0; if (_runtimeSelectables == null || _runtimeSelectables.Length != capacity) { Selectable[] previousSelectables = _runtimeSelectables; Navigation[] previousBaseline = _runtimeBaselineNavigation; int previousCount = _runtimeSelectableCount; _runtimeSelectables = capacity > 0 ? new Selectable[capacity] : System.Array.Empty(); _runtimeBaselineNavigation = capacity > 0 ? new Navigation[capacity] : System.Array.Empty(); CreateRuntimeHash(capacity); _runtimeSelectableCount = 0; if (preserveRuntimeSelectables && previousSelectables != null && capacity > 0) { int copyCount = previousCount < capacity ? previousCount : capacity; for (int i = 0; i < copyCount; i++) { Selectable selectable = previousSelectables[i]; if (selectable == null) { continue; } _runtimeSelectables[_runtimeSelectableCount] = selectable; _runtimeBaselineNavigation[_runtimeSelectableCount] = previousBaseline != null && i < previousBaseline.Length ? previousBaseline[i] : selectable.navigation; _runtimeSelectableCount++; } } MarkSelectableSetDirty(); } int bakedCount = BakedSelectableCount; if (_bakedBaselineNavigation == null || _bakedBaselineNavigation.Length != bakedCount) { _bakedBaselineNavigation = bakedCount > 0 ? new Navigation[bakedCount] : System.Array.Empty(); CreateBakedHash(bakedCount); MarkSelectableSetDirty(); } } private void CaptureBaselineBeforeSuppress() { EnsureRuntimeBuffers(true); RefreshSelectableHashesIfDirty(); CaptureBaseline(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount); CaptureBaseline(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount); } private void RefreshBaselineWhenUnsuppressed() { if (_navigationSuppressed) { return; } CaptureBaselineBeforeSuppress(); } private static void CaptureBaseline(Selectable[] selectables, Navigation[] baseline, int count) { if (selectables == null || baseline == null) { return; } for (int i = 0; i < count; i++) { Selectable selectable = selectables[i]; if (selectable != null) { baseline[i] = selectable.navigation; } } } private static void ApplySuppression(Selectable[] selectables, Navigation[] baseline, int count, bool suppressed) { if (selectables == null || baseline == null) { return; } for (int i = 0; i < count; i++) { Selectable selectable = selectables[i]; if (selectable == null) { continue; } if (suppressed) { SetSelectableSuppressed(selectable); } else { selectable.navigation = baseline[i]; } } } private static void SetSelectableSuppressed(Selectable selectable) { if (selectable == null) { return; } Navigation navigation = selectable.navigation; navigation.mode = Navigation.Mode.None; selectable.navigation = navigation; } private bool ContainsSelectable(Selectable selectable) { if (selectable == null) { return false; } RefreshSelectableHashesIfDirty(); int instanceId = selectable.GetInstanceID(); return FindHashIndex(_bakedSelectableHashIds, _bakedSelectableHashIndices, instanceId) >= 0 || FindHashIndex(_runtimeSelectableHashIds, _runtimeSelectableHashIndices, instanceId) >= 0; } private bool IsSelectableValid(Selectable selectable) { return IsSelectableUsable(selectable) && ContainsSelectable(selectable); } private void MarkSelectableSetDirty() { _selectableSetDirty = true; MarkSelectableAvailabilityDirty(); } private void MarkSelectableAvailabilityDirty() { _selectableAvailabilityDirty = true; } public void NotifySelectableStateChanged() { MarkSelectableAvailabilityDirty(); MarkRuntimeStateDirty(); } private void RefreshSelectableAvailabilityIfDirty() { if (!_selectableAvailabilityDirty) { return; } _availableSelectableCount = 0; _firstAvailableSelectable = null; AccumulateAvailableSelectables(_bakedSelectables, BakedSelectableCount); AccumulateAvailableSelectables(_runtimeSelectables, _runtimeSelectableCount); _selectableAvailabilityDirty = false; } private void AccumulateAvailableSelectables(Selectable[] selectables, int count) { if (selectables == null) { return; } for (int i = 0; i < count; i++) { Selectable selectable = selectables[i]; if (!IsSelectableUsable(selectable)) { continue; } if (_firstAvailableSelectable == null) { _firstAvailableSelectable = selectable; } _availableSelectableCount++; } } private void RefreshSelectableHashesIfDirty() { if (!_selectableSetDirty) { return; } RebuildBakedHash(); RebuildRuntimeHash(); _selectableSetDirty = false; } private void RebuildBakedHash() { ClearHash(_bakedSelectableHashIds, _bakedSelectableHashIndices); for (int i = 0; i < BakedSelectableCount; i++) { Selectable selectable = _bakedSelectables[i]; if (selectable != null) { AddHash(_bakedSelectableHashIds, _bakedSelectableHashIndices, selectable.GetInstanceID(), i); } } } private void RebuildRuntimeHash() { ClearHash(_runtimeSelectableHashIds, _runtimeSelectableHashIndices); for (int i = 0; i < _runtimeSelectableCount; i++) { Selectable selectable = _runtimeSelectables[i]; if (selectable != null) { AddHash(_runtimeSelectableHashIds, _runtimeSelectableHashIndices, selectable.GetInstanceID(), i); } } } private void CreateBakedHash(int itemCapacity) { int hashCapacity = GetHashCapacity(itemCapacity); _bakedSelectableHashIds = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty(); _bakedSelectableHashIndices = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty(); } private void CreateRuntimeHash(int itemCapacity) { int hashCapacity = GetHashCapacity(itemCapacity); _runtimeSelectableHashIds = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty(); _runtimeSelectableHashIndices = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty(); } private static int GetHashCapacity(int itemCapacity) { if (itemCapacity <= 0) { return 0; } int hashCapacity = 1; int required = itemCapacity << 1; while (hashCapacity < required) { hashCapacity <<= 1; } return hashCapacity; } private static void ClearHash(int[] ids, int[] indices) { if (ids == null || indices == null) { return; } for (int i = 0; i < ids.Length; i++) { ids[i] = 0; indices[i] = InvalidIndex; } } private static int FindHashIndex(int[] ids, int[] indices, int instanceId) { if (ids == null || indices == null || ids.Length == 0 || instanceId == 0) { return InvalidIndex; } int mask = ids.Length - 1; int index = instanceId & mask; for (int i = 0; i < ids.Length; i++) { int storedId = ids[index]; if (storedId == 0) { return InvalidIndex; } if (storedId == instanceId) { return indices[index]; } index = (index + 1) & mask; } return InvalidIndex; } private static void AddHash(int[] ids, int[] indices, int instanceId, int selectableIndex) { if (ids == null || indices == null || ids.Length == 0 || instanceId == 0) { return; } int mask = ids.Length - 1; int index = instanceId & mask; for (int i = 0; i < ids.Length; i++) { int storedId = ids[index]; if (storedId == 0 || storedId == instanceId) { ids[index] = instanceId; indices[index] = selectableIndex; return; } index = (index + 1) & mask; } } private int FindRuntimeIndex(Selectable selectable) { if (selectable == null) { return InvalidIndex; } RefreshSelectableHashesIfDirty(); return FindHashIndex(_runtimeSelectableHashIds, _runtimeSelectableHashIndices, selectable.GetInstanceID()); } private static int IndexOf(Selectable[] selectables, int count, Selectable selectable) { if (selectables == null || selectable == null) { return InvalidIndex; } for (int i = 0; i < count; i++) { if (selectables[i] == selectable) { return i; } } return InvalidIndex; } private static bool IsSelectableUsable(Selectable selectable) { return selectable != null && selectable.IsActive() && selectable.IsInteractable(); } private static Selectable GetSelectableFromObject(GameObject selectedObject) { if (selectedObject == null) { return null; } return selectedObject.TryGetComponent(out Selectable selectable) ? selectable : selectedObject.GetComponentInParent(); } private void MarkRuntimeStateDirty() { if (UXNavigationRuntime.TryGetInstance(out var runtime)) { runtime.MarkStateDirty(); } } private static void ReportCapacityExceeded() { #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.LogError("UXNavigationScope runtime selectable capacity exceeded."); #endif } } } #endif