UI para nuestro RPG: Parte 4
Componentes extras para la UI de nuestro RPG
En esta sección, se explorarán algunos componentes adicionales que pueden mejorar la experiencia del usuario en la interfaz de nuestro RPG. Estos componentes pueden incluir menús desplegables, zonas con scroll, y otros elementos interactivos que pueden hacer que la interfaz sea más atractiva y funcional.
Menús desplegables
Los menús desplegables son una excelente manera de organizar opciones y configuraciones en la interfaz de usuario. En Java, se pueden crear menús desplegables utilizando la clase JComboBox de Swing. Este componente permite a los usuarios seleccionar una opción de una lista desplegable, lo que puede ser útil para configuraciones de juego, selección de personajes, o cualquier otra funcionalidad que requiera opciones múltiples.
JComboBox y ComboPopup
Para los menús desplegables, debemos entender que el componente se divide en dos partes, por un lado, el ComboBox que es el elemento que se muestra en la interfaz, y, por otro lado, el ComboPopup que es el componente que se despliega cuando se hace clic en el ComboBox.
Por lo que las clases base deberían verser de la siguiente manera:
package app.game.rpg.ui.components.inputs;
import app.game.rpg.ui.components.borders.RPGRoundedBorder;
import app.game.rpg.ui.components.delegates.RPGCellRenderer;
import app.game.rpg.ui.components.delegates.RPGComboBoxUI;
import app.game.rpg.utils.FontCache;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicComboPopup;
import javax.swing.plaf.basic.ComboPopup;
import java.awt.*;
public class RPGComboBox<T> extends JComboBox<T> {
public RPGComboBox(T[] items) {
super(items);
}
@Override
public void updateUI() {
setUI(new RPGComboBoxUI());
}
}
package app.game.rpg.ui.components.inputs;
import app.game.rpg.ui.components.borders.RPGRoundedBorder;
import app.game.rpg.ui.components.delegates.Interactable;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicComboPopup;
import java.awt.*;
public class RPGComboPopup extends BasicComboPopup {
public RPGComboPopup(JComboBox<Object> combo) {
super(combo);
setBorder(new RPGRoundedBorder(3, 0));
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
protected void configureList() {
super.configureList();
list.setSelectionBackground(comboBox.getBackground());
list.setSelectionForeground(comboBox.getForeground());
}
}
Nota que en este caso el único delegate es el ComboBox ya que el ComboPopup no necesita ningún delegate.
Ahora veamos el delegate del ComboBox.
package app.game.rpg.ui.components.delegates;
import app.game.rpg.ui.components.borders.RPGRoundedBorder;
import app.game.rpg.ui.components.buttons.RPGColorState;
import app.game.rpg.ui.components.inputs.RPGComboPopup;
import app.game.rpg.ui.components.listeners.RPGMouseListener;
import app.game.rpg.utils.ColorPalette;
import app.game.rpg.utils.FontCache;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicComboBoxUI;
import javax.swing.plaf.basic.ComboPopup;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.Objects;
import static com.sun.java.accessibility.util.AWTEventMonitor.addMouseListener;
public class RPGComboBoxUI extends BasicComboBoxUI implements Interactable {
protected Color top;
protected Color bottom;
protected RPGMouseListener listener;
protected HashMap<RPGColorState, Color> colors = new HashMap<>();
@Override
public Paint getPaint(JComponent c) {
getColors();
// 🔵 Fondo degradado
return new LinearGradientPaint(0, 0, 0,
comboBox.getHeight(),
new float[]{0f, 0.25f, 1f}, new Color[]{top, bottom, top});
}
@Override
public void getColors() {
if (listener.isPressed()) {
top = ColorPalette.BLUE_PRESSED_TOP;
bottom = ColorPalette.BLUE_PRESSED_BOTTOM;
} else if (listener.isHover()) {
top = ColorPalette.BLUE_HOVER_TOP;
bottom = ColorPalette.BLUE_HOVER_BOTTOM;
} else {
top = ColorPalette.BLUE_TOP;
bottom = ColorPalette.BLUE_BOTTOM;
}
}
@Override
public void configureColors() {
colors.put(RPGColorState.DEFAULT_TOP, ColorPalette.BLUE_TOP);
colors.put(RPGColorState.DEFAULT_BOTTOM, ColorPalette.BLUE_BOTTOM);
colors.put(RPGColorState.HOVER_TOP, ColorPalette.BLUE_HOVER_TOP);
colors.put(RPGColorState.HOVER_BOTTOM, ColorPalette.BLUE_HOVER_BOTTOM);
colors.put(RPGColorState.PRESSED_TOP, ColorPalette.BLUE_PRESSED_TOP);
colors.put(RPGColorState.PRESSED_BOTTOM, ColorPalette.BLUE_PRESSED_BOTTOM);
}
@Override
protected void installDefaults() {
super.installDefaults();
listener = new RPGMouseListener(comboBox);
comboBox.setForeground(Color.WHITE);
comboBox.setOpaque(false);
comboBox.setEditable(false);
comboBox.setFont(FontCache.getFont("BoldPixels").deriveFont(24f));
comboBox.setBorder(new RPGRoundedBorder(3, 5));
comboBox.setFocusable(false);
comboBox.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
comboBox.setForeground(Color.WHITE);
comboBox.setRenderer(new RPGCellRenderer());
}
@Override
public void paintCurrentValue(Graphics g, Rectangle bounds, boolean hasFocus) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
//
g2.setPaint(getPaint(comboBox));
int x = 2;
int y = 2;
int w = comboBox.getWidth() - 5;
int h = comboBox.getHeight() - 5;
g2.fillRoundRect(x, y, w, h, 0, 0);
installRPGBright(g2, comboBox.getWidth(), comboBox.getHeight(), 0);
g2.setFont(comboBox.getFont());
g2.setColor(comboBox.getForeground());
x = bounds.x + 5;
y = ((comboBox.getFontMetrics(comboBox.getFont()).getHeight() + bounds.y) / 2) + 10;
w = bounds.width;
h = bounds.height;
g2.drawString(Objects.requireNonNull(comboBox.getSelectedItem()).toString(), x, y);
g2.dispose();
}
@Override
public void installRPGBright(Graphics2D g2, int w, int h, int radius) {
Color color = new Color(255, 255, 255, 100);
Color transparent = new Color(255, 255, 255, 0);
// ✨ Efecto de brillo superior (opcional pero muy RPG)
GradientPaint highlight = new GradientPaint(
0, 0, color,
0, (float) (h - 6) / 2, transparent
);
g2.setPaint(highlight);
g2.fillRoundRect(2, 2, w - 4, h / 2, radius, radius);
}
@Override
protected ComboPopup createPopup() {
return new RPGComboPopup(comboBox);
}
@Override
protected JButton createArrowButton() {
return null;
}
@Override
protected void installListeners() {
super.installListeners();
comboBox.addMouseListener(listener);
}
}
Como podrás notar, el delegate hace uso de una utilidad para el gradient, así como listener especial y un CellRenderer.
Ahora veamos el CellRenderer y el Listener.
package app.game.rpg.ui.components.delegates;
import app.game.rpg.ui.components.listeners.RPGMouseListener;
import app.game.rpg.utils.ColorPalette;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.ListUI;
import javax.swing.plaf.basic.BasicListUI;
import javax.swing.plaf.synth.SynthListUI;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
public class RPGCellRenderer extends DefaultListCellRenderer {
protected Color top;
protected Color bottom;
public Component getListCellRendererComponent(
JList<?> list,
Object value,
int index,
boolean isSelected,
boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
setBorder(new EmptyBorder(5, 5, 5, 5));
if (isSelected) {
top = ColorPalette.BLUE_TOP;
bottom = ColorPalette.BLUE_BOTTOM;
} else {
top = ColorPalette.GREEN_TOP;
bottom = ColorPalette.GREEN_BOTTOM;
}
return this;
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setPaint(getLinearGradientPaint());
g2d.fillRect(0, 0, getWidth(), getHeight());
// ✨ Efecto de brillo superior (opcional pero muy RPG)
GradientPaint highlight = new GradientPaint(
0, 0, new Color(255, 255, 255, 100),
0, (float) getHeight() / 2, new Color(255, 255, 255, 0)
);
g2d.setPaint(highlight);
g2d.fillRect(0, 0, getWidth(), getHeight());
super.paintComponent(g2d);
g2d.dispose();
}
private LinearGradientPaint getLinearGradientPaint() {
// 🔵 Fondo degradado
return new LinearGradientPaint(0, 0, 0, getHeight(),
new float[]{0f, 0.25f, 1f}, new Color[]{top, bottom, top});
}
}
El CellRenderer hace uso de un efecto de brillo superior, lo que le da un aspecto más RPG.
Ahora veamos el Listener.
package app.game.rpg.ui.components.listeners;
import javax.swing.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class RPGMouseListener extends MouseAdapter {
protected boolean hover = false;
protected boolean pressed = false;
protected JComponent component;
public RPGMouseListener(JComponent component) {
this.component = component;
}
@Override
public void mouseEntered(MouseEvent e) {
hover = true;
component.repaint();
}
@Override
public void mouseExited(MouseEvent e) {
hover = false;
pressed = false;
component.repaint();
}
@Override
public void mousePressed(MouseEvent e) {
pressed = true;
component.repaint();
}
@Override
public void mouseReleased(MouseEvent e) {
pressed = false;
component.repaint();
}
public boolean isHover() {
return hover;
}
public boolean isPressed() {
return pressed;
}
}
Ten en cuenta que el listener es un componente especial que se encarga de los eventos de mouse y los estados de hover y pressed.
Así mismo te presento la clase encargada de los colores.
package app.game.rpg.utils;
import java.awt.*;
public class ColorPalette {
public static Color GREEN_TOP = new Color(35, 165, 35);
public static Color GREEN_BOTTOM = new Color(15, 73, 15);
public static Color GREEN_HOVER_TOP = GREEN_TOP.brighter();
public static Color GREEN_HOVER_BOTTOM = GREEN_BOTTOM.brighter();
public static Color GREEN_PRESSED_TOP = GREEN_TOP.darker();
public static Color GREEN_PRESSED_BOTTOM = GREEN_BOTTOM.darker();
public static Color RED_TOP = new Color(191, 37, 37);
public static Color RED_BOTTOM = new Color(80, 16, 16);
public static Color RED_HOVER_TOP = RED_TOP.brighter();
public static Color RED_HOVER_BOTTOM = RED_BOTTOM.brighter();
public static Color RED_PRESSED_TOP = RED_TOP.darker();
public static Color RED_PRESSED_BOTTOM = RED_BOTTOM.darker();
public static Color BLUE_TOP = new Color(10, 111, 178);
public static Color BLUE_BOTTOM = new Color(5, 55, 89);
public static Color BLUE_HOVER_TOP = BLUE_TOP.brighter();
public static Color BLUE_HOVER_BOTTOM = BLUE_BOTTOM.brighter();
public static Color BLUE_PRESSED_TOP = BLUE_TOP.darker();
public static Color BLUE_PRESSED_BOTTOM = BLUE_BOTTOM.darker();
public static Color BORDER_LIGHT = new Color(255, 215, 0);
public static Color BORDER_DARK = new Color(128, 64, 0);
}
Conclusión
Como podrás notar el ComboBox resulta un tanto particular al involucrar varios elementos que son necesarios para su funcionamiento. Sin embargo, al comprender como trabaja el componente, es más fácil de usar y personalizar.
UI para nuestro RPG: Parte 3
En esta tercera parte de la serie sobre la UI para nuestro RPG, continuamos desarrollando la interfaz gráfica utilizando Java Swing, añadiendo más componentes y funcionalidades para mejorar la experiencia del usuario.
UI para nuestro RPG: Parte 5
En esta quinta entrega hablaremos de las caches y el uso de imágenes