Перейти к основному содержимому

Система меню

Система меню построена на компоненте MenuSelector -- универсальном, переиспользуемом контроллере, который обрабатывает навигацию курсора, ввод, аудио и стрелки-указатели для любого списка или сетки кнопок. Каждое игровое меню (меню паузы, боевое меню, магазин, декс, опции и т. д.) наследуется от него.

Расположение: Assets/OpenMon/Core/Runtime/UI/MenuSelector.cs Пространство имён: OpenMon.MCE.UI

MenuSelector расширяет HidableUiElement<MenuSelector> и реализует IInputReceiver. Он управляет списком дочерних MenuItem и предоставляет:

  • Вертикальную или горизонтальную навигацию
  • Циклическое перемещение курсора
  • Анимированную стрелку-указатель (встроенный в MCE API твинов — сторонние ассеты не нужны)
  • Удержание-для-прокрутки для быстрой навигации
  • Аудиообратную связь при перемещении и выборе
  • События для выбора, наведения и возврата

Ключевые сериализуемые поля​

[SerializeField] protected int DefaultSelected;          // Starting cursor position
[SerializeField] private bool RequestInputOnShow = true; // Auto-request input focus
[SerializeField] protected Navigation MenuNavigation; // Vertical or Horizontal
[SerializeField] protected bool InvertedControls = true; // Invert nav direction
[SerializeField] private bool HideWhenChosen = true; // Auto-hide on select
[SerializeField] private bool NoChoosing; // Browse-only mode
[SerializeField] public bool NoGoingBack; // Disable back button
[SerializeField] public bool ShowSelectorArrow = true; // Show animated arrow
[SerializeField] public bool CanHoldToScrollFaster; // Hold-to-repeat
[SerializeField] public float HoldNavigationInterval; // Repeat rate (seconds)
public List<MenuItem> MenuOptions = new(); // The button list

События​

СобытиеТипСрабатывает когда
OnButtonSelectedAction<int>Игрок подтверждает выбор (индекс выбранного элемента)
OnHoveredAction<int>Курсор перемещается на новый элемент
OnBackSelectedActionИгрок нажимает кнопку назад/отмены

Жизненный цикл​

Show(true)
└─ RequestInput() ──► IInputManager pushes this onto the input stack
└─ OnStateEnter()
├─ Instantiates selector arrow prefab
├─ Starts HoldNavigation coroutine
└─ Selects DefaultSelected after one frame

Show(false)
└─ ReleaseInput() ──► IInputManager pops this from the input stack
└─ OnStateExit()
├─ Stops HoldNavigation coroutine
├─ Deselects current item
└─ Destroys selector arrow

Поток навигации​

Когда игрок нажимает направление:

  1. OnNavigation() читает вектор ввода и извлекает соответствующую ось (Y для вертикали, X для горизонтали).
  2. Navigate() увеличивает или уменьшает индекс выбора, зацикливаясь на границах.
  3. Невзаимодействуемые кнопки пропускаются автоматически.
  4. Select() вызывается на новом индексе, что:
    • Снимает выбор с предыдущего элемента
    • Воспроизводит аудио навигации
    • Обновляет CurrentSelection
    • Перемещает стрелку-указатель через API OpenMon.Foundation.Tweening
    • Срабатывает OnHovered
    • Вызывает MenuItem.OnSelect() на новой кнопке
// Simplified navigation logic
protected virtual void Navigate(float input)
{
if (InvertedControls) input *= -1;

int newSelection = CurrentSelection;

// Find next interactable item, wrapping around
switch (input)
{
case > 0: newSelection = (newSelection + 1) % MenuOptions.Count; break;
case < 0: newSelection = (newSelection - 1 + MenuOptions.Count) % MenuOptions.Count; break;
}

Select(newSelection);
}

Удержание-для-прокрутки​

Когда CanHoldToScrollFaster равно true, удержание ввода навигации запускает повторяющиеся вызовы Navigate() с частотой HoldNavigationInterval. Это использует постоянную корутину:

protected IEnumerator HoldNavigation()
{
while (true)
{
if (Holding) Navigate(LastInput);
yield return new WaitForSeconds(HoldNavigationInterval);
}
}

Расположение: Assets/OpenMon/Core/Runtime/UI/MenuItem.cs

Каждая кнопка в MenuSelector — это MenuItem -- HidableAndSubscribableButton с:

  • ArrowSelectorPosition -- дочерним Transform, который определяет, где должна располагаться стрелка-указатель.
  • ToggleIgnoreLayout -- когда true, скрытие элемента также удаляет его из расчётов раскладки через LayoutElement.ignoreLayout.
  • OnSelect() / OnDeselect() -- вызываются родительским MenuSelector, чтобы запустить состояние выбора EventSystem Unity.
public class MenuItem : HidableAndSubscribableButton
{
public Transform ArrowSelectorPosition;

public virtual void OnSelect()
{
EventSystem.current.SetSelectedGameObject(null);
Button.Select();
}

public virtual void OnDeselect() => Button.OnDeselect(null);
}

ScrollableMenuSelector​

Расположение: Assets/OpenMon/Core/Runtime/UI/ScrollableMenuSelector.cs

Для меню, которые выше области просмотра (например, длинный список приёмов или предметов), ScrollableMenuSelector переопределяет Select(), чтобы автопрокручивать родительский ScrollRect:

public override void Select(int index, bool playAudio = true, bool updateArrow = true, bool force = false)
{
base.Select(index, playAudio, false, force);
UpdateScroll(); // Scroll to keep selection visible
UpdateSelectorArrowPosition(); // Then reposition the arrow
}

private void UpdateScroll()
{
if (MenuOptions.Count < 2) return;
Scroll.verticalNormalizedPosition = 1 - (float)CurrentSelection / (MenuOptions.Count - 1);
}

VirtualizedMenuSelector​

Расположение: Assets/OpenMon/Core/Runtime/UI/VirtualizedMenuSelector.cs

Для очень больших списков (сотни элементов, Декс, хранилище ПК) VirtualizedMenuSelector<TData, TButtonController, TButtonFactory> использует пулинг объектов, чтобы переиспользовать небольшой набор экземпляров кнопок по мере прокрутки игрока:

  • Создаётся фиксированное количество экземпляров TButtonController (достаточно для заполнения области просмотра плюс небольшой буфер).
  • По мере прокрутки ScrollRect элементы вне экрана перемещаются и заполняются новыми данными.
  • Абстрактный метод PopulateChildData(TButtonController child, TData data) должен быть реализован подклассами для привязки данных к каждой переиспользуемой кнопке.
// Setting data on a virtualized menu
public void SetButtons(List<TData> newData, bool clearPrevious = true)
{
Data = newData;
RowCount = Data.Count;
Refresh(clearPrevious);
}

Экземпляры кнопок создаются через Zenject GameObjectFactory<TButtonController>:

[Inject] private TButtonFactory buttonFactory;

// Inside CheckChildItems():
childItems[i] = buttonFactory.CreateUiGameObject(Content);

Пример: SavegameButton​

public class SavegameButton : VirtualizedMenuItem
{
[SerializeField] private TMP_Text Text;

public void SetSaveName(string nameToSet) => Text.SetText(nameToSet);

public class Factory : GameObjectFactory<SavegameButton> { }
}

Игровые экраны​

Главное меню (титульный экран)​

Расположение: Assets/OpenMon/Core/Runtime/UI/MainMenu/

Титульный экран предоставляет New Game, Continue (загрузка), Credits. Экран загрузки использует VirtualizedMenuSelector с элементами SavegameButton для отображения сохранённых игр.

Игровое меню паузы​

Расположение: Assets/OpenMon/Core/Runtime/UI/GameMenu/GameMenuScreen.cs

GameMenuScreen расширяет MenuSelector и выезжает/уезжает с анимациями твинов. Он маршрутизирует выборы через switch ButtonSelected(int index) с индексами 0–11 — оригинальные 8 плюс три записи застраиваемого контента (случаи 9–11); слот 8 скрыт:

ИндексЗаписьДействие
0DexDialogManager.ShowDexScreen(...)
1MonstersDialogManager.ShowPlayerRosterMenu(...)
2BagDialogManager.ShowBag(...)
3MapMapSceneLauncher.ShowMap(...)
4QuestsDialogManager.ShowQuestsScreen(...)
5ProfileDialogManager.ShowProfileScreen(...)
6OptionsDialogManager.ShowOptionsMenu()
7ExitПредлагает сохранить, затем выход или возврат в титул
9Battle TowerEphemeralTowerLauncher.Launch(diContainer, playerCharacter) — бесконечный, эфемерный забег
10My BaseEnterOrDecorateSecretBase() — войти в интерьер или открыть палитру украшения, находясь внутри
11My GymEnterOrManageGym() — войти в зал игрока или украсить/управлять, находясь внутри

Три новые записи локализованы (испанские метки Torre de Batalla / Mi Base / Mi Gimnasio в исходнике) и ограничены по доступности — каждая является безопасной пустой операцией, пока её авторский слот не подключён в префабе меню:

ЗаписьГейт доступности (сериализовано на GameMenuScreen)
Battle Tower (случай 9)у игрока есть непустой PlayerRoster
My Base (случай 10)назначен secretBaseInteriorScene (SceneInfoAsset); иначе вход пропускается
My Gym (случай 11)назначен gymInteriorScene (SceneInfoAsset); иначе вход пропускается

Случаи 9–11 внедряются через зависимости: diContainer конструкторно внедряет эфемерную башню, а teleporter (PlayerTeleporter) телепортирует игрока в интерьер базы/зала. Перед любой записью, которая забирает экран, CloseMenuForOverworldAction() отражает демонтаж OnBack, так что надземный мир не остаётся замороженным позади боя/сцены.

Меню поддерживает быстрое сохранение через OnExtra2 и может переключаться как через Back, так и через выделенную клавишу Menu (OnExtra1).

// Opening the game menu
DialogManager.ShowGameMenu(onButtons, playerCharacter, onBack);

Видимость кнопок контролируется через UpdateLayout(List<bool>) -- передайте список булевых значений, чтобы показать/скрыть конкретные записи (например, скрыть Dex, пока игрок его не получит).

Для игрового процесса за тремя новыми записями см. руководство Секретные базы и залы игрока.

Экран опций​

Расположение: Assets/OpenMon/Core/Runtime/UI/Options/

Предоставляет игровые настройки как элементы управления на основе MenuSelector:

  • Ползунки громкости (Global, Music, FX) через VolumeSlider / TweenableSlider
  • Разрешение, полноэкранный режим, выбор монитора
  • Выбор языка
  • Игровой процесс: скорость боя, стиль боя, сложность, сложность ловли, режим бега
  • Переключатели автосохранения, формат сохранения, отображение прозвищ

Боевой UI​

Расположение: Assets/OpenMon/Core/Runtime/UI/Battle/

Боевой HUD использует автономные подклассы MenuSelector (не маршрутизируемые через DialogManager):

  • MainBattleMenu -- Fight / Bag / Monster / Run с быстрым броском последнего шара (OnExtra2).
  • BattleMoveMenuSelector -- сетка из 4 приёмов.
  • TargetMonstersMenuSelector -- выбор цели для боёв с несколькими участниками.
  • BattleInfoPanel -- наложение с детальной информацией о характеристиках/типах.

Добавление пользовательского меню​

  1. Создайте префаб с MenuSelector (или подклассом) в корне и дочерними MenuItem для каждой кнопки.
  2. Установите ArrowSelectorPosition на каждом MenuItem в дочерний Transform, где должна появляться стрелка.
  3. Настройте MenuNavigation (Vertical/Horizontal), ссылки на аудио и префаб стрелки в Inspector.
  4. Подпишитесь на события в вашем контроллере:
public class MyCustomMenu : MonoBehaviour
{
[SerializeField] private MenuSelector menu;

private void OnEnable()
{
menu.OnButtonSelected += OnItemChosen;
menu.OnBackSelected += OnBack;
}

private void OnItemChosen(int index)
{
// Handle selection
}

private void OnBack()
{
// Close menu
}
}
  1. Для больших списков наследуйтесь от VirtualizedMenuSelector<TData, TButton, TFactory>, реализуйте PopulateChildData() и зарегистрируйте вашу фабрику кнопок в UIElementsInstaller.