Saltar al contenido principal

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.

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​

EventoTipoSe dispara cuando
OnButtonSelectedAction<int>El jugador confirma una selección (índice del elemento elegido)
OnHoveredAction<int>El cursor se mueve a un nuevo elemento
OnBackSelectedActionEl 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:

  1. OnNavigation() lee el vector de entrada y extrae el eje relevante (Y para vertical, X para horizontal).
  2. Navigate() incrementa o decrementa el índice de selección, envolviendo en los límites.
  3. Los botones no interactuables se omiten automáticamente.
  4. 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);
}
}

Ubicación: Assets/OpenMon/Core/Runtime/UI/MenuItem.cs

Cada botón en un MenuSelector es un MenuItem — un HidableAndSubscribableButton con:

  • ArrowSelectorPosition — un Transform hijo 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 via LayoutElement.ignoreLayout.
  • OnSelect() / OnDeselect() — llamados por el MenuSelector padre para disparar el estado de selección del EventSystem de 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 ScrollRect se 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:

ÍndiceEntradaAcción
0DexDialogManager.ShowDexScreen(...)
1MonstersDialogManager.ShowPlayerRosterMenu(...)
2BagDialogManager.ShowBag(...)
3MapMapSceneLauncher.ShowMap(...)
4QuestsDialogManager.ShowQuestsScreen(...)
5ProfileDialogManager.ShowProfileScreen(...)
6OptionsDialogManager.ShowOptionsMenu()
7ExitSolicita guardar, luego salir o volver al título
9Battle TowerEphemeralTowerLauncher.Launch(diContainer, playerCharacter) — partida infinita y efímera
10My BaseEnterOrDecorateSecretBase() — entra al interior, o abre la paleta de decoración cuando está dentro
11My GymEnterOrManageGym() — 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ú:

EntradaGate 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​

  1. Cree el prefab con un MenuSelector (o subclase) en la raíz y elementos MenuItem hijos para cada botón.
  2. Establezca el ArrowSelectorPosition en cada MenuItem a un Transform hijo donde debe aparecer la flecha.
  3. Configure MenuNavigation (Vertical/Horizontal), las referencias de audio y el prefab de flecha en el Inspector.
  4. 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
}
}
  1. Para listas grandes, herede de VirtualizedMenuSelector<TData, TButton, TFactory>, implemente PopulateChildData() y registre su fábrica de botones en UIElementsInstaller.