Система меню
Система меню построена на компоненте MenuSelector -- универсальном, переиспользуемом контроллере, который обрабатывает навигацию курсора, ввод, аудио и стрелки-указатели для любого списка или сетки кнопок. Каждое игровое меню (меню паузы, боевое меню, магазин, декс, опции и т. д.) наследуе тся от него.
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
События
| Событие | Тип | Срабатывает когда |
|---|---|---|
OnButtonSelected | Action<int> | Игрок подтверждает выбор (индекс выбранного элемента) |
OnHovered | Action<int> | Курсор перемещается на новый элемент |
OnBackSelected | Action | Игрок нажимает кнопку назад/отмены |
Жизненный цикл
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
Поток навигации
Когда игрок нажимает направление:
OnNavigation()читает вектор ввода и извлекает соответствующую ось (Y для вертикали, X для горизонтали).Navigate()увеличивает или уменьшает индекс выбора, зацикливаясь на границах.- Невзаимодействуемые кнопки пропускаются автоматически.
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);
}
}
MenuItem
Расположение: Assets/OpenMon/Core/Runtime/UI/MenuItem.cs
Каждая кнопка в MenuSelector — это MenuItem -- HidableAndSubscribableButton с:
ArrowSelectorPosition-- дочернимTransform, который определяет, где должна располагаться стрелка-указатель.ToggleIgnoreLayout-- когда true, скрытие элемента также удаляет его из расчётов раскладки черезLayoutElement.ignoreLayout.OnSelect()/OnDeselect()-- вызываются родительскимMenuSelector, чтобы запустить состояние выбораEventSystemUnity.
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 скрыт:
| Индекс | Запись | Действие |
|---|---|---|
| 0 | Dex | DialogManager.ShowDexScreen(...) |
| 1 | Monsters | DialogManager.ShowPlayerRosterMenu(...) |
| 2 | Bag | DialogManager.ShowBag(...) |
| 3 | Map | MapSceneLauncher.ShowMap(...) |
| 4 | Quests | DialogManager.ShowQuestsScreen(...) |
| 5 | Profile | DialogManager.ShowProfileScreen(...) |
| 6 | Options | DialogManager.ShowOptionsMenu() |
| 7 | Exit | Предлагает сохранить, затем выход или возврат в титул |
| 9 | Battle Tower | EphemeralTowerLauncher.Launch(diContainer, playerCharacter) — бесконечный, эфемерный забег |
| 10 | My Base | EnterOrDecorateSecretBase() — войти в интерьер или открыть палитру украшения, находясь внутри |
| 11 | My Gym | EnterOrManageGym() — войти в зал игрока или украсить/управлять, находясь внутри |
Три новые записи локализованы (испанские метки 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-- наложение с детальной информацией о характеристиках/типах.
Добавление пользовательского меню
- Создайте префаб с
MenuSelector(или подклассом) в корне и дочернимиMenuItemдля каждой кнопки. - Установите
ArrowSelectorPositionна каждомMenuItemв дочерний Transform, где должна появляться стрелка. - Настройте
MenuNavigation(Vertical/Horizontal), ссылки на аудио и префаб стрелки в Inspector. - Подпишитесь на события в вашем контроллере:
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
}
}
- Для больших списков наследуйтесь от
VirtualizedMenuSelector<TData, TButton, TFactory>, реализуйтеPopulateChildData()и зарегистрируйте вашу фабрику кнопок вUIElementsInstaller.