Skip to main content

Menu System

The menu system is built on the MenuSelector component -- a generic, reusable controller that handles cursor navigation, input, audio, and selector arrows for any list or grid of buttons. Every in-game menu (pause menu, battle menu, shop, dex, options, etc.) inherits from it.

Three screens, one component. What follows is the same MenuSelector in three different shapes:

The Dex

A long virtualised list. The counters across the top are seen / caught / forms, and the ???? rows are species the player has not observed yet — the entries exist, the names do not, because in an online world it is the server that decides what you have seen.

The party screen

Six fixed slots instead of a list, with the cursor moving in two dimensions.

The bag

Two selectors side by side — the category strip along the top and the item list under it — plus a third for the party on the left, where entries that would do nothing read "Sin efecto" rather than being hidden. A button that is visible and refuses is easier to learn than one that vanished.

Location: Assets/OpenMon/Core/Runtime/UI/MenuSelector.cs Namespace: OpenMon.MCE.UI

MenuSelector extends HidableUiElement<MenuSelector> and implements IInputReceiver. It manages a list of MenuItem children and provides:

  • Vertical or horizontal navigation
  • Wrap-around cursor movement
  • Animated selector arrow (MCE's bundled tween API -- no third-party asset needed)
  • Hold-to-scroll for fast navigation
  • Audio feedback on move and select
  • Events for selection, hover, and back

Key Serialized Fields​

[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

Events​

EventTypeFires When
OnButtonSelectedAction<int>Player confirms a selection (index of chosen item)
OnHoveredAction<int>Cursor moves to a new item
OnBackSelectedActionPlayer presses the back/cancel button

Lifecycle​

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

When the player presses a direction:

  1. OnNavigation() reads the input vector and extracts the relevant axis (Y for vertical, X for horizontal).
  2. Navigate() increments or decrements the selection index, wrapping around at boundaries.
  3. Non-interactable buttons are skipped automatically.
  4. Select() is called on the new index, which:
    • Deselects the previous item
    • Plays navigation audio
    • Updates CurrentSelection
    • Moves the selector arrow with the OpenMon.Foundation.Tweening API
    • Fires OnHovered
    • Calls MenuItem.OnSelect() on the new button
// 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);
}

Hold-to-Scroll​

When CanHoldToScrollFaster is true, holding the navigation input triggers repeated Navigate() calls at the HoldNavigationInterval rate. This uses a persistent coroutine:

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

Location: Assets/OpenMon/Core/Runtime/UI/MenuItem.cs

Each button in a MenuSelector is a MenuItem -- a HidableAndSubscribableButton with:

  • ArrowSelectorPosition -- a Transform child that defines where the selector arrow should land.
  • ToggleIgnoreLayout -- when true, hiding the item also removes it from layout calculations via LayoutElement.ignoreLayout.
  • OnSelect() / OnDeselect() -- called by the parent MenuSelector to trigger Unity's EventSystem selection state.
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​

Location: Assets/OpenMon/Core/Runtime/UI/ScrollableMenuSelector.cs

For menus that are taller than the viewport (e.g. a long list of moves or items), ScrollableMenuSelector overrides Select() to auto-scroll the parent 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​

Location: Assets/OpenMon/Core/Runtime/UI/VirtualizedMenuSelector.cs

For very large lists (hundreds of items, the Dex, PC storage), VirtualizedMenuSelector<TData, TButtonController, TButtonFactory> uses object pooling to recycle a small set of button instances as the player scrolls:

  • A fixed number of TButtonController instances are created (enough to fill the viewport plus a small buffer).
  • As the ScrollRect scrolls, off-screen items are repositioned and repopulated with new data.
  • The abstract method PopulateChildData(TButtonController child, TData data) must be implemented by subclasses to bind data to each recycled button.
// Setting data on a virtualized menu
public void SetButtons(List<TData> newData, bool clearPrevious = true)
{
Data = newData;
RowCount = Data.Count;
Refresh(clearPrevious);
}

Button instances are created through a Zenject GameObjectFactory<TButtonController>:

[Inject] private TButtonFactory buttonFactory;

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

Example: SavegameButton​

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

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

public class Factory : GameObjectFactory<SavegameButton> { }
}

Game Screens​

Location: Assets/OpenMon/Core/Runtime/UI/MainMenu/

The title screen provides New Game, Continue (load), Credits. The load screen uses a VirtualizedMenuSelector with SavegameButton items to display saved games.

In-Game Pause Menu​

Location: Assets/OpenMon/Core/Runtime/UI/GameMenu/GameMenuScreen.cs

GameMenuScreen extends MenuSelector and slides in/out with tween animations. It routes selections through a ButtonSelected(int index) switch with 12 slots — the original 8 plus three buildable-content entries (cases 9–11). Slot 8 is not in use and stays hidden, so the other entries keep their indices:

IndexEntryAction
0DexDialogManager.ShowDexScreen(...)
1MonstersDialogManager.ShowPlayerRosterMenu(...)
2BagDialogManager.ShowBag(...)
3MapMapSceneLauncher.ShowMap(...)
4QuestsDialogManager.ShowQuestsScreen(...)
5ProfileDialogManager.ShowProfileScreen(...)
6OptionsDialogManager.ShowOptionsMenu()
7ExitPrompts save, then quit or return to title
9Battle TowerEphemeralTowerLauncher.Launch(diContainer, playerCharacter) — endless, ephemeral run
10My BaseEnterOrDecorateSecretBase() — enter the interior, or open the decorate palette when inside
11My GymEnterOrManageGym() — enter the player's gym, or decorate/manage when inside

The three new entries are localized (Spanish labels Torre de Batalla / Mi Base / Mi Gimnasio in-source) and gated by availability — each is a safe no-op until its authoring slot is wired in the menu prefab:

EntryAvailability gate (serialized on GameMenuScreen)
Battle Tower (case 9)the player has a non-empty PlayerRoster
My Base (case 10)secretBaseInteriorScene (a SceneInfoAsset) assigned; otherwise enter is skipped
My Gym (case 11)gymInteriorScene (a SceneInfoAsset) assigned; otherwise enter is skipped

Cases 9–11 are dependency-injected: diContainer construct-injects the ephemeral tower, and teleporter (PlayerTeleporter) warps the player into the base/gym interior. Before any entry that takes over the screen, CloseMenuForOverworldAction() mirrors the OnBack teardown so the overworld is not left frozen behind the battle/scene.

The menu supports quick-save via OnExtra2 and can be toggled with both Back and the dedicated Menu key (OnExtra1).

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

Button visibility is controlled through UpdateLayout(List<bool>) -- pass a list of booleans to show/hide specific entries (e.g. hide Dex before the player gets it).

For the gameplay behind the three new entries, see the Secret Bases & Player Gyms guide.

Options Screen​

Location: Assets/OpenMon/Core/Runtime/UI/Options/

Exposes game settings as MenuSelector-based controls:

  • Volume sliders (Global, Music, FX) via VolumeSlider / TweenableSlider
  • Resolution, fullscreen mode, monitor selection
  • Language selection
  • Gameplay: battle speed, battle style, difficulty, catch difficulty, run mode
  • Auto-save toggles, save format, nickname display

Battle UI​

Location: Assets/OpenMon/Core/Runtime/UI/Battle/

The battle HUD uses standalone MenuSelector subclasses (not routed through DialogManager):

  • MainBattleMenu -- Fight / Bag / Monster / Run with quick-throw last ball (OnExtra2).
  • BattleMoveMenuSelector -- 4-move grid.
  • TargetMonstersMenuSelector -- Target picker for multi-battles.
  • BattleInfoPanel -- Detailed stat/type info overlay.

Adding a Custom Menu​

  1. Create the prefab with a MenuSelector (or subclass) at the root and MenuItem children for each button.
  2. Set the ArrowSelectorPosition on each MenuItem to a child Transform where the arrow should appear.
  3. Configure MenuNavigation (Vertical/Horizontal), audio references, and arrow prefab in the Inspector.
  4. Subscribe to events in your controller:
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. For large lists, inherit from VirtualizedMenuSelector<TData, TButton, TFactory>, implement PopulateChildData(), and register your button factory in UIElementsInstaller.