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.
MenuSelector
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 (DOTween)
- 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
| Event | Type | Fires When |
|---|---|---|
OnButtonSelected | Action<int> | Player confirms a selection (index of chosen item) |
OnHovered | Action<int> | Cursor moves to a new item |
OnBackSelected | Action | Player 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
Navigation Flow
When the player presses a direction:
OnNavigation()reads the input vector and extracts the relevant axis (Y for vertical, X for horizontal).Navigate()increments or decrements the selection index, wrapping around at boundaries.- Non-interactable buttons are skipped automatically.
Select()is called on the new index, which:- Deselects the previous item
- Plays navigation audio
- Updates
CurrentSelection - Moves the selector arrow with
DOTween - 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);
}
}
MenuItem
Location: Assets/OpenMon/Core/Runtime/UI/MenuItem.cs
Each button in a MenuSelector is a MenuItem -- a HidableAndSubscribableButton with:
ArrowSelectorPosition-- aTransformchild that defines where the selector arrow should land.ToggleIgnoreLayout-- when true, hiding the item also removes it from layout calculations viaLayoutElement.ignoreLayout.OnSelect()/OnDeselect()-- called by the parentMenuSelectorto trigger Unity'sEventSystemselection 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
TButtonControllerinstances are created (enough to fill the viewport plus a small buffer). - As the
ScrollRectscrolls, 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
Main Menu (Title Screen)
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 DOTween animations. It has 8 entries:
| Index | Entry | Action |
|---|---|---|
| 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 | Prompts save, then quit or return to title |
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).
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
- Create the prefab with a
MenuSelector(or subclass) at the root andMenuItemchildren for each button. - Set the
ArrowSelectorPositionon eachMenuItemto a child Transform where the arrow should appear. - Configure
MenuNavigation(Vertical/Horizontal), audio references, and arrow prefab in the Inspector. - 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
}
}
- For large lists, inherit from
VirtualizedMenuSelector<TData, TButton, TFactory>, implementPopulateChildData(), and register your button factory inUIElementsInstaller.