UI para nuestro RPG: Parte 2
Anteriormente...
En la primera parte de esta serie, comenzamos a construir la interfaz de usuario para nuestro RPG. Implementamos un sistema básico de barras, botones y elementos de nuestra interfaz. Ahora, en esta segunda parte, vamos a expandir esa base y agregar más funcionalidades para hacer nuestra UI más interactiva y funcional.
Paneles y Contenedores
Para organizar mejor nuestra interfaz, es importante utilizar paneles y contenedores. Estos nos permiten agrupar elementos relacionados y mejorar la experiencia del usuario. En esta sección, implementaremos un sistema de paneles que nos permitirá mostrar información relevante de manera clara y ordenada.
Creando un Panel
Los paneles en Swing son contenedores que pueden contener otros componentes. Para crear un panel, podemos usar la clase JPanel. Aquí hay un ejemplo de cómo crear un panel básico:
package app.game.rpg.ui.components.panels;
import app.game.rpg.ui.components.delegates.RPGPanelUI;
import javax.swing.*;
import java.awt.*;
/**
* RPGPanel es una clase personalizada que extiende JPanel para proporcionar un panel con un borde redondeado
* y degradado de color, diseñado específicamente para nuestra interfaz gráfica de juego RPG. Este panel se puede utilizar
* para mostrar diferentes elementos del juego en distintos estados de juego, como los personajes principales o las
* características de la partida actualmente activas.
*/
public class RPGPanel extends JPanel {
/**
* Constructor para crear un objeto RPGPanel con el color de inicio y final del degradado.
*
* @param startColor Color inicial seleccionado para el degradado del panel RPGPanel.
* @param endColor Color final seleccionado para el degradado del panel RPGPanel.
*/
public RPGPanel(Color startColor, Color endColor) {
setUI(new RPGPanelUI(startColor, endColor));
}
}
En esta clase RPGPanel, extendemos JPanel y utilizamos un UI personalizado (RPGPanelUI) para darle un estilo único a nuestro panel. Este panel puede ser utilizado para mostrar información como el inventario del jugador, las estadísticas de los personajes, o cualquier otra información relevante.
package app.game.rpg.ui.components.delegates;
import app.game.rpg.ui.components.borders.RPGRoundedBorder;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicPanelUI;
import java.awt.*;
/**
* RPGPanelUI es una clase personalizada que extiende BasicPanelUI para proporcionar un panel con un borde redondeado
* y degradado de color, diseñado específicamente para nuestra interfaz gráfica de juego RPG. Este panel se puede utilizar
* para mostrar diferentes elementos del juego en distintos estados de juego, como los personajes principales o las
* características de la partida actualmente activas.
*/
public class RPGPanelUI extends BasicPanelUI {
/**
* Color inicial del degradado del panel RPGPanelUI
*/
protected final Color startColor;
/**
* Color final del degradado del panel RPGPanelUI
*/
protected final Color endColor;
/**
* Constructor para crear un objeto RPGPanelUI con el color de inicio y final del degradado.
*
* @param startColor Color inicial seleccionado para el degradado del panel RPGPanelUI.
* @param endColor Color final seleccionado para el degradado del panel RPGPanelUI.
*/
public RPGPanelUI(Color startColor, Color endColor) {
this.startColor = startColor;
this.endColor = endColor;
}
/**
* Función que permite aplicar características por defecto a un JPanel. En nuestro caso, instalamos un borde
* compuesto de un borde redondeado y un borde vacío (padding interno), eliminamos su opacidad y un layout box
* para que los componentes a agregar se coloquen de forma vertical.
*
* @param p an instance of {@code JPanel}
*/
@Override
protected void installDefaults(JPanel p) {
super.installDefaults(p);
RPGRoundedBorder border = new RPGRoundedBorder(5, 0);
EmptyBorder emptyBorder = new EmptyBorder(10, 10, 10, 10);
p.setOpaque(false);
p.setBorder(new CompoundBorder(border, emptyBorder));
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
}
/**
* Función que permite dibujar el fondo degradado del panel RPGPanelUI en un componente {@code JComponent}.
* En nuestro caso usamos un degradado radial para dibujar una textura circular con un color inicial y final,
* sin embargo, esto puede modificarse y/o agregarse diversos modelos de degradado dado por ejemplo de un Enum.
*
* @param g Elemento <code>Graphics</code> del componente a dibujar el fondo degradado.
* @param c Componente que se va a dibujar el fondo degradado del panel RPGPanelUI.
*/
@Override
public void paint(Graphics g, JComponent c) {
Graphics2D g2d = (Graphics2D) g;
g2d.setPaint(getPaintMethod(c));
g2d.fillRect(0, 0, c.getWidth(), c.getHeight());
super.paint(g, c);
g2d.dispose();
}
/**
* Función que en este caso devuelve un degradado radial. Si fuera necesario se puede modificar para incorporar
* otro tipo de degradados.
*
* @param c Componente que se va a dibujar el fondo degradado del panel RPGPanelUI.
* @return El tipo Paint que va a representar el degradado.
*/
protected Paint getPaintMethod(JComponent c) {
// Creamos un punto central en las coordenadas (x: width/2, y: height/2)
Point center = new Point(c.getWidth() / 2, c.getHeight() / 2);
// Obtenemos el radio de dibujo obteniendo el máximo entre las dimensiones width y height del componente a
// pintar y dividiendo el resultado entre dos
float radius = (float) Math.max(c.getWidth(), c.getHeight()) / 2;
// Creamos un arreglo de floats que indica el ratio en el que se van a repartir los colores inicial y final.
float[] colorsRatio = new float[]{0f, 1f};
// Un arreglo de Color para almacenar los colores inicial y final
Color[] colors = new Color[]{startColor, endColor};
return new RadialGradientPaint(center, radius, colorsRatio, colors);
}
}
En esta clase RPGPanelUI, extendemos BasicPanelUI para personalizar la apariencia de nuestro panel. Implementamos un método paint para dibujar un fondo degradado y un método installDefaults para configurar el diseño y el borde del panel.
Como podrás notar, el método getPaintMethod devuelve un degradado radial, pero puedes modificarlo para usar otros tipos de degradados según tus necesidades. En otras palabras, podemos heredar esta clase y sobreescribir el método getPaintMethod para crear diferentes estilos de paneles en nuestra interfaz.
El Panel de Información del Personaje
Ahora que tenemos nuestro panel personalizado, podemos crear un panel específico para mostrar la información del personaje. Este panel puede incluir elementos como el nombre del personaje, su nivel, salud, mana, y otras estadísticas relevantes. Aquí hay un ejemplo de cómo podríamos implementar este panel:
package app.game.rpg.ui.components.panels;
import app.game.rpg.ui.components.buttons.RPGButton;
import app.game.rpg.ui.components.delegates.RPGBlueButtonUI;
import app.game.rpg.ui.components.delegates.RPGRedButtonUI;
import app.game.rpg.ui.components.labels.RPGLabel;
import app.game.rpg.ui.components.utils.RPGGradientType;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
/**
* Clase qué representa un slot, es decir una casilla en el que se puede colocar una partida guardada.
*/
public class RPGSlotPanel extends RPGPanel {
/**
* Nombre del slot.
*/
private final String slotName;
/**
* Constructor de la clase RPGSlotPanel que recibe el nombre del slot.
*
* @param slotName Nombre del slot.
*/
public RPGSlotPanel(String slotName) {
super(new Color(104, 0, 255, 255), new Color(39, 0, 95, 255));
this.slotName = slotName;
init();
}
/**
* Función que inicializa el slot.
*/
private void init() {
Font font = null;
try {
font = Font.createFont(Font.TRUETYPE_FONT, new File(Objects.requireNonNull(getClass().getResource("/fonts/BoldPixels.ttf")).getPath()));
} catch (FontFormatException | IOException e) {
throw new RuntimeException(e);
}
setBorder(new TitledBorder(getBorder(), slotName, TitledBorder.CENTER, TitledBorder.BELOW_TOP,
font.deriveFont(Font.PLAIN, 30f), Color.WHITE));
setLayout(new GridBagLayout());
addComponents();
}
/**
* Función que agrega los componentes al slot.
*/
private void addComponents() {
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = 2;
gbc.gridheight = 1;
gbc.insets = new Insets(10,10,10,10); //top padding
gbc.fill = GridBagConstraints.BOTH;
gbc.anchor = GridBagConstraints.CENTER;
add(new RPGLabel("Nombre: ", RPGGradientType.NONE), gbc);
gbc.gridy++;
add(new RPGLabel("Nivel: ", RPGGradientType.NONE), gbc);
gbc.gridy++;
add(new RPGLabel("Vida: X / Y", RPGGradientType.NONE), gbc);
gbc.gridy++;
add(new RPGLabel("Mana: X / Y", RPGGradientType.NONE), gbc);
gbc.gridy++;
add(new RPGLabel("Experiencia: ", RPGGradientType.NONE), gbc);
gbc.gridy++;
gbc.gridwidth= 1;
add(new RPGButton("Cargar"), gbc);
gbc.gridx++;
add(new RPGButton("Eliminar", new RPGRedButtonUI()), gbc);
gbc.gridx++;
add(new RPGButton("Nueva Partida", new RPGBlueButtonUI()), gbc);
}
}
En este ejemplo, RPGSlotPanel es un panel que representa un slot de partida guardada. Utiliza la clase RPGPanelUI para su apariencia y agrega varios componentes para mostrar la información del personaje, como su nombre, nivel, vida, mana y experiencia. También incluye botones para cargar, eliminar o crear una nueva partida. Puedes personalizar este panel aún más agregando más estadísticas o elementos visuales según las necesidades de tu juego.
Así mismo, podrás notar que este panel usa un GridBagLayout para organizar sus componentes, lo que permite una mayor flexibilidad en la disposición de los elementos dentro del panel. Puedes ajustar las restricciones del GridBagConstraints para cambiar la apariencia y el diseño de los componentes según tus preferencias.
Los botones personalizados
Además de los paneles personalizados, también podemos crear botones personalizados para nuestra interfaz. Por ejemplo, podríamos crear un botón con un diseño específico para cargar una partida guardada o para eliminar una partida. Aquí hay un ejemplo de cómo podríamos implementar un botón personalizado:
package app.game.rpg.ui.components.buttons;
import app.game.rpg.ui.components.delegates.RPGButtonUI;
import javax.swing.*;
public class RPGButton extends JButton {
public RPGButton(String text) {
super(text);
setUI(new RPGButtonUI());
}
public RPGButton(String text, RPGButtonUI ui) {
super(text);
setUI(ui);
}
}
En este ejemplo, RPGButton es una clase que extiende JButton y utiliza un RPGButtonUI para su apariencia. Puedes crear diferentes implementaciones de RPGButtonUI para tener botones con diferentes estilos, como un botón azul para crear una nueva partida o un botón rojo para eliminar una partida. Aquí hay un ejemplo de cómo podríamos implementar un botón verde personalizado:
package app.game.rpg.ui.components.delegates;
import app.game.rpg.ui.components.borders.RPGRoundedBorder;
import app.game.rpg.utils.FontCache;
import app.game.rpg.utils.ColorPalette;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicButtonUI;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
/**
* Clase personalizada para el UI de los botones en el RPG.
*/
public class RPGButtonUI extends BasicButtonUI {
protected boolean hover = false;
protected boolean pressed = false;
private final int radius = 15;
protected Color top;
protected Color bottom;
/**
* Instala los valores por defecto de los botones.
*
* @param b el botón al que se le aplicarán los estilos.
*/
@Override
protected void installDefaults(AbstractButton b) {
FontCache.addFont("BoldPixels", "BoldPixels.ttf");
b.setContentAreaFilled(false);
b.setFocusPainted(false);
b.setOpaque(false);
b.setForeground(Color.WHITE);
b.setCursor(new Cursor(Cursor.HAND_CURSOR));
b.setFont(FontCache.getFont("BoldPixels").deriveFont(22f));
b.setBorder(new RPGRoundedBorder(3, radius));
b.setBorder(new CompoundBorder(b.getBorder(), new EmptyBorder(5, 15, 5, 15)));
b.setPreferredSize(new Dimension((int) Math.max(b.getPreferredSize().getWidth(), 100), (int) b.getPreferredSize().getHeight()));
}
/**
* Función que agrega los listeners al botón.
*
* @param b botón al que se le agregarán los listeners.
*/
@Override
protected void installListeners(AbstractButton b) {
b.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
hover = true;
b.repaint();
}
@Override
public void mouseExited(MouseEvent e) {
hover = false;
pressed = false;
b.repaint();
}
@Override
public void mousePressed(MouseEvent e) {
pressed = true;
b.repaint();
}
@Override
public void mouseReleased(MouseEvent e) {
pressed = false;
b.repaint();
}
});
}
@Override
public void paint(Graphics g, JComponent c) {
Graphics2D g2 = (Graphics2D) g.create();
// Suavizado
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
int w = c.getWidth();
int h = c.getHeight();
// 🎨 Selección de colores según estado
LinearGradientPaint gradient = getLinearGradientPaint(c);
g2.setPaint(gradient);
g2.fillRoundRect(0, 0, w, h, radius, radius);
// ✨ Efecto de brillo superior (opcional pero muy RPG)
GradientPaint highlight = new GradientPaint(
0, 0, new Color(255, 255, 255, 100),
0, (float) h / 2, new Color(255, 255, 255, 0)
);
g2.setPaint(highlight);
g2.fillRoundRect(2, 2, w - 4, h / 2, radius, radius);
// 🔤 Dibuja el texto encima
super.paint(g2, c);
g2.dispose();
}
private LinearGradientPaint getLinearGradientPaint(JComponent c) {
getColors();
// 🔵 Fondo degradado
return new LinearGradientPaint(0, 0, 0, c.getHeight(),
new float[]{0f, 0.25f, 1f}, new Color[]{top, bottom, top});
}
protected void configureColors() {
if (pressed) {
top = ColorPalette.GREEN_PRESSED_TOP;
bottom = ColorPalette.GREEN_PRESSED_BOTTOM;
} else if (hover) {
top = ColorPalette.GREEN_HOVER_TOP;
bottom = ColorPalette.GREEN_HOVER_BOTTOM;
} else {
top = ColorPalette.GREEN_TOP;
bottom = ColorPalette.GREEN_BOTTOM;
}
}
}
En este ejemplo, RPGButtonUI es una clase que extiende BasicButtonUI y personaliza la apariencia de los botones. Utiliza un degradado para el fondo del botón y cambia los colores según el estado del botón (normal, hover o pressed). También agrega un efecto de brillo en la parte superior del botón para darle un aspecto más atractivo. Puedes crear otras clases similares para diferentes estilos de botones, como un botón rojo para eliminar una partida o un botón azul para crear una nueva partida.
Por ejemplo, podríamos crear un RPGRedButtonUI para los botones de eliminación:
package app.game.rpg.ui.components.delegates;
import app.game.rpg.utils.ColorPalette;
public class RPGRedButtonUI extends RPGButtonUI{
@Override
protected void configureColors() {
if (pressed) {
top = ColorPalette.RED_TOP;
bottom = ColorPalette.RED_BOTTOM;
} else if (hover) {
top = ColorPalette.RED_HOVER_TOP;
bottom = ColorPalette.RED_HOVER_BOTTOM;
} else {
top = ColorPalette.RED_TOP;
bottom = ColorPalette.RED_BOTTOM;
}
}
}
En este ejemplo, RPGRedButtonUI extiende RPGButtonUI y simplemente sobrescribe el método configureColors() para usar una paleta de colores roja en lugar de verde. De esta manera, puedes reutilizar la lógica de dibujo y solo cambiar los colores según el tipo de botón que quieras crear.
Las etiquetas personalizadas
Además de los botones, también podemos crear etiquetas personalizadas para mostrar información importante en nuestra interfaz, como el nombre del personaje, su nivel o su salud. Aquí hay un ejemplo de cómo podríamos implementar una etiqueta personalizada:
package app.game.rpg.ui.components.labels;
import app.game.rpg.ui.components.borders.RPGRoundedBorder;
import app.game.rpg.ui.components.delegates.RPGLabelUI;
import app.game.rpg.ui.components.utils.RPGGradientType;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import java.awt.*;
public class RPGLabel extends JLabel {
private Color backgroundColor;
public RPGLabel(String text) {
super(text);
backgroundColor = new Color(104, 0, 255, 255);
setUI(new RPGLabelUI());
}
public RPGLabel(String text, RPGGradientType gradientType) {
super(text);
backgroundColor = new Color(104, 0, 255, 255);
setUI(new RPGLabelUI(gradientType));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
}
}
En este ejemplo, RPGLabel es una clase que extiende JLabel y utiliza un RPGLabelUI para su apariencia. Al igual que con los botones, puedes crear diferentes implementaciones de RPGLabelUI para tener etiquetas con diferentes estilos, como una etiqueta con un fondo degradado o una etiqueta con un borde redondeado.
El delegador RPGLabelUI se encargará de dibujar el fondo y el borde de la etiqueta, mientras que el texto se dibujará automáticamente por la clase JLabel de la siguiente manera:
package app.game.rpg.ui.components.delegates;
import app.game.rpg.ui.components.borders.RPGRoundedBorder;
import app.game.rpg.ui.components.utils.RPGGradientType;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicLabelUI;
import java.awt.*;
import java.io.IOException;
import java.util.Objects;
public class RPGLabelUI extends BasicLabelUI {
public RPGGradientType gradientType = RPGGradientType.LINEAR_VERTICAL;
public RPGLabelUI() {
super();
}
public RPGLabelUI(RPGGradientType gradientType) {
super();
this.gradientType = gradientType;
}
@Override
protected void installDefaults(JLabel c) {
Font font;
try {
font = Font.createFont(Font.TRUETYPE_FONT, Objects.requireNonNull(getClass().getResourceAsStream("/fonts/BoldPixels.ttf"))).deriveFont(Font.PLAIN, 22f);
} catch (FontFormatException | IOException e) {
throw new RuntimeException(e);
}
c.setForeground(Color.WHITE);
c.setFont(font);
c.setHorizontalAlignment(SwingConstants.LEFT);
c.setVerticalAlignment(SwingConstants.CENTER);
// 🔥 Aplicamos el borde reutilizable
if (gradientType != RPGGradientType.NONE) {
c.setBorder(new RPGRoundedBorder(3, 10));
c.setBorder(new CompoundBorder(c.getBorder(), new EmptyBorder(5, 10, 5, 10)));
}
}
@Override
public void paint(Graphics g, JComponent c) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
if (gradientType != RPGGradientType.NONE) {
Paint gradient = switch (gradientType) {
case LINEAR_VERTICAL -> getLinearGradient(c.getWidth(), c.getHeight());
default -> getLinearGradient(c.getWidth(), c.getHeight());
};
g2d.setPaint(gradient);
g2d.fillRoundRect(0, 0, c.getWidth(), c.getHeight(), 10, 10);
}
super.paint(g2d, c);
g2d.dispose();
}
public Paint getLinearGradient(int width, int height) {
return new LinearGradientPaint(0, 0, 0, height,
new float[]{0f, 0.5f, 1f},
new Color[]{new Color(39, 0, 95, 255), new Color(104, 0, 255, 255), new Color(39, 0, 95, 255)});
}
}
Como podrás notar, RPGLabelUI es un delegador que se encarga de dibujar el fondo y el borde de la etiqueta. Utiliza un degradado para el fondo y un borde redondeado para darle un aspecto más atractivo. El texto se dibuja automáticamente por la clase JLabel, por lo que no necesitamos preocuparnos por eso en el delegador. Sin embargo, podemos usar el enum RPGGradientType para permitir diferentes tipos de degradados en nuestras etiquetas, lo que nos da aún más flexibilidad para personalizar la apariencia de nuestra interfaz. Esto mediante el proceso de herencia y composición, donde RPGLabelUI puede ser extendido para crear diferentes estilos de etiquetas sin tener que duplicar la lógica de dibujo.
Conclusión
En esta sección, hemos visto cómo crear componentes personalizados para nuestra interfaz de usuario en un juego RPG utilizando Java Swing. Hemos implementado botones y etiquetas con estilos únicos que se adaptan a la temática de nuestro juego. Al utilizar delegadores como RPGButtonUI y RPGLabelUI, podemos separar la lógica de dibujo de la lógica de negocio, lo que nos permite crear componentes reutilizables y fáciles de mantener. Además, al usar una paleta de colores consistente y efectos visuales como degradados y bordes redondeados, podemos darle a nuestra interfaz un aspecto más atractivo y profesional. En la próxima sección, exploraremos cómo organizar estos componentes en paneles y ventanas para construir la interfaz completa de nuestro juego RPG.
En los siguientes apartados, algunos otros elementos como campos de entrada, scroll panes, y el uso de ventanas emergentes (JDialog) para mostrar información adicional o confirmar acciones importantes, como eliminar una partida guardada. También veremos cómo manejar eventos de usuario para que nuestra interfaz sea interactiva y responda a las acciones del jugador de manera efectiva.
UI para nuestro RPG: Parte 1
En esta sección comenzaremos a diseñar la interfaz gráfica de nuestro RPG utilizando Swing, creando una ventana principal y los componentes básicos para la gestión de personajes.
Los Streams Entrada y Salida
En este capítulo se explican los Streams de Entrada y Salida en Java, que son fundamentales para manejar la lectura y escritura de datos en archivos y otros medios.