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 -- Generic Menu Navigation
MenuSelector is the base class nearly every interactive menu inherits from. It handles:
- Vertical or Horizontal navigation via the
Navigationenum. - Selector arrow that animates (DOTween) to the hovered item.
- Audio feedback on navigation and selection.
- Input integration through the
IInputReceiverinterface andIInputManager. - Wrap-around -- navigating past the last item wraps to the first and vice versa.
- Hold-to-scroll -- when
CanHoldToScrollFasteris enabled, holding the input repeats navigation at a configurable interval.
Two specialized variants exist:
| Class | Use Case |
|---|---|
ScrollableMenuSelector | Adds 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-- BindsDialogManager.Factoryfrom 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:
| Screen | Controller | Base Class | Launched Via |
|---|---|---|---|
| Game Menu | GameMenuScreen | MenuSelector | DialogManager.ShowGameMenu(...) |
| Monster Roster | MonstersMenuScreen | HidableUiElement | DialogManager.ShowPlayerRosterMenu(...) |
| Bag / Inventory | BagScreen | HidableUiElement | DialogManager.ShowBag(...) |
| Dex | DexScreen | VirtualizedMenuSelector | DialogManager.ShowDexScreen(...) |
| Single Monster Dex | SingleMonsterDexScreen | HidableUiElement | DialogManager.ShowSingleMonsterDexScreen(...) |
| Shop | ShopDialog | HidableUiElement | DialogManager.ShowShop(...) |
| Options | OptionsScreen | MenuSelector | DialogManager.ShowOptionsMenu() |
| Profile | ProfileScreen | HidableUiElement | DialogManager.ShowProfileScreen(...) |
| Quests | QuestsScreen | VirtualizedMenuSelector | DialogManager.ShowQuestsScreen(...) |
| Text Input | TextInputDialog | HidableUiElement | DialogManager.RequestTextInput(...) |
| Move Tutor | MoveTutorDialog | HidableUiElement | DialogManager.ShowMoveTutorDialog(...) |
Battle UI
The battle HUD has its own set of MenuSelector subclasses that are NOT managed by DialogManager:
| Controller | Base Class | Purpose |
|---|---|---|
MainBattleMenu | MenuSelector | Fight / Bag / Monster / Run selection |
BattleMoveMenuSelector | MenuSelector | Move selection during battle |
TargetMonstersMenuSelector | MenuSelector | Target selection in multi-battles |
BattleMonstersMenu | HidableUiElement | Switch monster during battle |
BattleInfoPanel | HidableUiElement<MoveInfoPanel> | Detailed battle state overlay |
LastBallMenu | MenuSelector | Quick-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
- Inherit from
MenuSelector(orScrollableMenuSelector/VirtualizedMenuSelectorfor large lists). - Add
MenuItemcomponents to each button in your menu. Set theArrowSelectorPositiontransform for the selector arrow. - Register factories in
UIElementsInstallerif your screen uses pooled/virtualized items. - Launch from
DialogManagerby adding a static method and serialized reference, following the existing pattern. - Use
[SerializeField] privatefor all inspector references -- never expose fields aspublicunless they are part of the API. - Use localization keys for all player-visible text. The
LocalizedTextMeshProandLocalizedTypeWriterTMPcomponents handle this automatically. - Prefer UI Toolkit for new editor tooling, but stick with UGUI for runtime game UI to maintain consistency with the existing system.