JTextField se te quedaría pequeño. Mejor un JTextArea.
Sin embargo, hacer esto va a requerir bastantes cambios en todo el proyecto.
La clase Batalla ahora tendrá que comunicarse de algún modo con ese JTextArea para lanzarle mensajes.
Pero recuerdo que algunos mensajes del combate, los lanzaban las distintas clases Soldado desde sus métodos atacar()..., ¿también habrá que buscarles conexión con el JTextArea o mejor pensar en otra solución?
También, usabamos un Scanner simplemente para pausar la consola de texto, poder leer lo que salía en pantalla, y pulsar la tecla enter para continuar con el siguiente turno de ataque.
Esto ahora no se podrá hacer y lo que vamos a tener van a ser decenas de mensajes a la velocidad de la luz, los combates van a durar apenas unos milisegundos.
Se puede intentar añadir un retardo entre un mensaje y otro..., pero para ello vamos a tener que jugar con hilos (threads)
Lo ideal sería reescribir el proyecto desde casi cero, ya teniendo en mente que estará destinado a una GUI y no ha mostrarse por consola de texto.
Pero bueno, parcheando por un lado y por otro, supongo que se podrá apañar...
De momento, crearía una nueva clase JPanel, que tenga un botón y un JTextArea.
Le daremos un método para que reciba un String y lo añada como nueva línea al JTextArea. Así abrimos una vía para que otra clase (Batalla) pueda indicarle que ha de ir mostrando.
El botón de este panel, ha de poner en marcha la clase Batalla, la cuál ha de tener acceso a los Ejercitos (Heroes y Bestias) que se encuentran en la clase JFrame principal.
Esto significa que desde este nuevo panel, no podemos llamar a la clase Batalla, porque desde aquí no podemos "ver" a los ejércitos.
Se puede solucionar tal y como hemos hecho en los paneles anteriores, añadir atributos para poder referenciar a esos ejercitos mediante el constructor de este panel.
Pero otra opción, y que podríamos aplicarla aquí para variar un poco, sería escribir el ActionListener de este botón en la clase principal, y luego hacérselo llegar mediante un método.
Así que haremos eso, un método que reciba un ActionListener y lo aplique al botón de este panel.
public class PanelTexto extends JPanel {
private JTextArea areaTexto;
private JButton btLuchar;
public PanelTexto() {
areaTexto = new JTextArea();
areaTexto.setEditable(false);
btLuchar = new JButton("¡Luchar!");
JScrollPane scrollArea = new JScrollPane();
scrollArea.setViewportView(areaTexto);
scrollArea.setPreferredSize(new Dimension(50, 200));
scrollArea.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(10, 20, 20, 20),
BorderFactory.createLoweredSoftBevelBorder()));
JPanel pnBoton = new JPanel();
pnBoton.add(btLuchar);
setLayout(new BorderLayout());
add(pnBoton, BorderLayout.NORTH);
add(scrollArea, BorderLayout.CENTER);
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(0, 10, 10, 10),
BorderFactory.createRaisedSoftBevelBorder()));
}
//La accion de este botón se escribirá en otra clase
public void setAccionBotonLuchar(ActionListener accion) {
btLuchar.addActionListener(accion);
}
public void nuevaLinea(String linea) {
areaTexto.append(linea + "\n");
}
}
Luego veremos como ponemos este panel en el JFrame, pero antes vamos tratar el tema de la clase Batalla y hacer que los mensajes salgan con algo de retraso entre ellos.
Para esto, como dije antes, deberíamos transformar la clase Batalla en un
Thread, un hilo.
No se si has trabajado con hilos antes, no son complicados la verdad. Si lo convertimos en un hilo, el proceso de batalla se llevará a cabo por separado del proceso que estará gestionando la interfaz gráfica: ventana, botones, etc..
Eso nos permite cosas como hacer "dormir" ese hilo durante unos milisegundos cada vez que queramos, y así podremos ver como los mensajes van apareciendo en el area de texto de uno en uno, y no todos de golpe como ocurrirá si no lo hacemos en un hilo paralelo.
Para convertir Batalla en un hilo, basta con hacer que herede de la clase Thread y poner el código que queremos que se ejecute en un método llamado run().
Si no quieres "cargarte" la clase que ya tenemos, puedes crear otra nueva.
Yo lo prefiero así y he creado una clase llamada
BatallaHilo.
Le vamos a dar tres atributos: dos objetos
Ejercito y un objeto
PanelTexto, que será una referencia al nuevo panel que acabamos de crear. Así esta clase tendrá acceso a todo lo que necesita, los Ejércitos para que se líen a mamporrazos entre ellos y el área de texto donde ir publicando los detalles de la contienda.
En el método run(), vamos a poner el mismo código que teníamos en el método estático llamado
batallar() de la anterior clase Batalla.
El código va a ser el mismo, pero los System.out.println() los vamos cambiar por llamadas al PanelTexto donde le pasamos los Strings para que los muestre en el JTextArea.
Es decir, donde antes hacíamos esto:
System.out.println(bestia.getNombre() + " ha muerto.");
Ahora haremos:
pnTexto.nuevaLinea(bestia.getNombre() + " ha muerto.");
El método pausa() donde antes usábamos el Scanner para detener la consola, ahora lo vamos a cambiar por un código que hará dormitar el hilo durante los milisegundos que queramos indicarle.
Esta sería la clase
BatallaHilopublic class BatallaHilo extends Thread {
private Ejercito heroes;
private Ejercito bestias;
private PanelTexto pnTexto;
public BatallaHilo(Ejercito heroes, Ejercito bestias, PanelTexto pnTexto) {
this.heroes = heroes;
this.bestias = bestias;
this.pnTexto = pnTexto;
}
@Override
public void run() {
//Mientras ningun ejército haya sido derrotado....
while(!heroes.esDerrotado() && !bestias.esDerrotado()) {
//Calculamos el total de turnos, según el ejército más grande
int turnosTotal;
if (heroes.soldados.size() >= bestias.soldados.size())
turnosTotal = heroes.soldados.size();
else
turnosTotal = bestias.soldados.size();
//Comienza una ronda de turnos
for (int turno = 0; turno < turnosTotal; turno++) {
//Seleccionamos combatientes
Heroe heroe = (Heroe) heroes.getSoldado(turno);
Bestia bestia = (Bestia) bestias.getSoldado(turno);
//Comprobamos que ninguno sea null
if (heroe == null && bestia == null)
//¿Ambos son null?Entonces esta ronda de turnos ha terminado
break;
else if (heroe == null) {
//No hay Heroe, Bestia queda en guardia
pnTexto.nuevaLinea(bestia.getNombre() + " queda en guardia");
pausa(500);
}
else if (bestia == null) {
//No hay Bestia, Heroe queda en guardia
pnTexto.nuevaLinea(heroe.getNombre() + " queda en guardia");
pausa(500);
}
else {
//Ninguno es null, comienza el combate
pnTexto.nuevaLinea("Lucha entre " + heroe + " y " + bestia);
pausa(250);
//Turno heroe
pnTexto.nuevaLinea("Turno de " + heroe.getNombre());
pausa(250);
pnTexto.nuevaLinea(heroe.atacar(bestia));
pnTexto.nuevaLinea("Datos Actualizados de " + bestia);
pausa(1000);
if (bestia.estaMuerto()) {
pnTexto.nuevaLinea(bestia.getNombre() + " ha muerto.");
pausa(1000);
}
else {
//Turno bestia
pnTexto.nuevaLinea("Turno de " + bestia.getNombre());
pausa(250);
pnTexto.nuevaLinea(bestia.atacar(heroe));
pnTexto.nuevaLinea("Datos Actualizados de " + heroe);
pausa(1000);
if (heroe.estaMuerto()) {
pnTexto.nuevaLinea(heroe.getNombre() + " ha muerto.");
pausa(1000);
}
}
}
//Turno combate finalizado, ejercitos actualizan sus filas
heroes.comprobarEjercito();
bestias.comprobarEjercito();
//Y se inicia el siguiente turno
}
}
//Las rondas de turnos han finalizado porque algún ejército ha sido derrotado. Comprobamos
if (heroes.esDerrotado())
pnTexto.nuevaLinea("Han ganado las Bestias. Soldados restantes: " + bestias.soldados.size());
else
pnTexto.nuevaLinea("Han ganado los Heroes. Soldados restantes: " + heroes.soldados.size());
}
private void pausa(long milis) {
try {
sleep(milis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Hay otro cambio importante aquí. Recuerda que dije que algunos mensajes en pantalla, venían del método atacar de las clases Soldado, por ejemplo:
@Override
public void atacar(Soldado enemigo) {
if (enemigo instanceof Orcos) {
//Regla específica cuando un Elfo ataca un Orco
Random dado = new Random();
int tirada1 = dado.nextInt(101);
int tirada2 = dado.nextInt(101);
System.out.println("Primer dado: " + tirada1);
System.out.println("Segundo dado: " + tirada2);
int maximo = Math.max(tirada1, tirada2);
System.out.println("*****¡¡El odio élfico hacia los Orcos incrementa el ataque en 1.5x!!*****");
maximo *= 1.5;
System.out.println("Valor de ataque resultante: " + maximo);
enemigo.recibirAtaque(maximo);
}
else //Si no es Orco, se aplica la regla general de los Heroes
super.atacar(enemigo);
}
Estos mensajes se lanzan a la consola durante la ejecución de la "batalla"_
else {
//Turno bestia
System.out.println("Turno de " + bestia.getNombre());
bestia.atacar(heroe);
System.out.println("Datos Actualizados de " + heroe);
Nosotros hemos reconducido los System.out de la clase
Batalla para que ahora salgan por el area de texto, pero estos no los tenemos controlados porque provienen de otras clases...
¿Cómo hacemos para tener control sobre ellos?
Pues vamos a tener que cambiar los métodos atacar() de las clases Soldado, para que en lugar de que hagan System.out por su cuenta, nos retornen esos mensajes como String, y así podamos reconducirlos para que vayan al JTextArea.
Esta "reconducción", ya la he puesto en el código de la clase
BatallaHilo que he puesto antes:
else {
//Turno bestia
pnTexto.nuevaLinea("Turno de " + bestia.getNombre());
pausa(250);
pnTexto.nuevaLinea(bestia.atacar(heroe));
pnTexto.nuevaLinea("Datos Actualizados de " + heroe);
Pero para que funcione, hay que modificar las clases Soldado. Comenzando por la clase madre. Aquí pusimos un método abstracto de tipo void para que las clases hijas lo sobreesribiesen a su manera cada una.
Ya no será void, ahora retornará un String
Clase
Soldado //Este método lo han de sobreescribir Heroe y Bestia, porque será distinto para cada uno
public abstract String atacar(Soldado enemigo);
Las siguientes clases hijas, ya no harán System.out. Ahora retornarán los mensajes en un String.
Clase
Heroe @Override
public String atacar(Soldado enemigo) {
// El ataque será el mejor lanzamiento entre dos dados de 0 a 100
Random dado = new Random();
int tirada1 = dado.nextInt(101);
int tirada2 = dado.nextInt(101);
//System.out.println("Primer dado: " + tirada1);
//System.out.println("Segundo dado: " + tirada2);
int maximo = Math.max(tirada1, tirada2);
enemigo.recibirAtaque(maximo);
return "Primer dado: " + tirada1 + "\n" + "Segundo dado: " + tirada2;
}
Clase
Bestia @Override
public String atacar(Soldado enemigo) {
//Único lanzamiento de un dado entre 0 y 90
Random dado = new Random();
int tirada = dado.nextInt(91);
//System.out.println("Resultado del dado es: " + tirada);
enemigo.recibirAtaque(tirada);
return "Resultado del dado es: " + tirada;
}
La clase
Elfos, es la única que tiene una versión propia de este método por aquello de que queríamos un ataque especial cuando luchaba con
Orcos.
También ha de ser "reconducido":
@Override
public String atacar(Soldado enemigo) {
if (enemigo instanceof Orcos) {
//Regla específica cuando un Elfo ataca un Orco
Random dado = new Random();
int tirada1 = dado.nextInt(101);
int tirada2 = dado.nextInt(101);
//System.out.println("Primer dado: " + tirada1);
//System.out.println("Segundo dado: " + tirada2);
int maximo = Math.max(tirada1, tirada2);
//System.out.println("*****¡¡El odio élfico hacia los Orcos incrementa el ataque en 1.5x!!*****");
maximo *= 1.5;
//System.out.println("Valor de ataque resultante: " + maximo);
enemigo.recibirAtaque(maximo);
return "Primer dado: " + tirada1 + "\n" + "Segundo dado: " + tirada2 + "\n"
+ "*****¡¡El odio élfico hacia los Orcos incrementa el ataque en 1.5x!!*****\n"
+ "Valor de ataque resultante: " + maximo;
}
else //Si no es Orco, se aplica la regla general de los Heroes
return super.atacar(enemigo);
}
Vale, tenemos un nuevo panel con area de texto y una renovada clase
Batalla que ahora es un hilo que publicará mensajes en el area de texto.
Ahora nos vamos a la clase principal JFrame, porque tenemos que encajar este panel en la interfaz y escribir el ActionListener para el botón de "Luchar".
Este ActionListener lo que hará será poner en marcha el hilo de batalla.
Resalto en azul los cambios:
public class BatallaRPG extends JFrame {
//Modelo
private Ejercito bestias;
private Ejercito heroes;
//Vista
private PanelCrearSoldado crearHeroes;
private PanelCrearSoldado crearBestias;
private PanelTexto pnTexto;
public BatallaRPG() {
bestias = new Ejercito();
heroes = new Ejercito();
reclutar();
//Este panel referenciará los Heroes
crearHeroes = new PanelCrearSoldado("Heroes", new String[] {"Elfo", "Humano", "Hobbit"}, heroes);
//Este refenciará a la Bestias
crearBestias = new PanelCrearSoldado("Bestias", new String[] {"Trasgo", "Orco"}, bestias);
pnTexto = new PanelTexto();
pnTexto.setAccionBotonLuchar(new AccionBotonLuchar());
JPanel pnSuperior = new JPanel();
pnSuperior.add(crearHeroes);
pnSuperior.add(crearBestias);
JPanel pnInferior = new JPanel();
pnInferior.add(pnTexto);
setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
add(pnSuperior);
add(pnTexto);
setTitle("Batalla RPG");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
private class AccionBotonLuchar implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
BatallaHilo batalla = new BatallaHilo(heroes, bestias, pnTexto);
batalla.start();
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new BatallaRPG();
}
});
}
}
Y con todo esto, ya tenemos un JTextArea que muestra los vaivenes de la batalla.

Hay una pega, el JTextArea no muestra directamente las últimas lineas que se añaden, tenemos que estar haciendo scroll todo el rato para ver los últimos mensajes.
No se si este comportamiento se puede cambiar, lo investigaré.
Pregunta si algo no ha quedado claro, o si sientes que te has perdido..., ya imagino que no esperabas tener que hacer taaaaantos cambios