Autor Tema: ejercicio java juego bola que se mueve y rebota en bordes tablero  (Leído 4030 veces)

manuromero73

  • Sin experiencia
  • *
  • APR2.COM
  • Mensajes: 40
    • Ver Perfil
Hola me gustaría recabar ayuda con el ejercicico siguiente en Java.

Debe iniciar un programa con una ventana gráfica que ocupe aproximadamente la mitad de la pantalla del usuario, centrada sobre la misma. La ventana tendrá un panel izquierdo con borde marcado donde tendrá lugar la animación gráfica, que ocupará aproximadamente 4/5 partes de la ventana gráfica. Y un panel derecho con cinco botones: "Iniciar", "Acelerar", "Frenar", "Detener/Continuar" y "Resetear" que será 1/5 parte aproximadamente de la ventana gráfica.

Inicialmente debe aparecer un elemento gráfico circular ("la bola") centrado sobre el panel de animación o tablero. El comportamiento debe ser el siguiente:

1. Si se pulsa "Iniciar", la bola deberá moverse en una dirección aleatoria con una velocidad inicial predeterminada y continuar hasta alcanzar el borde del panel. En ese momento, deberá rebotar en un ángulo aleatorio de entre 30 y 150 grados sexagesimales, continuando su movimiento hasta chocar con otro borde y así sucesivamente.
2. Si se pulsa "Acelerar" la bola incrementa su velocidad. Sucesivas pulsaciones generarán nuevos incrementos de velocidad hasta el valor tope que se determine.
3. Si se pulsa "Frenar" el comportamiento es análogo al de acelerar, pero en este caso disminuyendo la velocidad hasta el valor límite que se determine.
4. Si se pulsa "Detener/Continuar" la bola quedará quieta (detener) o reanudará su movimiento si estaba quieta (continuar)
5. Si se pulsa "Resetear" se vuelve al estado inicial (bola centrada sobre el panel de animación y a la espera de que se pulse un botón).

Las pulsaciones sin sentido no generarán ninguna respuesta. Por ejemplo si la bola no ha iniciado su movimiento o si está en pausa, al pulsar en acelerar no habrá ninguna respuesta puesto que no se puede acelerar. Si se pulsa detener/continuar antes de "Iniciar" no habrá respuesta, etc.

Deberá realizar un diseño orientado a objetos con clases como las siguientes:

Clase Bola: se encargará de representar y dibujar la bola y de permitir el movimiento de esta (métodos posicionarInicialmente, avanzar (angulo), rebotar(angulo), obtenerPosicion)

Clase Tablero: se encargará de representar y dibujar el tablero (métodos obtenerCoordenadasOrigen, obtenerCoordenadasOpuestasOrigen)

Clase PanelBotones: se encargará de contener los botones e invocar los métodos que correspondan con cada pulsación.

Clase GestorJuego: se encargará de posicionar la bola sobre el tablero en su posición inicial y de hacer que la bola se mueva y rebote cuando deba hacerlo, de que la bola acelere, frene, se detenga/continúe o se resetee.

Clase Juego: contendrá el método main desde donde se generará el tablero, la bola y resto de elementos necesarios. El tablero y bola se pasarán a un GestorJuego que contendrá los métodos operativos de reacción a la pulsación de botones.

El diseño podrá modificarse según lo estime oportuno en cualquiera de sus aspectos, añadiendo o eliminando clases si así se desea, pero respetando:

a) Que exista orientación a objetos (distintas clases con distintas responsabilidades)
b) Funcionalidad: un elemento circular que se mueve por la pantalla y rebota en los bordes.

Kabuto

  • Moderador Global
  • Experto
  • *******
  • Mensajes: 989
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #1 en: 13 de Abril 2023, 13:51 »
Uuff.. interesante, pero complicado... ¿no?
La verdad es que no se muy bien como hacer que la bolita rebote en distintos ángulos y tal..
Moverla, se puede hacer con un SwingWorker o un Thread. Que detecte bordes y rebote en un ángulo u otro, ahí es donde está lo complicado.

Pero bueno, de momento se puede ir creando el tablero y los botones, para tener algo sobre lo que trabajar y experimentar.

No dispongo de mucho tiempo  :( pero he podido hacer la interfaz básica.
Los botones no están bonitos, pero bueno, ocuparnos de su estética sería lo último

Dejo esto por aquí, que como digo ahora mismo no tiene ninguna funcionalidad, pero es una interfaz sobre la que ir trabajando.
Puedes usarla o si tú ya has creado otra, compártela por aquí también y así vamos viendo distintas soluciones.

Clase Tablero
Código: [Seleccionar]
public class Tablero extends JPanel {

public Tablero(int ancho, int alto) {
setPreferredSize(new Dimension(ancho, alto));
setBorder(BorderFactory.createEtchedBorder(0));
setBackground(Color.WHITE);
}

}

Clase PanelBotones
Código: [Seleccionar]
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");
btFrenar = new JButton("Frenar");
btDetenContinuar = new JButton("Detener/Continuar");
btReset = new JButton("Reset");
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 clase Juego. Aquí es donde obtenemos las dimensiones de la pantalla para así decirle a los otros dos paneles cuáles han de ser sus dimensiones.
Código: [Seleccionar]
public class Juego extends JFrame {

private Dimension dim;
private Tablero tablero;
private PanelBotones botones;

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);
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();
}
});
}
}


De momento no he podido hacer más.
Iré comentando por aquí cuando pueda seguir.

Un saludo.
NO respondo dudas por mensaje privado
Publicando vuestras dudas en el foro público conseguimos:
- Que más gente aporte respuestas mejores o complementarias.
- Que otras personas puedan aprender de vuestras dudas.

Mejor en PÚBLICO que en privado. Gracias

Kabuto

  • Moderador Global
  • Experto
  • *******
  • Mensajes: 989
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #2 en: 14 de Abril 2023, 12:51 »
Veamos, he podido avanzar un poco más, pero la verdad no se si voy por buen camino. Da igual, ye veremos donde acaba esto ...  :P

La Bola no nos dicen como crearla. Podría ser una imagen insertada en un JLabel, esto ya lo he hecho otras veces.

Pero voy a intentarlo con un Shape, en este caso, con una Ellipse2D

Así que la clase Bola va a heredar de Ellipse2D.
De momento va a tener 4 atributos: posición X, posición Y, ancho y alto. Estos atributos se calcularán según las dimensiones del Tablero

Al heredar de Ellipse2D, nos obliga a sobre escribir varios métodos heredados.
Uno de ellos, el getBounds2D()... no estoy muy seguro de si lo que le he escrito es válido o no. Ya se verá, de todos modos puede que ni siquiera lo necesitemos.
Lo mismo con el setFrame(), ese incluso lo he dejado vacío.

De momento, esta es la clase Bola. Luego ya se irá viendo si hay que añadirle más cosas, corregirla o tirarla a la papelera xD
Código: [Seleccionar]
public class Bola extends Ellipse2D {

private int posX;
private int posY;
private double ancho;
private double alto;

public Bola(int x, int y, int w, int h) {
posX = x;
posY = y;
ancho = w;
alto = h;
}

@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) {

}

}

He modificado la clase Tablero, añadiendo los atributos ancho y alto y también un atributo Bola
Mi intención era inicializar la Bola por separado en la clase GestorJuego (que veremos después) y luego pintarla en Tablero.
Pero no he conseguido/sabido hacer que el tablero la pinte.

Pensé que con el método getGraphics() de Tablero(que es un JPanel) obtendría su contexto gráfico para poder pintar, pero siempre me retornaba valor null  ???

Así que como no he podido pintar desde FUERA del Tablero, voy a tener que pintar desde DENTRO

Así que la Bola se va a inicializar dentro de Tablero y lo que haremos será poder retornarla con un método get para que ya luego la clase GestorJuego pueda interactuar con ella.

Para pintar desde DENTRO, lo que hacemos es sobre escribir el método paint() que Tablero hereda por ser hija de JPanel y ahí pintaremos la Bola.
En teoría, para hacer que se "mueva" lo que haremos será cambiar sus atributos de posicion X e Y  para luego "repintar" el tablero y que la bola se desplace.

Pero antes de "mover" la bola, hay que borrar el tablero, ya que de lo contrario lo que haremos será pintar muchas bolas.
Por eso, antes de pintar la bola, lo que hago es pintar un rectángulo de color blanco que abarca todo el Tablero. Y ya después pintamos la bola.

Le he puesto también un método para "resetear" el tablero, es decir, volver a colocar la bola en su posición inicial.

Código: [Seleccionar]
public 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);
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);
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 la clase GestorJuego.
De momento apenas tiene código, pero va a ser la clase más difícil porque aquí es donde habrá que poner la bola en movimiento, calcular si colisiona, cambiar el ángulo de su dirección....
Y de momento no tengo hecho nada de eso.

Por ahora solo tiene el constructor, que recibe una referencia del Tablero para interactuar con él.
La Bola está dentro del Tablero, así que de hecho podrá interactuar con ambos.

Tiene un método, donde construimos y retornamos un ActionListener para resetear el tablero.
Desde aquí construiremos todos los ActionListeners para las distintas funciones y los retornamos para poder hacerselos llegar a los botones de la clase PanelBotones

Código: [Seleccionar]
public class GestorJuego {

private Tablero tablero;

public GestorJuego(Tablero tablero) {
this.tablero = tablero;
}

public ActionListener getAccionReset() {
return new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
tablero.reset();
}
};
}

}

Por último, en la clase principal Juego. Añado el nuevo atributo GestorJuego y en el constructor vemos cómo le hago llegar al botón Reset la acción para que pueda resetear el tablero.

Código: [Seleccionar]
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.btReset.addActionListener(gestor.getAccionReset());

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();
}
});
}
}

Si ejecutamos este código, veremos ahora una bolita azul en el centro del tablero.
De momento está inmovil, a ver cuando puedo ponerme a pensar lo difícil, hacer que se mueva y colisione correctamente

Un saludo.
« Última modificación: 14 de Abril 2023, 12:55 por Kabuto »
NO respondo dudas por mensaje privado
Publicando vuestras dudas en el foro público conseguimos:
- Que más gente aporte respuestas mejores o complementarias.
- Que otras personas puedan aprender de vuestras dudas.

Mejor en PÚBLICO que en privado. Gracias

Kabuto

  • Moderador Global
  • Experto
  • *******
  • Mensajes: 989
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #3 en: 16 de Abril 2023, 02:00 »
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 Bola
Código: [Seleccionar]
public 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 Tablero
Código: [Seleccionar]
public 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.
Código: [Seleccionar]
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.
Código: [Seleccionar]
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.
Código: [Seleccionar]
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...  ::)
« Última modificación: 16 de Abril 2023, 02:05 por Kabuto »
NO respondo dudas por mensaje privado
Publicando vuestras dudas en el foro público conseguimos:
- Que más gente aporte respuestas mejores o complementarias.
- Que otras personas puedan aprender de vuestras dudas.

Mejor en PÚBLICO que en privado. Gracias

Kabuto

  • Moderador Global
  • Experto
  • *******
  • Mensajes: 989
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #4 en: 17 de Abril 2023, 00:24 »
Pues efectivamente, la solución ha sido establecer un valor fijo para una de las "desviaciones".
Así una desviación decide el ángulo de rebote y la otra la velocidad en píxeles, que será constante.

Lo que sí podremos seguir controlando desde GestorJuego son los milisegundos que transcurren entre cada repintado del tablero, variando así la sensación de velocidad.

En la clase Bola lo que he hecho es añadir una constante y se irá aplicando a una u otra de las dos desviaciones, según donde ocurra la colisión y cambiando a positivo o negativo según corresponda.

Código: [Seleccionar]
public 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;

final int VELOCIDAD;

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();
VELOCIDAD = 5; //Una de las desviaciones ha de tener siempre ese valor
}

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) {
//Una desviación se calcula al azar, para decidir el ángulo
//Otra usará el valor de velocidad
desviacionX = VELOCIDAD;
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) {
//A la desviación de X le damos el valor de velocidad
desviacionX = VELOCIDAD;
//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 = VELOCIDAD * -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 será la velocidad
desviacionY = VELOCIDAD;
//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 = VELOCIDAD * -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) {

}

}

Y eso es todo, ahora ya estaría funcionando el programa.

Lo último que he cambiado también, es el aspecto de los botones.
Ya los he centrado mejor y ampliado la fuente con un valor relativo según el ancho del panel.
Código: [Seleccionar]
public class PanelBotones extends JPanel {

public MiBoton btIniciar;
public MiBoton btAcelerar;
public MiBoton btFrenar;
public MiBoton btDetenContinuar;
public MiBoton btReset;

public PanelBotones(int ancho, int alto) {
btIniciar = new MiBoton("Iniciar", ancho);
btAcelerar = new MiBoton("Acelerar", ancho);
btAcelerar.setEnabled(false);
btFrenar = new MiBoton("Frenar", ancho);
btFrenar.setEnabled(false);
btDetenContinuar = new MiBoton("Detener/Continuar", ancho);
btDetenContinuar.setEnabled(false);
btReset = new MiBoton("Reset", ancho);
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) {
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
boton.setAlignmentX(Component.CENTER_ALIGNMENT);
add(Box.createVerticalGlue());
add(boton);
add(Box.createVerticalGlue());
}
}

public class MiBoton extends JButton {
public MiBoton(String txt, int ancho) {
super(txt);
//Tamaño fuente se calcula según el ancho del panel
float sizeFuente = getFont().getSize() * ancho * 0.7f /100;
setFont(getFont().deriveFont(sizeFuente));
}
}

}


En este vídeo puede verse el programa en funcionamiento:
https://www.youtube.com/watch?v=H59d522WDxM

Además adjunto un zip con el código fuente final, por si alguien se ha perdido con tantos cambios en la clases.


Ha sido interesante este ejercicio, no estaba seguro de si me iba a salir o no.
Espero que se entienda toda la lógica que he seguido. Preguntad lo que haga falta.

Y sería maravilloso si alguien propusiera una solución distinta. Seguro que hay alguna forma más eficiente y correcta de hacer esto.

Un saludo.
NO respondo dudas por mensaje privado
Publicando vuestras dudas en el foro público conseguimos:
- Que más gente aporte respuestas mejores o complementarias.
- Que otras personas puedan aprender de vuestras dudas.

Mejor en PÚBLICO que en privado. Gracias

manuromero73

  • Sin experiencia
  • *
  • APR2.COM
  • Mensajes: 40
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #5 en: 30 de Abril 2023, 11:50 »
Hola Kabuto una vez más más que gracias por el codigo y por la explicacion. Creo que es dificil o casi imposible encontrar respuestas con tanta dedicacion y explicaciones tan claras como las que tú haces. Como en otras ocasiones trato de desmenuzar todo el codigo para entenderlo todo, de ahi mis preguntas de aprendizaje empiezan por qué introduces los cinco botones dentro de JPanel, que a su vez se introducen en el JPanel PanelBotones en lugar de colocar los botones directamente sobre el JPanel PanelBotones. Por otro lado por qué usar pack() y por qué usar setLocationRelativeTo(null). Por otro lado lo que yo entiendo es que SwingUtilities.invokeLater(new Runnable() lo que hace es crear un hilo de ejecucion, no sé si es así y también me gustaria preguntar qué pasa si no creas un hilo de ejecución.

Kabuto

  • Moderador Global
  • Experto
  • *******
  • Mensajes: 989
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #6 en: 01 de Mayo 2023, 00:03 »
por qué introduces los cinco botones dentro de JPanel, que a su vez se introducen en el JPanel PanelBotones en lugar de colocar los botones directamente sobre el JPanel PanelBotones.
Es para maquetar un poco mejor los botones.
En ese panel, estoy colocando los elementos con un GridLayout.
GridLayout tiene un característica de doble filo, hace que todos los componentes que contiene se maximicen para ocupar el máximo espacio posible del contenedor que está maquetando.

Si colocamos directamente los botones, estos crecerán hasta llenar el panel por completo.
Puedes comprobarlo, comenta las líneas originales y pon líneas donde se añadan directamente los botones.
Citar
   public PanelBotones(int ancho, int alto) {
      btIniciar = new MiBoton("Iniciar", ancho);
      btAcelerar = new MiBoton("Acelerar", ancho);
      btAcelerar.setEnabled(false);
      btFrenar = new MiBoton("Frenar", ancho);
      btFrenar.setEnabled(false);
      btDetenContinuar = new MiBoton("Detener/Continuar", ancho);
      btDetenContinuar.setEnabled(false);
      btReset = new MiBoton("Reset", ancho);
      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));*/

      add(btIniciar);
      add(btAcelerar);
      add(btFrenar);
      add(btDetenContinuar);
      add(btReset);

   }
Verás que el aspecto de los botones cambia:


Y bueno, la verdad es que en este caso tampoco quedan mal así tan grandes.
Pero la mayoría de las veces vamos a querer que tengan un tamaño "normal". La forma de conseguirlo es "envolverlos" con un JPanel.
De esta manera, lo que se amplia es el JPanel, no los botones.

Si usamos un JPanel tal como viene por defecto, puede servir. Pero entonces, no solo supone demasiado código porque hay que declarar un JPanel para cada botón:
Citar
      setPreferredSize(new Dimension(ancho, alto));
      setLayout(new GridLayout(5, 1));
      JPanel pn1 = new JPanel();
      pn1.add(btIniciar);
      JPanel pn2 = new JPanel();
      pn2.add(btAcelerar);
      JPanel pn3 = new JPanel();
      pn3.add(btFrenar);
      JPanel pn4 = new JPanel();
      pn4.add(btDetenContinuar);
      JPanel pn5 = new JPanel();
      pn5.add(btReset);
      add(pn1);
      add(pn2);
      add(pn3);
      add(pn4);
      add(pn5);

Si no que además los botones se van a colocar en la parte superior del panel que le está envolviendo. Porque por defecto los JPanel se maquetan con FlowLayout.
Eso hará que el primer botón se vea muy cerca del marco superior y el último botón en cambio, se verá muy alejado del marco inferior.


Para ahorrar código y que además los botones queden centrados en los paneles que les envuelven, podemos crear una clase hija de JPanel y establecer ahí una maquetación más adecuada:
Citar
      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) {
         setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
         boton.setAlignmentX(Component.CENTER_ALIGNMENT);
         add(Box.createVerticalGlue());
         add(boton);
         add(Box.createVerticalGlue());
      }

   }

En esa clase estamos diciendo que el JPanel se maquete con BoxLayout en vertical, es decir, que los elementos se vayan colocando de arriba a abajo.
Con setAligmentX le estamos diciendo que el botón ha de estar en el centro respecto al eje horizontal, de lo contrario se pegará a la izquierda del panel.
No hay forma de decirle también se coloque en el centro respecto al eje vertical, por narices se colocará arriba del panel.
Pero eso lo solucionamos añadiendo dos "cajas" con Box.createVerticalGlue().
Estas "cajas" ocuparán todo el espacio que puedan, dejando lo necesario para que el botón se coloque entre ellas. Así el botón quedará perfectamente centrado dentro de su panel.

Y en la interfaz veremos que los botones están centrados, y que hay el mismo espacio entre el marco superior y el marco inferior.

Cita de: manuromero73
Por otro lado por qué usar pack() y por qué usar setLocationRelativeTo(null).

pack() lo que hace es dar automáticamente al JFrame las dimensiones necesarias para respetar los preferredSize() de todos los componentes que componen la interfaz, ya sean paneles, botones, campos, etc..
Digamos que sustituye al método setSize() con el que le diríamos al JFrame exactamente que tamaño queremos que tenga. Habrá veces que nos interese darle unas dimensiones concretas y exactas.
Pero en este caso, como ya le hemos dicho a cada panel sus "tamaños preferidos", podemos usar pack() y el respetará y sumará esos "tamaños preferidos" para que el JFrame pueda contenerlos.

setLocationRelativeTo() es para indicarle en que posición queremos que aparezca el marco.
Podríamos decirle por ejemplo que se colocara encima, debajo, etc... de otro marco que ya estuviera en pantalla, si fuera el caso.
Pero si le damos valor null como argumento, entonces lo que hace es colocarlo en el centro de la pantalla, que es lo que nos va a interesar la mayoría de las veces.

Cita de: manuromero73
Por otro lado lo que yo entiendo es que SwingUtilities.invokeLater(new Runnable() lo que hace es crear un hilo de ejecucion, no sé si es así y también me gustaria preguntar qué pasa si no creas un hilo de ejecución.

Sí, más o menos.
Si las aplicaciones Swing las ejecutamos en el hilo principal del programa, puede ocurrir que si ese hilo se mantiene ocupado por otra tarea, entonces la interfaz se quede como bloqueada y sin responder durante el tiempo que esa otra tarea está ocupando el hilo principal.

Por ejemplo, a lo mejor el programa está haciendo una conexión online para descargar algo de una base de datos, o bien está haciendo unos cálculos con enormes matrices de tipo double que requiere muchos ciclos de computación.
Y mientras esto ocurre el usuario notará que los botones tardan en hacer caso, que el texto que está tecleando tarda mucho en aparecer en pantalla y que la interfaz en general no está respondiendo.

Para evitar eso, se suele trasladar la aplicación Swing a un segundo hilo que de hecho se crea automáticamente (no lo creamos nosotros), al hilo del Event Dispatcher(Despachador de Eventos). Este es el hilo que está atento a cuando ocurre un evento: un click de ratón, un tecla pulsada, un desplegable se abre para mostrar una lista de elementos,...
Este hilo en realidad tiene poca cosa que hacer, así que al mismo tiempo que "escucha" posibles eventos también puede encargarse de actualizar los cambios que ocurren en la interfaz. Ya que de hecho, normalmente la interfaz solo cambia cuando previamente ocurre un evento. Así que se encargará de escuchar el evento y acto seguido actualizar la interfaz según el evento que haya ocurrido.

Por tanto, es costumbre lanzar la aplicación Swing como un Runnable() con esta instrucción para que use el hilo del Event Dispatcher y no dependa el hilo principal que puede verse "ahogado" por otras tareas.
Código: [Seleccionar]
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new Juego();
}
});

En realidad, la mayoría de las veces el programa va a funcionar igual aunque no hagamos esto.
Porque de hecho, los programadores ya saben qué tareas pueden ahogar el hilo principal y normalmente las lanzaran en otros hilos.

En este programa por ejemplo, lo de mover la bola es una tarea algo pesada, porque lo que estamos haciendo en realidad es repintar un panel varias veces por segundo.
Este proceso lo estamos realizando dentro de un SwingWorker que como ya dije, lo que hace es crear un nuevo hilo para esta tarea.
Por tanto, nuestro programa consta de tres hilos(Threads): el principal, el del Event Dispatcher y el que estamos creando nosotros con el SwingWorker.

Así que podríamos ejecutar la interfaz tranquilamente en el hilo principal sin tener que derivarla al hilo del Event Dispatcher, porque el trabajo "duro" ya lo hace el SwingWorker.

Pero bueno, ya casi que a modo de "buenas costumbres", siempre mejor derivar la interfaz al hilo del Event Dispatcher, incluso aunque sea una aplicación sencilla o las tareas pesadas estén controladas con otros hilos.

Un saludo.
NO respondo dudas por mensaje privado
Publicando vuestras dudas en el foro público conseguimos:
- Que más gente aporte respuestas mejores o complementarias.
- Que otras personas puedan aprender de vuestras dudas.

Mejor en PÚBLICO que en privado. Gracias

manuromero73

  • Sin experiencia
  • *
  • APR2.COM
  • Mensajes: 40
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #7 en: 16 de Septiembre 2023, 18:19 »
Vuelvo mas mar de dudas ¿Por que se incluye @SuppressWarnings("serial") en el codigo?,y el método paint(Graphics g) entiendo que pinta un elemento grafico como un JPanel, ¿la g representa el interior del elemento grafico para poder manipularlo? Y esto que entiendo es una conversión de tipos: Graphics2D G2D = (Graphics2D)g;  ¿Por qué se hace así en lugr de manipular directamente el objeto g? Gracias


Kabuto

  • Moderador Global
  • Experto
  • *******
  • Mensajes: 989
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #8 en: 16 de Septiembre 2023, 23:33 »
Citar
¿Por que se incluye @SuppressWarnings("serial") en el codigo?
Esto está relacionado con la "serialización" de objetos.
Algunas clases (por ejemplo la clase JPanel) implementan la interfaz Serializable, que es la que permite que un objeto pueda ser transformado en una "serie de bytes" y de ese modo transmitirlo a otro dispositivo o también guardarlo en disco.

Luego ese objeto, al recibirlo el otro dispositivo o al leerlo del disco donde se guardó, esos bytes se transforman de nuevo en un objeto, conservando los valores de sus atributos tal cuál estaban cuando se transmitió/guardó.

Es decir, que si yo tengo una clase Persona que implementa la interfaz serializable, y creo el objeto Persona persona01, pues yo podría transformalo en bytes, y enviártelo a ti a través de una red o un soporte físico y tu luego recuperarlo desde tu programa, siempre que también tengas el código de esa clase Persona

Vale, ¿pero que pasaría si mi clase Persona tiene un código distinto de tu clase Persona? Esto puede ocurrir si por ejemplo ha habido una actualización en el programa y a Persona se le ha añadido un nuevo atributo, o un nuevo método, que no existe en el código de tu clase porque todavía no has actualizado tu programa.

Pues ya tenemos un problema, tu clase no va a saber interpretar bien mi objeto y el programa se puede bloquear, o sufrir pérdida de datos, o quien sabe...

Para poder controlar este problema, las clases que implementan esta capacidad incluyen un atributo llamado serialVersionUID al que se recomienda al programador que le de un valor numérico para indicar un número de versión.
Cada vez que se haga un cambio importante en el código, se ha de modificar este número de versión.
De esta manera, el programa antes de intentar recuperar un objeto que ya ha sido serializado, puede comprobar el número de versión con el que fue serializado. Si no coincide con el número de versión de su propio código, entonces no lo recuperará y se podrá evitar cuelgues o pérdidas de datos.

Entonces, todo esto está muy bien si vamos a serializar objetos. Pero si no lo vamos a hacer, como es el caso de este programa, entonces esto del número de serie nos da igual y no hace falta ponerlo.
Sin embargo, el compilador o IDE que estemos usando puede lanzar un warning si no lo ponemos, ya que considera que es algo importante de lo que conviene avisar al programador.

Una forma de pedirle al compilador de que no nos toque las narices con esta vaina, es con la etiqueta @SuppressWarnings("serial"). Ahí le decimos que NO nos avise por la ausencia de número de versión.

Citar
y el método paint(Graphics g) entiendo que pinta un elemento grafico como un JPanel, ¿la g representa el interior del elemento grafico para poder manipularlo? Y esto que entiendo es una conversión de tipos: Graphics2D G2D = (Graphics2D)g;  ¿Por qué se hace así en lugr de manipular directamente el objeto g?

Esto tiene que ver con la herencia de clases y el polimorfismo.
Y sobre todo de como queríamos gestionar la Bola


El objeto g es de la superclase Graphics, que ya de por sí posee varios métodos y funcionalidades para dibujar elementos gráficos en pantalla.
Para dibujar una bola, podríamos haber usado su método:
Código: [Seleccionar]
drawOval(int x, int y, int width, int height)Pero fíjate que cada vez que lo llamamos, tenemos que darle coordenadas y dimensiones.
Esas coordenadas x e y, dependen de la posición previa, de la "desviación" que calculábamos de forma aleatoria, de la velocidad actual de la "bola"...

Todo esto se podría haber calculado ahí en la clase Tablero. Pero es más cómodo y elegante separar esos cálculos y escribirlos en una clase que represente a una Bola, ya que esa es la gracia de la programación orientada a objetos, que podemos representar cualquier entidad que se nos ocurra.

Nuestra Bola es en realidad un objeto de la clase Ellipse2D y para que se dibuje se la tenemos que pasar al objeto que se encarga de pintar el Tablero, es decir, a Graphics g
Pero la clase Graphics es demasiado genérica y no sabe qué es una Ellipse2D. No podemos hacérsela llegar a ninguno de sus métodos.

Tenemos que subir un nivel en su línea hereditaria y trabajar con Graphics2D. Esta clase hija es menos genérica, esta más especializada en trabajar con figuras 2D y por supuesto sabe lo que es una Ellipse2D y cómo dibujarla en pantalla.

Por eso al objeto g, le pedimos que cambie de tipo y pase a comportarse como un Graphics2D.
Gracias al polimorfismo, el objeto g puede comportarse como un Graphics o como un Graphics2D, según nos convenga.
NO respondo dudas por mensaje privado
Publicando vuestras dudas en el foro público conseguimos:
- Que más gente aporte respuestas mejores o complementarias.
- Que otras personas puedan aprender de vuestras dudas.

Mejor en PÚBLICO que en privado. Gracias

manuromero73

  • Sin experiencia
  • *
  • APR2.COM
  • Mensajes: 40
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #9 en: 17 de Septiembre 2023, 20:30 »
Creo haber entendido todo excepto de donde sale el objeto g ¿la g en la clase Tablero representa el interior del elemento grafico Jpanel para poder manipularlo? No veo ningún lugar donde se invoque paint(objetoPasadoAlMetodo) y no se si estoy equivocado pero ese metodo parace que no se invoca. Si no se invoca ¿cuando se ejecuta y por qué?
Por otro lado, ¿cuando creas un objeto Tablero con tablero = new Tablero((ancho / 5 * 4), alto); este objeto al crearlo no tiene representación grafica en la pantalla? Me refiero a que si estando creado el objeto, este no es visible y pasa a ser visible cuando se invoca una instrucción concreta. ¿En qué momento concreto? Entiendo que con repaint se redibuja el elemento grafico con los nuevos datos. Pero el elemento inicial en que se dibuja el elemento Tablero no lo tengo claro, gracias.

Kabuto

  • Moderador Global
  • Experto
  • *******
  • Mensajes: 989
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #10 en: 18 de Septiembre 2023, 12:07 »
La creación del objeto g, las invocaciones a paint() y otras miles de instrucciones más, se encarga de hacerlo las librerías Swing que son las que dibujan toda la interfaz gráfica.

Esas instrucciones no las escribimos nosotros, de lo contrario, significaría que cada para programa tendríamos que escribir también todas las clases que intervienen en crear una interfaz. Todo eso ya está escrito en las librerías Swing, nosotros solo tenemos que elegir que elementos queremos mostrar, donde se posicionan, que aspecto van a tener y cómo han de comportarse.


Todo eso, empieza a ponerse en marcha cuando invocamos el JFrame principal.

Es decir, en la clase Juego, en su método main() lo "único" que hacemos es instanciar un objeto de Juego

Citar
   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         @Override
         public void run() {
            new Juego();   
         }
      });
   }

Pero esa simple instrucción, pone en marcha todas las otras miles de instrucciones que intervienen en el programa.
Ya que Juego es un JFrame, y en su constructor ya le hemos dicho que muestre dos paneles, uno es el Tablero y otro contiene los botones.
Así que el JFrame ya se encarga de activar las librerías Swing y crea todos los objetos Graphics que necesite, y llama a paint() cuando hace falta
, e invoca todo lo que haya que invocar sin que nosotros lo veamos.

Por ejemplo, este es el código de la clase JPanel, ahí puedes ver que se llaman a métodos, a constantes, a variables y otras clases que nosotros no hemos escrito y que no tenemos ni puñetera idea de qué hacen ni para que sirven.

Código: [Seleccionar]
/*
 * Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.swing;

import java.awt.FlowLayout;
import java.awt.LayoutManager;
import java.beans.BeanProperty;
import java.beans.JavaBean;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serial;

import javax.accessibility.Accessible;
import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleRole;
import javax.swing.plaf.PanelUI;

/**
 * <code>JPanel</code> is a generic lightweight container.
 * For examples and task-oriented documentation for JPanel, see
 * <a
 href="https://docs.oracle.com/javase/tutorial/uiswing/components/panel.html">How to Use Panels</a>,
 * a section in <em>The Java Tutorial</em>.
 * <p>
 * <strong>Warning:</strong> Swing is not thread safe. For more
 * information see <a
 * href="package-summary.html#threading">Swing's Threading
 * Policy</a>.
 * <p>
 * <strong>Warning:</strong>
 * Serialized objects of this class will not be compatible with
 * future Swing releases. The current serialization support is
 * appropriate for short term storage or RMI between applications running
 * the same version of Swing.  As of 1.4, support for long term storage
 * of all JavaBeans
 * has been added to the <code>java.beans</code> package.
 * Please see {@link java.beans.XMLEncoder}.
 *
 * @author Arnaud Weber
 * @author Steve Wilson
 * @since 1.2
 */
@JavaBean(defaultProperty = "UI", description = "A generic lightweight container.")
@SuppressWarnings("serial") // Same-version serialization only
public class JPanel extends JComponent implements Accessible
{
    /**
     * @see #getUIClassID
     * @see #readObject
     */
    private static final String uiClassID = "PanelUI";

    /**
     * Creates a new JPanel with the specified layout manager and buffering
     * strategy.
     *
     * @param layout  the LayoutManager to use
     * @param isDoubleBuffered  a boolean, true for double-buffering, which
     *        uses additional memory space to achieve fast, flicker-free
     *        updates
     */
    public JPanel(LayoutManager layout, boolean isDoubleBuffered) {
        setLayout(layout);
        setDoubleBuffered(isDoubleBuffered);
        setUIProperty("opaque", Boolean.TRUE);
        updateUI();
    }

    /**
     * Create a new buffered JPanel with the specified layout manager
     *
     * @param layout  the LayoutManager to use
     */
    public JPanel(LayoutManager layout) {
        this(layout, true);
    }

    /**
     * Creates a new <code>JPanel</code> with <code>FlowLayout</code>
     * and the specified buffering strategy.
     * If <code>isDoubleBuffered</code> is true, the <code>JPanel</code>
     * will use a double buffer.
     *
     * @param isDoubleBuffered  a boolean, true for double-buffering, which
     *        uses additional memory space to achieve fast, flicker-free
     *        updates
     */
    public JPanel(boolean isDoubleBuffered) {
        this(new FlowLayout(), isDoubleBuffered);
    }

    /**
     * Creates a new <code>JPanel</code> with a double buffer
     * and a flow layout.
     */
    public JPanel() {
        this(true);
    }

    /**
     * Resets the UI property with a value from the current look and feel.
     *
     * @see JComponent#updateUI
     */
    public void updateUI() {
        setUI((PanelUI)UIManager.getUI(this));
    }

    /**
     * Returns the look and feel (L&amp;F) object that renders this component.
     *
     * @return the PanelUI object that renders this component
     * @since 1.4
     */
    public PanelUI getUI() {
        return (PanelUI)ui;
    }


    /**
     * Sets the look and feel (L&amp;F) object that renders this component.
     *
     * @param ui  the PanelUI L&amp;F object
     * @see UIDefaults#getUI
     * @since 1.4
     */
    @BeanProperty(hidden = true, visualUpdate = true, description
            = "The UI object that implements the Component's LookAndFeel.")
    public void setUI(PanelUI ui) {
        super.setUI(ui);
    }

    /**
     * Returns a string that specifies the name of the L&amp;F class
     * that renders this component.
     *
     * @return the string "PanelUI"
     * @see JComponent#getUIClassID
     * @see UIDefaults#getUI
     */
    @BeanProperty(bound = false, expert = true, description
            = "A string that specifies the name of the L&F class.")
    public String getUIClassID() {
        return uiClassID;
    }


    /**
     * See readObject() and writeObject() in JComponent for more
     * information about serialization in Swing.
     */
    @Serial
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        if (getUIClassID().equals(uiClassID)) {
            byte count = JComponent.getWriteObjCounter(this);
            JComponent.setWriteObjCounter(this, --count);
            if (count == 0 && ui != null) {
                ui.installUI(this);
            }
        }
    }


    /**
     * Returns a string representation of this JPanel. This method
     * is intended to be used only for debugging purposes, and the
     * content and format of the returned string may vary between
     * implementations. The returned string may be empty but may not
     * be <code>null</code>.
     *
     * @return  a string representation of this JPanel.
     */
    protected String paramString() {
        return super.paramString();
    }

/////////////////
// Accessibility support
////////////////

    /**
     * Gets the AccessibleContext associated with this JPanel.
     * For JPanels, the AccessibleContext takes the form of an
     * AccessibleJPanel.
     * A new AccessibleJPanel instance is created if necessary.
     *
     * @return an AccessibleJPanel that serves as the
     *         AccessibleContext of this JPanel
     */
    @BeanProperty(bound = false)
    public AccessibleContext getAccessibleContext() {
        if (accessibleContext == null) {
            accessibleContext = new AccessibleJPanel();
        }
        return accessibleContext;
    }

    /**
     * This class implements accessibility support for the
     * <code>JPanel</code> class.  It provides an implementation of the
     * Java Accessibility API appropriate to panel user-interface
     * elements.
     * <p>
     * <strong>Warning:</strong>
     * Serialized objects of this class will not be compatible with
     * future Swing releases. The current serialization support is
     * appropriate for short term storage or RMI between applications running
     * the same version of Swing.  As of 1.4, support for long term storage
     * of all JavaBeans
     * has been added to the <code>java.beans</code> package.
     * Please see {@link java.beans.XMLEncoder}.
     */
    @SuppressWarnings("serial") // Same-version serialization only
    protected class AccessibleJPanel extends AccessibleJComponent {

        /**
         * Constructs an {@code AccessibleJPanel}.
         */
        protected AccessibleJPanel() {}

        /**
         * Get the role of this object.
         *
         * @return an instance of AccessibleRole describing the role of the
         * object
         */
        public AccessibleRole getAccessibleRole() {
            return AccessibleRole.PANEL;
        }
    }
}

NO respondo dudas por mensaje privado
Publicando vuestras dudas en el foro público conseguimos:
- Que más gente aporte respuestas mejores o complementarias.
- Que otras personas puedan aprender de vuestras dudas.

Mejor en PÚBLICO que en privado. Gracias

manuromero73

  • Sin experiencia
  • *
  • APR2.COM
  • Mensajes: 40
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #11 en: 19 de Septiembre 2023, 19:49 »
Por ese lado creo entenderlo, hay clases de Java que estan creadas por los desarrolladores de Java y que nosotros usamos pero no hemos desarrollado.

Creo entender que el paint de un elemento grafico se ejecutará cada vez que mandemos a redibujar un elemento grafico y que el elemento grafico del Tablero incluye un JPanel por ser la propia clase extend de JFrame y una Bola por ser la Bola un atributo del Tablero. Al invocarse el repaint del tablero, se invoca el paint del Tablero, y el paint del Tablero dibuja todos los elementos gráficos vinculados al tablero bien sean por su propia naturaleza (extensión de JPanel) como por ser atributos del Tablero (en este caso un objeto Bola). Y en realidad podria haber sido la Bola otro elemento grafico distinto pero como indicaste

 
Citar
Mi intención era inicializar la Bola por separado en la clase GestorJuego (que veremos después) y luego pintarla en Tablero. Pero no he conseguido/sabido hacer que el tablero la pinte.

¿Si la clase Bola hubiera sido independiente del Tablero habría tenido un método paint? No veo método paint en la clase Bola, pero supongo que podría existir metodo paint si fuera independiente? ¿la g en la clase Tablero representa el interior del elemento grafico Jpanel para poder manipularlo?

He creado un objeto Tablero sin ejecutar el main y no se muestra nada en pantalla. He creado un objeto Juego sin ejecutar el main y se muestra el tablero con la bola en pantalla, y se pueden pulsar botones y funciona bien. Entiendo que si hacemos esto estariamos ejecutando el programa en el hilo principal, y ademas tenemos el SwingWorker, por lo que estariamos usando dos hilos, mientras que si iniciamos el programa con el main usamos tres hilos, uno el principal otro para los eventos y otro para mover la bola.

Viendo el codigo creo entender que el Tablero no se dibuja hasta que no se crea el Juego, porque el Juego es un JFrame dentro del cual esta el tablero. Asi estaria el JFrame de Juego como elemento de maximo nivel, y dentro de este tenemos el panel de botones y el tablero. Y dentro del tablero tenemos la bola o eso es lo que interpreto.
Gracias por la ayuda.

Kabuto

  • Moderador Global
  • Experto
  • *******
  • Mensajes: 989
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #12 en: 20 de Septiembre 2023, 11:41 »
Citar
¿Si la clase Bola hubiera sido independiente del Tablero habría tenido un método paint? No veo método paint en la clase Bola, pero supongo que podría existir metodo paint si fuera independiente?

Bola es una Ellipse2D y si consultamos su documentación en la API de Java, vemos que no tiene método paint. Es decir, no puede dibujarse en pantalla por sí sola.

Solo tienen representación gráfica(paint) las clases que descienden de Component, que es una clase de la librería AWT.
Esta librería es la primera que usaba Java en sus inicios para mostrar GUI's en pantalla.
Pero enseguida demostró tener muchas limitaciones, por lo que apenas 2 años después apareció la librería Swing, que está construida sobre AWT, por lo que en realidad cuando construimos interfaces estamos usando ambas librerías.

A estos Component(JFrame, JPanel, JLabel, JDialog, etc.....) podemos pasarle otros elementos para que los "pinten" dentro de sus límites, en nuestro caso, una elipse bidimensional.

Citar
¿la g en la clase Tablero representa el interior del elemento grafico Jpanel para poder manipularlo?
La g más bien es el "pincel" que pinta el elemento gráfico.
A g se le indica qué elemento ha de pintar, la posición de la pantalla donde ha de ser pintado, con qué tamaño, con qué colores, con qué fuente de texto....

« Última modificación: 20 de Septiembre 2023, 11:47 por Kabuto »
NO respondo dudas por mensaje privado
Publicando vuestras dudas en el foro público conseguimos:
- Que más gente aporte respuestas mejores o complementarias.
- Que otras personas puedan aprender de vuestras dudas.

Mejor en PÚBLICO que en privado. Gracias

manuromero73

  • Sin experiencia
  • *
  • APR2.COM
  • Mensajes: 40
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #13 en: 23 de Septiembre 2023, 21:50 »
Gracias por las explicaciones, uffff. He estado tratando de hacer calculos sobre como se inicia la bola y la velocidad a la que se mueve la bola inicialmente y no se si lo que me sale es así. Si no entiendo mal la bola se desplaza inicialmente una desviacionX y una desviacionY, y en el momento inicial la desviacionX puede tomar valor 5 ó -5, mientras que la desviacionY puede tomar valores entre 5, 4, 3, 2, 1, 0, -1, -2, -3, -4 ó -5. Si esto es asi, supongamos que la x vale 5 y la y vale 0, en ese caso la bola se movería 5 pixeles/ud tiempo. Supongamos que la x vale 5 y la y vale 5, en ese caso la bola se movería siguiendo un triángulo sqr(5^2+5^2) = raiz de 50 = 7,07 pixeles/ud tiempo.
Los angulos en que podría salir la bola serían entre 45 y 315 (-45) si va hacia la derecha. O entre 135 y 225 si va hacia la izquierda. El resto de ángulos no serían posibles en la salida al estar fijados los valores de partida a los rangos especificos.

Cuando se produce una colision una de las dos desviaciones tomaria el valor de VELOCIDAD, es decir, que si no hemos cambiado nada, una de las dos desviaciones vale 5 y la otra puede valer 5, 4, 3, 2, 1, 0, -1, -2, -3, -4 ó -5. De este modo la bola puede moverse o bien a 5 pixeles/ud tiempo en vertical o en horizontal, o con un máximo de 7,07 pixeles/ud tiempo si toma un ángulo de rebote máximo.

Esto explicaría que la bola parezca ralentizarse levemente en algunos momentos y acelerarse levemente en otros. Los cambios máximos de velocidad serian el pasar de 5 a 7,07 o el pasar de 7,07 a 5. Lo cual no es muy apreciable visualmente aunque creo que si se aprecia aunque el ojo puede engañar je je.

Si esto fuera asi (que seguro no estoy) no habria una velocidad constante de la bola en ningun caso, aunque no se pulse acelerar ni frenar, por el mero hecho de que las desviaciones la pueden acelerar o frenar levemente. Y para que la velocidad fuera constante tendria que basarse en un angulo y una distancia a avanzar. Y conocido esto calcular cuanto tiene que ser la desviacionX y la desviacionY. Creo que esto tiene logica en el sentido de que si estamos en un punto y caminamos 5 pasos a la derecha y 5 pasos hacia arriba, no habremos caminado la misma distancia que si caminamos 5 pasos a la derecha y ninguno hacia arriba. Para caminar la misma distancia en cada movimiento tendriamos que coger una cuerda y siendo nosotros el centro trazar una circunferencia de radio 5 y avanzar hasta un punto de esa circunferencia y de esa manera habriamos avanzado 5 pixeles/ud de tiempo y nos estariamos moviendo a una velocidad uniforme o eso creo. No se si tu lo ves igual.

Kabuto

  • Moderador Global
  • Experto
  • *******
  • Mensajes: 989
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #14 en: 23 de Septiembre 2023, 23:19 »
Claro.
El problema es que estamos limitados a movernos mediante números enteros.
Es decir, o nos movemos 5 píxeles, ó 4, ó 3... pero no podemos movernos 2,5 píxeles, ni 3,75.

Por eso hice que una de las desviaciones siempre fuera valor 5. Esto limita los ángulos de movimientos posibles, pero consigue que la velocidad sea constante.
Antes de eso, la velocidad cambiaba radicalmente, si las desviaciones eran por ejemplo 1 y 2 iba muy lento en comparación a cuando resultaban ser 4 y 5 por ejemplo.
NO respondo dudas por mensaje privado
Publicando vuestras dudas en el foro público conseguimos:
- Que más gente aporte respuestas mejores o complementarias.
- Que otras personas puedan aprender de vuestras dudas.

Mejor en PÚBLICO que en privado. Gracias

manuromero73

  • Sin experiencia
  • *
  • APR2.COM
  • Mensajes: 40
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #15 en: 25 de Septiembre 2023, 19:09 »
No acabo de verlo claro, por un lado no se si esta equivocado lo que he planteado respecto a la velocidad, pero si no esta equivocado la velocidad no es constante sino que sufre variaciones (pequeñas pero apreciables).
Por otro lado veo que la clase Ellipse2D es una clase abstracta cuyas implementaciones son Ellipse2D.Double y Ellipse2D.Float que en ambos casos parecen trabajar con decimales, de modo que seria posible fijar el avance con decimales, por eso no acabo de entender por qué no se podría trabajar con decimales. He buscado codigo y he encontrado este en que mueve una bola por la pantalla y parece que el avance se calcula con decimales, al menos usa private double xPosition, yPosition; aunque luego tambien usa redondeos

Código: [Seleccionar]
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class BolaDeEjemplo implements Runnable {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new BolaDeEjemplo());
    }

    private final BolaDeEjemploModel model;

    private final DrawingPanel drawingPanel;

    public BolaDeEjemplo() {
        this.model = new BolaDeEjemploModel();
        this.drawingPanel = new DrawingPanel(model);
    }

    @Override
    public void run() {
        JFrame frame = new JFrame("Bola Ejemplo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        frame.add(drawingPanel, BorderLayout.CENTER);

        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);

        Timer timer = new Timer(30, new BallMotionListener(this, model));
        timer.start();
    }

    public void repaint() {
        drawingPanel.repaint();
    }

    public class DrawingPanel extends JPanel {

        private static final long serialVersionUID = 1L;

        private final BolaDeEjemploModel model;

        public DrawingPanel(BolaDeEjemploModel model) {
            this.model = model;
            this.setPreferredSize(model.getDrawingPanelDimension());
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            Ball ball = model.getBall();
            Point2D point = ball.getPosition();
            int x = (int) Math.round(point.getX());
            int y = (int) Math.round(point.getY());
            int radius = ball.getRadius();
            int diameter = radius + radius;

            g.setColor(Color.BLUE);
            g.fillOval(x - radius, y - radius, diameter, diameter);
        }

    }

    public class BallMotionListener implements ActionListener {

        private final BolaDeEjemplo view;

        private final BolaDeEjemploModel model;

        public BallMotionListener(BolaDeEjemplo view, BolaDeEjemploModel model) {
            this.view = view;
            this.model = model;
        }

        @Override
        public void actionPerformed(ActionEvent event) {
            model.getBall().calculateNewPosition();
            view.repaint();
        }

    }

    public class BolaDeEjemploModel {

        private final Ball ball;

        private final Dimension drawingPanelDimension;

        public BolaDeEjemploModel() {
            this.drawingPanelDimension = new Dimension(400, 400);
            this.ball = new Ball(4, 325, drawingPanelDimension);
        }

        public Ball getBall() {
            return ball;
        }

        public Dimension getDrawingPanelDimension() {
            return drawingPanelDimension;
        }

    }

    public class Ball {

        private double xPosition, yPosition;

        private int angle, radius, speed;

        private final Dimension drawingPanelDimension;

        public Ball(int speed, int angle, Dimension drawingPanelDimension) {
            this.speed = speed;
            this.radius = 12;
            this.angle = angle;
            this.drawingPanelDimension = drawingPanelDimension;
            this.xPosition = drawingPanelDimension.width / 2;
            this.yPosition = drawingPanelDimension.height / 2;
        }

        public int getAngle() {
            return angle;
        }

        public void setAngle(int angle) {
            this.angle = angle;
        }

        public int getSpeed() {
            return speed;
        }

        public void setSpeed(int speed) {
            this.speed = speed;
        }

        public int getRadius() {
            return radius;
        }

        public void calculateNewPosition() {
            double theta = Math.toRadians(angle);
            double xDifference = Math.cos(theta) * speed;
            double yDifference = Math.sin(theta) * speed;
            xPosition += xDifference;
            yPosition += yDifference;

            if (xPosition < radius) {
                angle = 540 - angle;
            }

            if (xPosition > drawingPanelDimension.width - radius) {
                angle = 540 - angle;
            }

            if (yPosition < radius) {
                angle = 360 - angle;
            }

            if (yPosition > drawingPanelDimension.height - radius) {
                angle = 360 - angle;
            }

            angle %= 360;
        }

        public Point2D getPosition() {
            return new Point2D.Double(xPosition, yPosition);
        }

    }

}

Kabuto

  • Moderador Global
  • Experto
  • *******
  • Mensajes: 989
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #16 en: 26 de Septiembre 2023, 18:00 »
Sí.
El cálculo que se hace en ese código es más completo, hermoso y preciso.

Pero a la hora de la verdad, cuándo toca pintar la bola, se hace un redondeo y se castea a valores enteros:

Citar
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            Ball ball = model.getBall();
            Point2D point = ball.getPosition();
            int x = (int) Math.round(point.getX());
            int y = (int) Math.round(point.getY());

Es decir, que si los cálculos dicen que la bola ha de avanzar 2,35 pixeles, pues avanzará 2 píxeles.
Y si dicen que ha de avanzar 2,84 píxeles, pues avanzará 3 píxeles.

Y es porque los píxeles son celdas físicas de una pantalla que, o están pintados de un color, o lo están de otro.
No puede estar 1/3 pixel pintado de blanco y los otros 2/3 pintados de azul, por ejemplo.

Es como si al caminar nosotros, solo pudiéramos avanzar metros completos, nunca medio metro, ni 1 metro y 750 cm...


En cualquier caso, con ese código ya no hay que hacer la "chapuza" que hice yo, de mantener constante una de las desviaciones para que la diferencia de velocidad en cada rebote no fuera tan notable.

Es lo bueno de saber matemáticas.
Es muy interesante, pero hay cosas que no entiendo.

Por ejemplo, no se por qué para el eje x se usa la constante 540 y para el eje y en cambio es 360  ???

Citar

        public void calculateNewPosition() {
            double theta = Math.toRadians(angle);
            double xDifference = Math.cos(theta) * speed;
            double yDifference = Math.sin(theta) * speed;
            xPosition += xDifference;
            yPosition += yDifference;

            if (xPosition < radius) {
                angle = 540 - angle;
            }

            if (xPosition > drawingPanelDimension.width - radius) {
                angle = 540 - angle;
            }

            if (yPosition < radius) {
                angle = 360 - angle;
            }

            if (yPosition > drawingPanelDimension.height - radius) {
                angle = 360 - angle;
            }
NO respondo dudas por mensaje privado
Publicando vuestras dudas en el foro público conseguimos:
- Que más gente aporte respuestas mejores o complementarias.
- Que otras personas puedan aprender de vuestras dudas.

Mejor en PÚBLICO que en privado. Gracias

manuromero73

  • Sin experiencia
  • *
  • APR2.COM
  • Mensajes: 40
    • Ver Perfil
Re: ejercicio java juego bola que se mueve y rebota en bordes tablero
« Respuesta #17 en: 14 de Octubre 2023, 13:12 »
He estado haciendo pruebas y me cuesta bastante depende de las matematicas y la vision espacial que cada uno tengamos jeje. Lo que he entendido es que se hacen estas ejecuciones.

Establece el ángulo de partida en 325º (en grados sexagesimales es más fácil de ver, aunque luego para el cálculo matemático usa radianes porque es la forma de trabajar los grados que se suele usar en matemáticas y que usa Java, pero para la lógica nos podemos olvidar de los radianes).

Si establecemos el ángulo de partida en 0, la bola sale en horizontal hacia la derecha y rebota en la pared derecha adoptando movimiento en sentido 180º (pi radianes) con lo que sale rebotada hacia la izquierda en horizontal, y al llegar a la pared izquierda vuelve a rebotar igual, con lo cual lo que hace es moverse siempre en horizontal.

Si establecemos el ángulo de partida en 90, la bola sale en vertical hacia abajo y rebota en la pared inferior adoptando movimiento en sentido 180º (pi radianes) con lo que sale rebotada hacia arriba en vertical, y al llegar a la pared superior vuelve a rebotar igual, con lo cual lo que hace es moverse siempre en vertical.

Con esto ya tenemos cómo trabaja los ángulos que es 0 horizontal derecha 90 abajo 180 horizontal izquierda 270 arriba 360 equivale a 0 completando "una vuelta", 450 equivale a 90 ya que sería como una vuelta y noventa más con lo cual llegamos al mismo angulo (450%360=90 es decir es el resto y % es el operador módulo que devuelve el resto de una división, 540 equivale a 180 ya que 540%360 = 180 es el resto) y así sucesivamente hasta el valor que queramos. Por ejemplo 1210º equivale a 130º porque el resto de dividir 1210 entre 360 es 130 es decir 1210%360 = 130

Para mover la bola primero comprueba si hay rebote:

Citar

            if (xPosition < radius) { //Bola rebota con pared izquierda
                angle = 540 - angle;
            }

            if (xPosition > drawingPanelDimension.width - radius) { //Bola rebota con pared derecha
                angle = 540 - angle;
            }

            if (yPosition < radius) { //Bola rebota con pared inferior
                angle = 360 - angle;
            }

            if (yPosition > drawingPanelDimension.height - radius) { //Bola rebota con pared superior
                angle = 360 - angle;
            }

           angle %= 360; // Operador modulo resto de division entre 360


Haya habido o no rebote termina calculando el ángulo como angle %= 360; que si no me equivoco es lo mismo que angle = angle % 360, es decir, el valor de angle es el resto de dividir angle entre 360. Para el valor de partida 325 al dividir entre 360 el resto es 325, de modo que la bola sigue siempre en el ángulo en que esté hasta llegar a rebotar con una pared.
Dado que 270 equivale a vertical y 360 hacia la derecha, con 325 está tomando un ángulo que no es exactamente el intermedio entre ambos que seria 315º, sino un poco superior, con lo cual la bola sale hacia arriba y hacia la derecha (ligeramente más hacia arriba que hacia la derecha).

La primera pared a la que llega es la derecha y cuando esto ocurre hace el calculo angle = 540 - angle; que devuelve 215º y al aplicar  angle %= 360; lo que hace es transformarlo a un valor entre 0 y 360 pero aquí sigue resultando 215. Aqui 540 equivale a 180. Si la bola fuera con 0 de partida, al restar 0 a 540, queda 540, que al ser 180, implica un rebote formando un angulo de 180º respecto al ángulo inicial. Lo que hace es calcular un ángulo simétrico respecto al angulo que traía respecto al eje y. Por ejemplo para 30º el simétrico sería 180-30 = 150º y esto es lo que ocurre si hacemos 540-30 = 510 y si ahora hacemos el módulo entre 360 tenemos 510%360 = 150º. Así restando a 540 el angulo con el que venía obtenemos el ángulo "simétrico" respecto al eje y, lo cual genera un efecto de rebote natural.

La idea como yo la veo es: si la bola rebota con pared derecha o izquierda, el nuevo ángulo es la diferencia entre 540 (equivale a 180) y el ángulo que traía, lo que equivale a mantener la componente vertical y hacer opuesta la componente horizontal, es hacer una simetría respecto al eje vertical. Si la bola va con 0º la componente vertical es nula, con lo cual al hacerse opuesta sale en dirección 180. Si la bola va con 10º ya existe una cierta componente vertical y el rebote que se genera en lugar de ser de 180º es a 170º que es hacia la izquierda y ligeramente hacia abajo en este caso. Si la bola va con 20º el rebote es a 160º. Si la bola va con 30º el rebote es a 150º y así sucesivamente.

Usar 540º nos permite poder operar con cualquier ángulo entre 0 y 360 obteniendo unos resultados correctos y sin tener que operar con ángulos negativos.

Si la bola choca con la pared superior o inferior, por ejemplo llevando un angulo de avance de 80º genera la simetria en este caso respecto al eje x haciendo la resta angle = 360 - angle; por ejemplo para 80 el angulo de rebote es 280 que es una simetria respecto al eje x. Para 10º el angulo de rebote sería 350º.

Aqui las simetrias serian el efecto rebote: si rebota con pared lateral, simetria con la vertical para lo cual ha de usarse 540, si rebota con pared superior o inferior, simetria con la horizontal para lo cual ha de usarse 360.

La simetria respecto a la vertical se consigue a partir de 360 grados. En cambio respecto a la horizontal se consigue con 180. Pero si usaramos 180 no funcionaria con angulos mayores de 180. En cambio usando 540 que es lo mismo se obtiene el angulo de rebote sin importar si el angulo es superior a 180. Luego aplicando el modulo se transforma a un valor entre 0 y 360 equivalente y asi se continua.

El problema que le veo a esto es que es una forma de rebotar absolutamente matematica, lo cual es muy bonito y muy perfecto, pero ocurre una cosa, que es que al no existir nada de aleatoriedad los movimientos se repiten siempre exactamente igual. Es decir, en cada ejecución todo se repite porque siempre se producen los mismos calculos matematicos. Por ejemplo si el angulo de partida es 0 lo pondríamos en this.ball = new Ball(4, 325, drawingPanelDimension); cambiándolo por this.ball = new Ball(4, 0, drawingPanelDimension); pues la bola rebota simétricamente respecto al eje vertical y como va perfectamente horizontal, rebota perfectamente horizontal y lo que hace es estar continuamente de un lado a otro repitiendo siempre el mismo movimiento. Para que no se repitiera el movimiento tendriamos que introducir un factor de aleatoriedad. Pero bueno todo depende de los objetivos que se quisieran lograr en unos casos puede ser interesante hacerlo matematico y en otros un poco aleatorio.

Si buscamos explicaciones normalmente nos vienen como es lo mas habitual, con el angulo de 90 hacia arriba, pero por lo dicho anteriormente java lo trabaja hacia abajo lo cual nos complica un poco la vision porque tenemos que girarlo todo pero para ver la idea nos vale igual  8)


Aqui vemos como si choca con la pared superior con angulo 60 tiene que salir rebotado con angulo 300. Si choca con la pared lateral con angulo 60 tiene que salir rebotado con angulo 120 etc.

 

Sobre la educación, sólo puedo decir que es el tema más importante en el que nosotros, como pueblo, debemos involucrarnos.

Abraham Lincoln (1808-1865) Presidente estadounidense.

aprenderaprogramar.com: Desde 2006 comprometidos con la didáctica y divulgación de la programación

Preguntas y respuestas

¿Cómo establecer o cambiar la imagen asociada (avatar) de usuario?
  1. Inicia sesión con tu nombre de usuario y contraseña.
  2. Pulsa en perfil --> perfil del foro
  3. Elige la imagen personalizada que quieras usar. Puedes escogerla de una galería de imágenes o subirla desde tu ordenador.
  4. En la parte final de la página pulsa el botón "cambiar perfil".