Skip to main content

UI System Overview

OpenMon's UI layer lives under Assets/OpenMon/Core/Runtime/UI/ and is built entirely on Unity's UGUI (Canvas + RectTransform). Every screen, menu, and dialog shares two foundational building blocks: MenuSelector for navigable lists/grids and DialogManager for modal text boxes, choices, and complex popups.

Architecture at a Glance

UI/
├── MenuSelector.cs # Base class for ALL navigable menus
├── ScrollableMenuSelector.cs # MenuSelector + ScrollRect support
├── VirtualizedMenuSelector.cs # MenuSelector + recycled/pooled items
├── MenuItem.cs # Individual button inside a MenuSelector
├── VirtualizedMenuItem.cs # MenuItem aware of its row in a pool
├── UIElementsInstaller.cs # Zenject installer for UI factories
├── Battle/ # Battle HUD: main menu, move menu, target selector
├── Bags/ # Inventory / bag screen with pocket tabs
├── Dex/ # Full Pokedex-style screen with tabs
├── Dialogs/ # DialogManager + choice menu, text input, popups
├── GameMenu/ # In-game pause menu (Dex, Mons, Bag, Map, etc.)
├── MainMenu/ # Title screen: New Game, Continue, Credits
├── Monsters/ # Monster roster menu, summary, storage
├── Options/ # Settings screen (volume, resolution, language, etc.)
├── Shops/ # Buy/sell shop interface
├── Quests/ # Quest log screen
├── Profile/ # Player profile / trainer card
├── Map/ # World map overlay
└── ...

The Two Pillars

MenuSelector is the base class nearly every interactive menu inherits from. It handles:

  • Vertical or Horizontal navigation via the Navigation enum.
  • Selector arrow that animates (DOTween) to the hovered item.
  • Audio feedback on navigation and selection.
  • Input integration through the IInputReceiver interface and IInputManager.
  • Wrap-around -- navigating past the last item wraps to the first and vice versa.
  • Hold-to-scroll -- when CanHoldToScrollFaster is enabled, holding the input repeats navigation at a configurable interval.

Two specialized variants exist:

ClassUse Case
ScrollableMenuSelectorAdds a ScrollRect that auto-scrolls to keep the selected item visible.
VirtualizedMenuSelector<TData, TButton, TFactory>Object-pooled list for large data sets (e.g. Dex, Storage). Recycles button instances as the player scrolls.

See the Menus page for a deep dive into MenuSelector.

DialogManager -- Modal Dialogs and Complex Popups

DialogManager is a Singleton that owns every modal UI overlay in the game. It manages:

  • Basic text dialogs with typewriter animation, speaker name panels, and localization.
  • Choice menus (Yes/No, multiple options up to 11).
  • Full-screen screens accessible via static methods: Dex, Bag, Roster, Shop, Options, Profile, Quests, Map.
  • Specialized popups: XP gain panel, move replacement dialog, move tutor, new monster popup, text input dialog.
  • Notifications through a child NotificationManager.

The DialogManager is instantiated via Zenject's factory pattern:

// DialogManagerInstaller.cs
Container.BindFactory<DialogManager, DialogManager.Factory>()
.FromComponentInNewPrefab(Prefab);

See the Dialogs page for the full API.

Dependency Injection

All UI factories are registered through two Zenject ScriptableObjectInstallers:

  • DialogManagerInstaller -- Binds DialogManager.Factory from a prefab.
  • UIElementsInstaller -- Binds factories for item buttons, monster buttons, dex buttons, move buttons, quest buttons, savegame buttons, and more. Each binding is scoped with .WhenInjectedInto<T>() so the correct prefab variant is used per screen.
// Example from UIElementsInstaller
Container.BindFactory<ItemButton, ItemButton.Factory>()
.FromComponentInNewPrefab(BagItemButtonPrefab)
.AsCached()
.WhenInjectedInto<BagTab>()
.Lazy();

Dedicated Screen Controllers

Each major screen either extends MenuSelector (for grid/list navigation), VirtualizedMenuSelector (for large pooled lists), or HidableUiElement (for custom layouts). All are launched through DialogManager:

ScreenControllerBase ClassLaunched Via
Game MenuGameMenuScreenMenuSelectorDialogManager.ShowGameMenu(...)
Monster RosterMonstersMenuScreenHidableUiElementDialogManager.ShowPlayerRosterMenu(...)
Bag / InventoryBagScreenHidableUiElementDialogManager.ShowBag(...)
DexDexScreenVirtualizedMenuSelectorDialogManager.ShowDexScreen(...)
Single Monster DexSingleMonsterDexScreenHidableUiElementDialogManager.ShowSingleMonsterDexScreen(...)
ShopShopDialogHidableUiElementDialogManager.ShowShop(...)
OptionsOptionsScreenMenuSelectorDialogManager.ShowOptionsMenu()
ProfileProfileScreenHidableUiElementDialogManager.ShowProfileScreen(...)
QuestsQuestsScreenVirtualizedMenuSelectorDialogManager.ShowQuestsScreen(...)
Text InputTextInputDialogHidableUiElementDialogManager.RequestTextInput(...)
Move TutorMoveTutorDialogHidableUiElementDialogManager.ShowMoveTutorDialog(...)

Battle UI

The battle HUD has its own set of MenuSelector subclasses that are NOT managed by DialogManager:

ControllerBase ClassPurpose
MainBattleMenuMenuSelectorFight / Bag / Monster / Run selection
BattleMoveMenuSelectorMenuSelectorMove selection during battle
TargetMonstersMenuSelectorMenuSelectorTarget selection in multi-battles
BattleMonstersMenuHidableUiElementSwitch monster during battle
BattleInfoPanelHidableUiElement<MoveInfoPanel>Detailed battle state overlay
LastBallMenuMenuSelectorQuick-throw last-used ball

Input System Integration

All menus implement IInputReceiver, which extends Unity's New Input System action interfaces:

public interface IInputReceiver : MCEInputActions.IMainActions,
MCEInputActions.IUIActions,
MCEInputActions.ITextInputActions
{
InputType GetInputType();
void OnStateEnter();
void OnStateExit();
string GetDebugName();
}

The IInputManager maintains a stack of receivers. When a menu calls RequestInput(), it pushes itself onto the stack and becomes the active receiver. Calling ReleaseInput() pops it. This naturally handles nested menus -- opening the Bag from the Game Menu pushes the Bag's input receiver on top, and closing it restores input to the Game Menu.

Best Practices for Custom UI Screens

  1. Inherit from MenuSelector (or ScrollableMenuSelector / VirtualizedMenuSelector for large lists).
  2. Add MenuItem components to each button in your menu. Set the ArrowSelectorPosition transform for the selector arrow.
  3. Register factories in UIElementsInstaller if your screen uses pooled/virtualized items.
  4. Launch from DialogManager by adding a static method and serialized reference, following the existing pattern.
  5. Use [SerializeField] private for all inspector references -- never expose fields as public unless they are part of the API.
  6. Use localization keys for all player-visible text. The LocalizedTextMeshPro and LocalizedTypeWriterTMP components handle this automatically.
  7. Prefer UI Toolkit for new editor tooling, but stick with UGUI for runtime game UI to maintain consistency with the existing system.