Sistema de menús
El sistema de menús está construido sobre el componente MenuSelector — un controlador genérico y reutilizable que maneja la navegación del cursor, entrada, audio y flechas de selección para cualquier lista o cuadrícula de botones. Cada menú del juego (menú de pausa, menú de combate, tienda, dex, opciones, etc.) hereda de él.
MenuSelector
Ubicación: Assets/OpenMon/Core/Runtime/UI/MenuSelector.cs
Namespace: OpenMon.MCE.UI
MenuSelector extiende HidableUiElement<MenuSelector> e implementa IInputReceiver. Gestiona una lista de elementos MenuItem hijos y proporciona:
- Navegación vertical u horizontal
- Movimiento de cursor con wrap-around
- Flecha de selección animada (API de tweening incluida en MCE; no requiere ningún asset de terceros)
- Hold-to-scroll para navegación rápida
- Retroalimentación de audio al moverse y seleccionar
- Eventos para selección, hover y retroceso
Campos serializados clave
[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
Eventos
| Evento | Tipo | Se dispara cuando |
|---|---|---|
OnButtonSelected | Action<int> | El jugador confirma una selección (índice del elemento elegido) |
OnHovered | Action<int> | El cursor se mueve a un nuevo elemento |
OnBackSelected | Action | El jugador presiona el botón de retroceso/cancelar |
Ciclo de vida
Show(true)
└─ RequestInput() ──► IInputManager empuja esto a la pila de entrada
└─ OnStateEnter()
├─ Instancia el prefab de la flecha de selección
├─ Inicia la corrutina HoldNavigation
└─ Selecciona DefaultSelected después de un frame
Show(false)
└─ ReleaseInput() ──► IInputManager saca esto de la pila de entrada
└─ OnStateExit()
├─ Detiene la corrutina HoldNavigation
├─ Deselecciona el elemento actual
└─ Destruye la flecha de selección
Flujo de navegación
Cuando el jugador presiona una dirección:
OnNavigation()lee el vector de entrada y extrae el eje relevante (Y para vertical, X para horizontal).Navigate()incrementa o decrementa el índice de selección, envolviendo en los límites.- Los botones no interactuables se omiten automáticamente.
- Se llama a
Select()en el nuevo índice, que:- Deselecciona el elemento anterior
- Reproduce el audio de navegación
- Actualiza
CurrentSelection - Mueve la flecha de selección con la API
OpenMon.Foundation.Tweening - Dispara
OnHovered - Llama a
MenuItem.OnSelect()en el nuevo botón
// 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
Cuando CanHoldToScrollFaster es true, mantener presionada la entrada de navegación dispara llamadas repetidas a Navigate() a la tasa de HoldNavigationInterval. Esto usa una corrutina persistente:
protected IEnumerator HoldNavigation()
{
while (true)
{
if (Holding) Navigate(LastInput);
yield return new WaitForSeconds(HoldNavigationInterval);
}
}
MenuItem
Ubicación: Assets/OpenMon/Core/Runtime/UI/MenuItem.cs
Cada botón en un MenuSelector es un MenuItem — un HidableAndSubscribableButton con:
ArrowSelectorPosition— unTransformhijo que define dónde debe posarse la flecha de selección.ToggleIgnoreLayout— cuando es true, ocultar el elemento también lo elimina de los cálculos de layout viaLayoutElement.ignoreLayout.OnSelect()/OnDeselect()— llamados por elMenuSelectorpadre para disparar el estado de selección delEventSystemde Unity.
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
Ubicación: Assets/OpenMon/Core/Runtime/UI/ScrollableMenuSelector.cs
Para menús que son más altos que el viewport (p. ej. una lista larga de movimientos u objetos), ScrollableMenuSelector sobreescribe Select() para auto-desplazar el ScrollRect padre:
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
Ubicación: Assets/OpenMon/Core/Runtime/UI/VirtualizedMenuSelector.cs
Para listas muy grandes (cientos de elementos, el Dex, el almacenamiento del PC), VirtualizedMenuSelector<TData, TButtonController, TButtonFactory> usa object pooling para reciclar un pequeño conjunto de instancias de botón a medida que el jugador se desplaza:
- Se crea un número fijo de instancias de
TButtonController(suficientes para llenar el viewport más un pequeño búfer). - A medida que el
ScrollRectse desplaza, los elementos fuera de pantalla se reposicionan y repueblan con nuevos datos. - El método abstracto
PopulateChildData(TButtonController child, TData data)debe ser implementado por las subclases para enlazar datos a cada botón reciclado.
// Setting data on a virtualized menu
public void SetButtons(List<TData> newData, bool clearPrevious = true)
{
Data = newData;
RowCount = Data.Count;
Refresh(clearPrevious);
}
Las instancias de botón se crean a través de un GameObjectFactory<TButtonController> de Zenject:
[Inject] private TButtonFactory buttonFactory;
// Inside CheckChildItems():
childItems[i] = buttonFactory.CreateUiGameObject(Content);
Ejemplo: SavegameButton
public class SavegameButton : VirtualizedMenuItem
{
[SerializeField] private TMP_Text Text;
public void SetSaveName(string nameToSet) => Text.SetText(nameToSet);
public class Factory : GameObjectFactory<SavegameButton> { }
}
Pantallas del juego
Menú principal (pantalla de título)
Ubicación: Assets/OpenMon/Core/Runtime/UI/MainMenu/
La pantalla de título ofrece Nueva partida, Continuar (cargar) y Créditos. La pantalla de carga usa un VirtualizedMenuSelector con elementos SavegameButton para mostrar las partidas guardadas.
Menú de pausa en el juego
Ubicación: Assets/OpenMon/Core/Runtime/UI/GameMenu/GameMenuScreen.cs
GameMenuScreen extiende MenuSelector y se desliza dentro/fuera con animaciones de tweening. Enruta las selecciones a través de un switch ButtonSelected(int index) con índices del 0 al 11 — las 8 originales más tres entradas de contenido construible (casos 9-11); la ranura 8 permanece oculta:
| Índice | Entrada | Acción |
|---|---|---|
| 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 | Solicita guardar, luego salir o volver al título |
| 9 | Battle Tower | EphemeralTowerLauncher.Launch(diContainer, playerCharacter) — partida infinita y efímera |
| 10 | My Base | EnterOrDecorateSecretBase() — entra al interior, o abre la paleta de decoración cuando está dentro |
| 11 | My Gym | EnterOrManageGym() — entra al gimnasio del jugador, o decora/gestiona cuando está dentro |
Las tres entradas nuevas están localizadas (etiquetas en español Torre de Batalla / Mi Base / Mi Gimnasio en el código fuente) y protegidas por disponibilidad — cada una es un no-op seguro hasta que su ranura de autoría esté conectada en el prefab del menú:
| Entrada | Gate de disponibilidad (serializado en GameMenuScreen) |
|---|---|
| Battle Tower (caso 9) | el jugador tiene un PlayerRoster no vacío |
| My Base (caso 10) | secretBaseInteriorScene (un SceneInfoAsset) asignado; de lo contrario se omite la entrada |
| My Gym (caso 11) | gymInteriorScene (un SceneInfoAsset) asignado; de lo contrario se omite la entrada |
Los casos 9-11 usan inyección de dependencias: diContainer inyecta por constructor la torre efímera, y teleporter (PlayerTeleporter) transporta al jugador al interior de la base/gimnasio. Antes de cualquier entrada que se apodere de la pantalla, CloseMenuForOverworldAction() refleja el desmontaje de OnBack para que el mundo no quede congelado detrás de la batalla/escena.
El menú soporta guardado rápido via OnExtra2 y se puede alternar tanto con Back como con la tecla de Menú dedicada (OnExtra1).
// Opening the game menu
DialogManager.ShowGameMenu(onButtons, playerCharacter, onBack);
La visibilidad de los botones se controla mediante UpdateLayout(List<bool>) — pase una lista de booleanos para mostrar/ocultar entradas específicas (p. ej. ocultar Dex antes de que el jugador lo obtenga).
Para la jugabilidad detrás de las tres entradas nuevas, consulte la guía Bases secretas y gimnasios de jugador.
Pantalla de opciones
Ubicación: Assets/OpenMon/Core/Runtime/UI/Options/
Expone los ajustes del juego como controles basados en MenuSelector:
- Deslizadores de volumen (Global, Música, FX) via
VolumeSlider/TweenableSlider - Resolución, modo de pantalla completa, selección de monitor
- Selección de idioma
- Jugabilidad: velocidad de batalla, estilo de batalla, dificultad, dificultad de captura, modo de carrera
- Alternadores de autoguardado, formato de guardado, visualización de apodos
UI de batalla
Ubicación: Assets/OpenMon/Core/Runtime/UI/Battle/
El HUD de batalla usa subclases independientes de MenuSelector (no enrutadas a través de DialogManager):
MainBattleMenu— Fight / Bag / Monster / Run con lanzamiento rápido de la última ball (OnExtra2).BattleMoveMenuSelector— Cuadrícula de 4 movimientos.TargetMonstersMenuSelector— Selector de objetivo para batallas múltiples.BattleInfoPanel— Superposición de información detallada de estadísticas/tipo.
Agregar un menú personalizado
- Cree el prefab con un
MenuSelector(o subclase) en la raíz y elementosMenuItemhijos para cada botón. - Establezca el
ArrowSelectorPositionen cadaMenuItema un Transform hijo donde debe aparecer la flecha. - Configure
MenuNavigation(Vertical/Horizontal), las referencias de audio y el prefab de flecha en el Inspector. - Suscríbase a los eventos en su controlador:
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
}
}
- Para listas grandes, herede de
VirtualizedMenuSelector<TData, TButton, TFactory>, implementePopulateChildData()y registre su fábrica de botones enUIElementsInstaller.