OK, más avances.
Bueno, en realidad lo he terminado, pero hay un detalle que no ha quedado bien respecto a la velocidad de la bola que luego explicaré.
He de decir que aunque he usado las clases que pedía el enunciado, no he usado ninguno de los métodos que se proponían. El camino que he ido siguiendo me ha llevado a un enfoque distinto.
Dije que la clase
GestorJuego iba a tener el código más complicado porque era donde se iba a calcular colisiones, rebotes, y tal....
Pues bueno, al final el código no solo ha resultado ser menos complicado de lo que pensaba, si no que además al final es la propia clase
Bola quien controla si ha colisionado y decide hacia donde debe rebotar.
Para que la bola pueda hacer esto, le he dado nuevos atributos.
Unos de esos atributos son los limites para X e Y, es decir, tanto para un eje como para el otro, la bola puede ir desde 0 hasta un límite.
Esos límites serán las dimensiones del tablero, pero restándole las dimensiones de la propia bola.
Es decir, si el tablero de ancho mide 500 pixeles, y el ancho de la bola es 10 pixeles, pues el límite será 490. De lo contrario, la bola se saldría del tablero antes de rebotar.
Otros de esos atributos, son las "desviaciones" de X e Y. Estas desviaciones son las que deciden la dirección y ángulo en la que la bola se mueve.
Son valores int que comienzan con valor 0 (bola no se mueve) y luego adoptan valores al azar entre 1 y 5, o bien entre -1 y -5. Es decir, pueden ser positivos o negativos.
Estas desviaciones son las cantidades en las que se incrementan o decrementan las posiciones X e Y de la bola. Así el movimiento se dará en distintos ángulos y direcciones.
Por tanto, tendremos un método
mover() que lo que hará es aplicar estas desviaciones a X e Y para dar sensación de movimiento cada vez que se repinte el tablero.
Si la bola estuviera en reposo, se calcularán unas desviaciones al azar, de lo contrario, se aplicarán las desviaciones que estén vigentes.
Tendremos otro método llamado
detener() para poner a 0 estas desviaciones con lo que la bola pasa a estado de reposo.
Y luego, el método más importante, será el llamado
comprobarColision().
Este método comprueba si la bola ha colisionado con alguno de los cuatro bordes del tablero.
Dependiendo de con cuál borde ha colisionado, calculará nuevas desviaciones de distintas formas.
Por ejemplo, si colisiona con la parte superior, el norte, significa que el eje Y tenía una desviación negativa y tras la colisión obligatoriamente ha de pasar a positiva para ahora dirigirse hacia el sur
El eje X en cambio, ha de conservar la misma dirección, sea este u oeste.
Clase
Bolapublic class Bola extends Ellipse2D {
private int posX;
private int posY;
private double ancho;
private double alto;
//Atributos necesario para calcular colisiones y rebotes
private int desviacionX;
private int desviacionY;
private final int LIMITE_X;
private final int LIMITE_Y;
private Random azar;
public Bola(int x, int y, int w, int h, int limiteX, int limiteY) {
posX = x;
posY = y;
ancho = w;
alto = h;
desviacionX = 0;
desviacionY = 0;
//Los límites se calculan restando las dimensiones de la bola a las dimensiones del tablero
LIMITE_X = (int) (limiteX - ancho);
LIMITE_Y = (int) (limiteY - alto);
azar = new Random();
}
public void mover() {
//Antes de mover, comprobamos si la bola está detenida (desviaciones = 0)
//Si está detenida, calcularemos nuevas desviaciones.
//Si no, la bola se moverá según las desviaciones ya existentes
if (desviacionX == 0 && desviacionY == 0) {
//Calculamos nuevas desviaciones al azar para empezar a mover
desviacionX = azar.nextInt(6);
desviacionY = azar.nextInt(6);
//Elegimos al azar si son negativas o positivos
if (azar.nextBoolean())
desviacionX *= -1;
if (azar.nextBoolean())
desviacionY *= -1;
}
//El movimiento consiste en alterar la posicion según las "desviaciones"
posX += desviacionX;
posY += desviacionY;
}
public void detener() {
desviacionX = 0;
desviacionY = 0;
}
public void comprobarColision() {
//Comprobamos si hemos alcanzado algún limite
//en cuyo caso calcularemos nuevas desviaciones según donde hayamos colisionado
//Colisión en el borde oeste
if (posX <= 0) {
//La desviacion de X ha de ser positiva y mayor que 0
desviacionX = azar.nextInt(5) + 1;
//Desviacion de Y varía, pero ha de mantener la misma dirección actual
if (desviacionY < 0) {
desviacionY = azar.nextInt(5) + 1;
desviacionY *= -1;
}
else
desviacionY = azar.nextInt(5) + 1;
}
//Colisión en el borde este
if (posX >= LIMITE_X) {
//La desviacion de X ha de ser negativa
desviacionX = azar.nextInt(5) + 1;
desviacionX *= -1;
//Desviacion de Y varía, pero ha de mantener la misma dirección actual
if (desviacionY < 0) {
desviacionY = azar.nextInt(5) + 1;
desviacionY *= -1;
}
else
desviacionY = azar.nextInt(5) + 1;
}
//Colisión en el borde norte
if (posY <= 0) {
//La desviacion de Y ha de ser positiva
desviacionY = azar.nextInt(5) + 1;
//Desviacion de X varía, pero ha de mantener la misma dirección actual
if (desviacionX < 0) {
desviacionX = azar.nextInt(5) + 1;
desviacionX *= -1;
}
else
desviacionX = azar.nextInt(5) + 1;
}
//Colisión en el borde sur
if (posY >= LIMITE_Y) {
//La desviacion de Y ha de ser negativa
desviacionY = azar.nextInt(5) + 1;
desviacionY *= -1;
//Desviacion de X varía, pero ha de mantener la misma dirección actual
if (desviacionX < 0) {
desviacionX = azar.nextInt(5) + 1;
desviacionX *= -1;
}
else
desviacionX = azar.nextInt(5) + 1;
}
}
@Override
public Rectangle2D getBounds2D() {
Rectangle2D rect = new Rectangle2D.Double(posX, posY, ancho, alto);
return rect.getBounds2D();
}
@Override
public double getX() {
return posX;
}
@Override
public double getY() {
return posY;
}
@Override
public double getWidth() {
return ancho;
}
@Override
public double getHeight() {
return alto;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public void setFrame(double x, double y, double w, double h) {
}
}
Hemos dicho que clase
Bola ahora conoce sus límites de rebote. Estos límites, se los hacemos llegar por constructor y es la clase
Tablero quien se los proporciona.
Clase
Tableropublic class Tablero extends JPanel {
private int ancho;
private int alto;
private Bola bola;
public Tablero(int ancho, int alto) {
this.ancho = ancho;
this.alto = alto;
bola = new Bola(ancho/2, alto/2,
ancho/30, alto/25, ancho, alto);
setPreferredSize(new Dimension(ancho, alto));
setBorder(BorderFactory.createEtchedBorder(0));
setBackground(Color.WHITE);
}
public int getAncho() {
return ancho;
}
public int getAlto() {
return alto;
}
public Bola getBola() {
return bola;
}
public void reset() {
bola = new Bola(ancho / 2, alto / 2,
ancho / 30, alto / 25, ancho, alto);
repaint();
}
@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D G2D = (Graphics2D)g;
//Limpiamos panel con un rectangulo blanco
G2D.setColor(Color.WHITE);
G2D.fillRect(0, 0, ancho, alto);
//Ahora pintamos bola
G2D.setColor(Color.BLUE);
G2D.fill(bola);
G2D.draw(bola);
}
}
Veamos ahora la clase
GestorJuego.
Como dije al principio, esta clase ha quedado liberada de tener que calcular colisiones y rebotes, ya que es la propia
Bola quien se encarga.
Pero sigue teniendo un papel importante.
Dentro de esta clase he creado un SwingWorker que es quien pondrá en marcha el movimiento de la bola.
Un SwingWorker es como un Thread, es decir, sirve para crear una hilo de ejecución separado del hilo principal. Pero es una versión mejorada y pensada para usar cuando hemos creado una interfaz con Swing, ya que a veces un Thread normal puede interferir con el "despachador de eventos", es decir, el hilo que se encarga de comprobar si en la interfaz Swing pulsamos un botón, o seleccionamos un campo de texto, o redimensionamos una ventana, etc...
Este SwingWorker lo que hará será:
-pedir a la bola que se mueva
-pedir a la bola que compruebe si ha colisionado
-repintar el tablero para que se refleje el cambio de posición de la bola
-pausar la acción unos determinados milisegundos.
Estos milisegundos son los que, en teoría, van a controlar la velocidad de movimiento de la bola. Pero luego veremos que no logra cumplir del todo bien este objetivo.
Todo este proceso se repite mientras un atributo boolean tenga valor true.
Este atributo es el que indica si la bola ha de moverse o ha de pararse.
Otro cambio introducido en esta clase es que ahora recibe una referencia del objeto
PanelBotones.
Al principio dije que los ActionListener para los botones los íbamos a crear aquí en
GestorJuego y con unos getter los haríamos llegar a los botones del tablero.
Pero, para poder controlar de forma óptima que los botones actúen con lógica (por ejemplo no poder incrementar la velocidad si bola no está moviéndose) al final lo mejor es poder activar/desactivar los botones según el estado de la bola.
Y para ello lo más fácil es poder acceder directamente a los botones desde la clase
GestorJuego, así que le he dado una referencia al objeto
PanelBotones y así puedo cambiar estado de los botones y añadirles los ActionListener sin tener que usar getters.
public class GestorJuego {
private Tablero tablero;
private PanelBotones botones;
private long velocidad;
private boolean mover;
private MovimientoBola movimiento;
public GestorJuego(Tablero tablero, PanelBotones botones) {
this.tablero = tablero;
this.botones = botones;
botones.btIniciar.addActionListener(new AccionIniciar());
botones.btAcelerar.addActionListener(new AccionAcelerar());
botones.btFrenar.addActionListener(new AccionFrenar());
botones.btDetenContinuar.addActionListener(new AccionIniciaDetiene());
botones.btReset.addActionListener(new AccionReset());
velocidad = 25;
mover = false;
}
private class MovimientoBola extends SwingWorker {
@Override
protected Object doInBackground() throws Exception {
while(mover) {
tablero.getBola().mover();
tablero.getBola().comprobarColision();
tablero.repaint();
Thread.sleep(velocidad);
}
return null;
}
}
private class AccionReset implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
mover = false;
tablero.getBola().detener();
tablero.reset();
botones.btIniciar.setEnabled(true);
botones.btAcelerar.setEnabled(false);
botones.btFrenar.setEnabled(false);
botones.btDetenContinuar.setEnabled(false);
botones.btReset.setEnabled(false);
}
}
private class AccionIniciar implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (!mover) {
mover = true;
velocidad = 25;
movimiento = new MovimientoBola();
movimiento.execute();
botones.btIniciar.setEnabled(false);
botones.btAcelerar.setEnabled(true);
botones.btFrenar.setEnabled(true);
botones.btDetenContinuar.setEnabled(true);
botones.btReset.setEnabled(true);
}
}
}
private class AccionAcelerar implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (mover) {
if (velocidad > 5)
velocidad -= 5;
}
}
}
private class AccionFrenar implements ActionListener {
public void actionPerformed(ActionEvent e) {
if (mover)
velocidad += 5;
}
}
private class AccionIniciaDetiene implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (mover) {
mover = false;
botones.btAcelerar.setEnabled(false);
botones.btFrenar.setEnabled(false);
}
else {
mover = true;
movimiento = new MovimientoBola();
movimiento.execute();
botones.btAcelerar.setEnabled(true);
botones.btFrenar.setEnabled(true);
}
}
}
}
La clase
PanelBotones apenas cambia, lo único es que en el constructor desactivo todos los botones excepto el botón "Iniciar".
Estos botones luego se irán activando y desactivando según el estado de la bola.
public class PanelBotones extends JPanel {
public JButton btIniciar;
public JButton btAcelerar;
public JButton btFrenar;
public JButton btDetenContinuar;
public JButton btReset;
public PanelBotones(int ancho, int alto) {
btIniciar = new JButton("Iniciar");
btAcelerar = new JButton("Acelerar");
btAcelerar.setEnabled(false);
btFrenar = new JButton("Frenar");
btFrenar.setEnabled(false);
btDetenContinuar = new JButton("Detener/Continuar");
btDetenContinuar.setEnabled(false);
btReset = new JButton("Reset");
btReset.setEnabled(false);
setPreferredSize(new Dimension(ancho, alto));
setLayout(new GridLayout(5, 1));
add(new PanelBoton(btIniciar));
add(new PanelBoton(btAcelerar));
add(new PanelBoton(btFrenar));
add(new PanelBoton(btDetenContinuar));
add(new PanelBoton(btReset));
}
private class PanelBoton extends JPanel {
public PanelBoton(JButton boton) {
add(boton);
}
}
}
Y por último la clase
Juego. Solo cambia que aquí ahora ya no le pasamos a los botones los ActionListener como había planeado al principio.
Ahora a la clase
GestorJuego le pasamos una referencia a
PanelBotones y ya se encargará él de gestionar los botones.
public class Juego extends JFrame {
private Dimension dim;
private Tablero tablero;
private PanelBotones botones;
private GestorJuego gestor;
public Juego() {
dim = Toolkit.getDefaultToolkit().getScreenSize();
int ancho = (int)(dim.getWidth() / 2);
int alto = (int)(dim.getHeight() / 2);
tablero = new Tablero((ancho / 5 * 4), alto);
botones = new PanelBotones(ancho / 5, alto);
gestor = new GestorJuego(tablero, botones);
setLayout(new FlowLayout());
add(tablero);
add(botones);
setTitle("Bola rebota");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setResizable(false);
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new Juego();
}
});
}
}
Bien, pues si ejecutamos el programa veremos que los botones funcionan correctamente y la bola se desplaza y rebota de forma coherente.
Sin embargo, también veremos que
tras algunos rebotes la bola cambia su velocidad sin que hayamos usado los botones "Frenar" y "Acelerar".¿Por qué?
El problema está en la forma en que calculo las "desviaciones".
La intención de que los valores cambien entre 1 y 5 o bien entre -1 y -5, es para que la bola rebote en distintos angulos.
Si ambas desviaciones adoptan valor 3, se moverá en 135º.
Y si una es 3 y la otra -3, pues se moverá en 45º
Y si una es valor 2 y la otra 5, pues el ángulo será otro, etc...
El problema es que esto no solo afecta al ángulo, si no también a la velocidad.
Si ambas desviaciones adoptan valor 1, también se moverá en 135º, pero mucho más lento que cuando ambas valen 3, y ya ni te cuento si ambas valen 5.
Al final, los milisegundos que usamos para "dormir" al SwingWorker, no es lo único que influye en la velocidad de la
Bola.
Ahora mismo no se muy bien como solucionar esto.
Quizás hacer que la velocidad se decida según un valor que una de las dos "desviaciones" tendrá que utilizar obligatoriamente, y según el rebote, esa "obligación" se transmita, o no, a la otra "desviación"
Así, una "desviación" decidirá la velocidad y la otra el ángulo..., no se, no estoy seguro.
Habrá que probarlo, pero hoy ya no, la cama me llama.
Mañana será otro día...