Что это: Компонент очень гибок: он может работать как с реальными GameObject (активируя/деактивируя их), так и с виртуальным количеством элементов, просто отслеживая индекс. Он также поддерживает зациклив...
Как использовать: см. разделы ниже.
Selector — это универсальный компонент для управления выбором элементов из списка. Он позволяет легко переключаться между различными опциями, будь то вкладки UI, выбор персонажа, элементы инвентаря или узлы в дереве навыков.
Компонент очень гибок: он может работать как с реальными GameObject (активируя/деактивируя их), так и с виртуальным количеством элементов, просто отслеживая индекс. Он также поддерживает зацикливание и уникальный режим "заполнения", который активирует все элементы до текущего индекса.
- Пространство имен:
Neo.Tools - Путь к файлу:
Assets/Neoxider/Scripts/Tools/View/Selector.cs
Компонент, который управляет текущим индексом выбора и соответствующим образом активирует/деактивирует GameObject из своего списка или просто вызывает события, передавая текущий индекс.
_items(GameObject[]): МассивGameObject, которые будут управляться селектором. Если этот массив заполнен, селектор будет активировать/деактивировать объекты._count(int): Если_itemsпуст, селектор будет работать с этим числом как с виртуальным количеством элементов. Полезно, когда вам нужен только индекс, а не управлениеGameObject._setChild(bool): Одноразовое действие. Еслиtrue, компонент заполнит массив_itemsвсеми дочернимиGameObjectсвоего объекта, а затем вернет_setChildвfalse._autoUpdateFromChildren(bool, по умолчаниюtrue): Еслиtrue, компонент подписан на изменение дочерних объектов: автоматически поддерживает_itemsв актуальном состоянии (при добавлении/удалении детей) и при обновлении списка вызываетUpdateSelection(), чтобы отображение соответствовало текущему индексу. При старте (вOnEnable) список принудительно синхронизируется с дочерними объектами только если включёнstartOnAwake; при этом установка стартового индекса и применение выбора тоже выполняются только приstartOnAwake == true.
-
_loop(bool): Еслиtrue, при достижении конца списка селектор вернется в начало, и наоборот. -
_allowEmptyEffectiveIndex(bool, по умолчаниюfalse): Разрешить ли эффективному индексу быть-1(ничего не выбрано). Полезно для скинов/"пустого" состояния. Влияет на границы индекса с учетом_indexOffset. -
_useRandomSelection(bool, по умолчаниюfalse): Включает функциональность случайного выбора (например, методSetRandom()). -
_useNextPreviousAsRandom(bool, по умолчаниюfalse): Если включено вместе с_useRandomSelection, методыNext()иPrevious()будут работать как случайный выбор (вызыватьSetRandom()), а не шагать на +1/-1. -
_keepOthersActiveOnRandom(bool, по умолчаниюfalse): Еслиtrue, вызовSetRandom()без аргументов только включает выпавший элемент и не выключает остальные (эквивалентноSetRandom(false)).Set/Next/Previousпо-прежнему применяют обычное правило «один активен» (или fill). -
Уникальная выборка (по умолчанию выключена):
_uniqueSelectionMode(bool, по умолчаниюfalse): В режиме уникальной выборки при случайном выборе (и при ручномSet(index)) каждый индекс считается «использованным» и не повторяется до сброса или до конца цикла. Когда все индексы использованы, вызываетсяOnUniqueCycleComplete; при включённом_resetUniqueWhenCycleCompleteнабор сбрасывается и цикл начинается заново._resetUniqueWhenCycleComplete(bool, по умолчаниюtrue): После полного цикла в unique-режиме: еслиtrue— автоматически сбросить набор и снова выбирать случайно; еслиfalse— следующийSetRandom()ничего не сделает до вызоваResetUnique().
-
_fillMode(bool): Важная настройка!- Если
false(по умолчанию): Только один элемент в списке будет активен (тот, который соответствует_currentIndex). - Если
true: Все элементы от начала списка до_currentIndex(включительно) будут активны. Идеально для индикаторов прогресса, звезд рейтинга или разблокировки навыков.
- Если
-
_indexOffset(int): Смещение, которое добавляется к_currentIndexпри отображении или использовании. Полезно, если вы хотите, чтобы первый элемент отображался как "1" вместо "0". -
_notifySelectorItemsOnly(bool, по умолчаниюfalse): Если включено, на элементах с SelectorItem селектор вызываетSelectorItem.SetActive, а неGameObject.SetActiveна этомGameObject. У SelectorItem это логическое активное состояние (поле,ReactiveProperty, события) — не то же самое, что активность объекта в иерархии Unity. Удобно для сценария «аномалии»: визуал вешается на OnActivated / OnDeactivated или наActive.OnChanged. Если какой‑то элемент в массиве безSelectorItem, при включённом управлении селектор по‑прежнему может вызвать для негоGameObject.SetActive. -
_controlGameObjectActive(bool, по умолчаниюtrue): Разрешить прокидывать выбор в элементы. Еслиfalse, Selector не вызывает ниGameObject.SetActive, ниSelectorItem.SetActive; остаются только индекс, random/unique и событияOnSelectionChanged*. Чтобы работал режим «только SelectorItem», этот флаг должен бытьtrue— иначе вызовы к SelectorItem тоже отключаются (название в инспекторе вводит в заблуждение: это не «обязательно крутить активность GameObject»).
_saveEnabled(bool, по умолчаниюfalse): Включает сохранение состояния селектора черезSaveProvider. По умолчанию сохранение выключено, селектор работает как раньше._saveKey(string, по умолчанию"Selector"): Базовый ключ сохранения. Для разных селекторов важно задавать уникальные ключи. Внутри используются суффиксы_Indexи_Excluded._saveMode(SelectorSaveMode, флаги, по умолчаниюIndex | ExcludedIndices): Что именно сохранять (флаги можно снять/добавить в инспекторе):Index— сохраняется текущий индекс (Value) по ключу<SaveKey>_Index.ExcludedIndices— сохраняется набор исключённых индексов (ExcludeIndex/IncludeIndex/IncludeAllIndices) по ключу<SaveKey>_Excluded(список через запятую).
Item(GameObject): Получает текущий выбранныйGameObjectс учетом_indexOffset. Возвращаетnull, если нет элементов или индекс вне границ.Value(int): Текущий индекс выбора (get/set).Count(int): Количество доступных элементов (get/set). При установке автоматически обновляет выбор, если есть элементы.HasItems(bool): Возвращаетtrue, если селектор работает с массивомGameObject.IsAtStart(bool): Возвращаетtrue, если селектор находится в начале списка.IsAtEnd(bool): Возвращаетtrue, если селектор находится в конце списка.IndexWithOffset(int): Текущий индекс с учетом смещения_indexOffset.UniqueSelectionMode(bool): Включён ли режим уникальной выборки.UniqueRemainingCount(int): В unique-режиме — сколько индексов ещё не выбраны в текущем цикле; 0 при завершении цикла или при выключенном режиме.CountActive(int): Сколько элементов фактически включено. Если задан массив_itemsи виртуальный_countне используется (_count <= 0), считаются реальные состояния:GameObject.activeSelf, а в режиме Notify Selector Items Only при наличииSelectorItem—SelectorItem.ActiveValue. Если работаете только с виртуальным_countили без объектов — прежняя логическая формула (0/1 или длина префикса в fill). Подходит для условий вроде «сколько аномалий горит» при additive random. СобытиеOnCountActiveChangedвызывается при изменении этого числа.NotifySelectorItemsOnly(bool): Режим «только уведомлять SelectorItem» (get/set).ExcludedCount(int): Число индексов, исключённых из случайного пула.IsExcluded(int index): Возвращаетtrue, если индекс исключён из пула дляSetRandom().ExcludedIndices(IReadOnlyList<int>): Снимок исключённых индексов пула (виден в Inspector в списке).UsedIndicesForUnique(IReadOnlyList<int>): Снимок индексов, уже использованных в unique-selection режиме (debug, виден в Inspector).IncludedIndices(IReadOnlyList<int>): Снимок включённых индексов пула Random (все индексы из текущих границ, кроме исключённых), виден в Inspector.
Примечание: в Editor можно заполнить список исключённых индексов (через _excludedIndicesInspector), и при старте Selector создаст внутренний пул для Random. В Play Mode списки автоматически синхронизируются с внутренними HashSet.
- Для изменения исключений используйте методы
ExcludeIndex(int),IncludeIndex(int)иIncludeAllIndices(). Они обновляют внутреннийHashSetи синхронизируют инспекторные списки. - Если нужно задать исключения списком целиком, используйте
SetExcludedIndices(...)(полезно для сценариев «прочитал настройки/данные» и одним вызовом обновил пул).
Selector наследуется от NeoNetworkComponent, поэтому по умолчанию остаётся локальным/offline. Сетевая репликация используется только при isNetworked = true и активной Mirror-сессии.
AuthorityModeвыбирает, кто может менять селектор по сети:None(по умолчанию),OwnerOnly,ServerOnly.- Авторитетное состояние сервера включает текущий индекс,
FillMode, исключённые индексы random-пула и снимок активных элементов для additive random. - В host-режиме локальный RPC-echo пропускается, поэтому
OnSelectionChangedне должен срабатывать дважды на одно локальное действие. - Late-join клиенты применяют серверное SyncVar-состояние через
ApplyNetworkState().
Next(): Переключает выбор на следующий элемент. Безопасно обрабатывает пустые списки.Previous(): Переключает выбор на предыдущий элемент. Безопасно обрабатывает пустые списки.Set(int index): Устанавливает выбор на конкретный индекс. Безопасно обрабатывает пустые списки и некорректные индексы.SetRandom(): Устанавливает выбор на случайный индекс в текущем допустимом диапазоне (учитывает_keepOthersActiveOnRandom).SetRandom(bool deactivateOthers): Случайный индекс; еслиdeactivateOthers == true, невыбранные элементы выключаются (как обычный селектор); еслиfalse— включается только выпавший, остальные не трогаются. В режиме fill параметр игнорируется (всегда полное применение префикса).SetFirst(): Устанавливает выбор на первый элемент (индекс 0). Безопасно обрабатывает пустые списки.SetLast(): Устанавливает выбор на последний элемент. Безопасно обрабатывает пустые списки.GetSelectedItem(): Возвращает текущий выбранныйGameObjectилиnull.GetCurrentIndex(): Возвращает текущий индекс выбора.GetCount(): Возвращает количество доступных элементов.IsValidIndex(int index): Проверяет, является ли индекс валидным.ToggleFillMode(): Переключает режим_fillMode.Reset(): Сбрасывает выбор на начальный индекс (по умолчанию 0).ResetUnique(): Сбрасывает учёт «использованных» индексов в режиме уникальной выборки, чтобы случайный выбор снова мог повторяться. ВызываетOnUniqueReset.ResetAll(): ВызываетReset()иResetUnique()(индекс в начало + сброс unique-набора).ExcludeIndex(int index): Исключает индекс из пула дляSetRandom()(например, когда элемент «исправлен»). Исключённые индексы не выбираются, пока не вызваныIncludeIndexилиIncludeAllIndices.IncludeIndex(int index): Возвращает индекс в пул.IncludeAllIndices(): Очищает список исключённых — полный пул снова доступен дляSetRandom().ToggleIndex(int index, bool? state = null): Активирует/деактивируетGameObjectпо указанному индексу (с учетом_indexOffset).
OnSelectionChanged(UnityEvent<int>): Вызывается каждый раз, когда текущий выбор меняется. Передаёт новый индекс.OnFinished: Вызывается, когда селектор достигает конца списка (только если_loopвыключен).OnUniqueCycleComplete: Вызывается в режиме уникальной выборки, когда все индексы были выбраны по разу (цикл завершён). Если включён авто-сброс, после вызова набор сбрасывается.OnUniqueReset: Вызывается при вызовеResetUnique()(сброс учёта использованных индексов).OnSelectionChangedGameObject(UnityEvent<GameObject>): Вызывается при смене выбора, передаёт текущий выбранныйGameObject(илиnull).OnCountActiveChanged(UnityEvent<int>): Вызывается, когда меняется значениеCountActive(послеUpdateSelection, а также послеToggleIndex).
- Создайте пустой
GameObject(например,TabSelector). - Сделайте ваши вкладки UI (или другие
GameObject) дочерними по отношению кTabSelector. - Добавьте компонент
SelectorнаTabSelector. - В инспекторе
Selectorоставьте включенным_autoUpdateFromChildren(по умолчаниюtrue) — он автоматически заполнит_itemsи будет обновлять его при изменениях детей. Если нужно заполнить один раз вручную — включите_setChildили нажмитеRefreshItems(). - Привяжите кнопки "Вперед" и "Назад" к методам
Next()иPrevious()компонентаSelector. - К событию
OnSelectionChangedможно привязать логику, которая, например, обновляет текст заголовка вкладки.
- Создайте 5 звезд-изображений и сделайте их дочерними к
StarContainer. - Добавьте
SelectorнаStarContainer. - Включите
_setChildи_fillMode. - Теперь, если вы вызовете
Selector.Set(2), то активируются звезды с индексом 0, 1 и 2. - Чтобы значение 0 не активировало ни одной звезды, установите
_allowEmptyEffectiveIndex = trueи_indexOffset = -1— тогдаValue = 0даст пустое состояние.
- Добавьте
Selectorна любойGameObject. - Вместо заполнения
_items, установите_countв 5 (например). - Теперь
Selectorбудет отслеживать индекс от 0 до 4, вызываяOnSelectionChangedпри каждом изменении.
- Включите
_useRandomSelectionи при необходимости_useNextPreviousAsRandom. - Включите
_uniqueSelectionMode. При каждомSetRandom()(и при ручномSet(index)) индекс запоминается; повтор не будет до сброса или до конца цикла. - Когда все индексы использованы, вызывается
OnUniqueCycleComplete. Если_resetUniqueWhenCycleComplete = true, набор сбрасывается и цикл начинается заново; иначе следующийSetRandom()ничего не сделает до вызоваResetUnique(). - Вызовите
ResetUnique()(илиResetAll()), чтобы снова разрешить повторы. В Inspector доступны кнопки Reset Unique и Reset All.
- Создайте селектор для скинов персонажа с возможностью "без скина".
- Добавьте
Selectorна контейнер со скинами. - Включите
_allowEmptyEffectiveIndex = trueи установите_indexOffset = -1. - Теперь
Value = 0дастeffectiveIndex = -1(ничего не выбрано), и все элементы будут деактивированы. Value = 1дастeffectiveIndex = 0(первый скин),Value = 2дастeffectiveIndex = 1(второй скин) и т.д.- Используйте
SetFirst()для установки на минимальное значение (может быть отрицательным).
- Создайте родительский объект и сделайте дочерними объекты-аномалии (каждый с компонентом SelectorItem).
- Добавьте Selector на родителя. Включите
_useRandomSelection, при необходимости_uniqueSelectionMode. Включите_notifySelectorItemsOnlyи оставьте_controlGameObjectActiveвключённым: тогда на каждом дочернем объекте с SelectorItem будет вызываться толькоSelectorItem.SetActive, безGameObject.SetActiveсо стороны селектора. - Настройте TimerObject с интервалом появления (например,
useRandomDurationс timeMin/timeMax). В OnTimerCompleted вызовите Selector.SetRandom() — таймер сам вызывает команду выбора следующей аномалии. - На каждом SelectorItem подпишитесь на OnActivated (показать аномалию) и при «исправлении» вызовите ExcludeFromSelector(), чтобы исключить этот индекс из пула до сброса.
- Условие поражения: NeoCondition по полю Selector.CountActive (например,
CountActive >= 4→ поражение). См. также Examples/AnomalyGame.md. - Для сценария «новый день» без отдельного скрипта используйте уже имеющиеся возможности Selector: в конце дня или при сбросе уровня вызывайте Selector.IncludeAllIndices() и при необходимости Selector.ResetAll(). Если аномалия уже была исправлена в течение дня, исключайте её через SelectorItem.ExcludeFromSelector() или Selector.ExcludeIndex(int).
Компонент Selector был улучшен для обеспечения безопасной работы в различных сценариях:
- Все методы проверяют наличие элементов перед выполнением операций.
- Свойство
ItemпроверяетHasItemsперед доступом к массиву_items. - Методы
Next(),Previous(),Set(),SetFirst(),SetLast()безопасно обрабатывают случаи, когдаCount == 0.
Start()вызываетUpdateSelection()только еслиstartOnAwake == trueиCount > 0. При выключенномstartOnAwakeобъекты не переключаются при старте.OnEnable()вызываетSet(_startIndex)только еслиstartOnAwake == trueиCount > 0.- При изменении дочерних объектов (
RefreshItemsFromChildren) в Play Mode список_itemsсинхронизируется всегда, аUpdateSelection()(активация /SelectorItem) вызывается только еслиstartOnAwake == trueили вы явно вызвалиRefreshItems()в инспекторе (оно передаёт принудительное применение выбора). ПриstartOnAwake == falseавто-синхронизация детей не переключает элементы, пока вы не вызовете, например,Set/Next/SetRandom. Countsetter обновляет выбор только если есть доступные элементы.
UpdateSelection()сохраняет ссылку на массив_itemsв локальную переменную для защиты от изменения массива во время выполнения цикла.- Все обращения к элементам массива проверяют на
nullперед использованием.
GetCurrentBounds()возвращает безопасные значения(0, 0)еслиCount <= 0.GetSelectedItem()используетCountвместо_items.Lengthдля проверки границ в виртуальном режиме.- Свойство
Itemучитывает_indexOffsetпри вычислении эффективного индекса. - Отрицательные индексы: При
_allowEmptyEffectiveIndex == trueэффективный индекс может быть-1(ничего не выбрано). В этом случае:UpdateSelection()деактивирует все элементыGameObject, независимо от режима_fillMode.- Свойство
Itemи методGetSelectedItem()возвращаютnullприeffectiveIndex == -1. SetFirst()устанавливает индекс на минимальное допустимое значение (может быть отрицательным с учетом_indexOffset).IsValidIndex()корректно проверяет валидность индексов, включая отрицательные значения в допустимом диапазоне.
OnValidate()вызываетUpdateSelection()только в режиме воспроизведения (Application.isPlaying), чтобы избежать побочных эффектов в редакторе.
Все предупреждения теперь содержат подробную информацию о причине ошибки:
- "Cannot update selection - items array is null or empty, or count is 0"
- "Cannot move to next - no items available (items array is null/empty or count is 0)"
- И другие аналогичные сообщения для всех методов.
- Инициализация: Убедитесь, что
_itemsзаполнен или_count > 0перед использованием компонента. - Виртуальный режим: При использовании виртуального счетчика (
_count > 0), методы, работающие сGameObject, будут возвращатьnullили не выполнять операции. - Проверка перед использованием: Используйте
HasItemsдля проверки наличияGameObjectперед обращением к свойствуItem. - Валидация индексов: Используйте
IsValidIndex()перед вызовомSet()с пользовательским индексом. Этот метод учитывает_allowEmptyEffectiveIndexи_indexOffset, поэтому может возвращатьtrueдля отрицательных индексов, если они находятся в допустимом диапазоне. - Отрицательные индексы: При использовании
_allowEmptyEffectiveIndex == true:- Эффективный индекс
-1означает "ничего не выбрано" и все элементы будут деактивированы. - Свойство
ItemиGetSelectedItem()вернутnullприeffectiveIndex == -1. - Используйте
GetCurrentBounds()для получения допустимого диапазона индексов, который может включать отрицательные значения.
- Эффективный индекс