Java - Ayuda en la creación de un juego

 
Vista:

Ayuda en la creación de un juego

Publicado por Joel Sebastian (1 intervención) el 10/05/2020 07:57:59
Hola!, soy algo nuevo programando y estoy resolviendo un ejercicio (soy estudiante) el cual se me está complicando un poco, es como un pequeño juego de un tanque que dispara y tiene que destruir objetos creados en el panel, ya hice la parte de la creación de los objetos con una lista dinámica ya que si un objeto se elimina ese espacio tiene que quedar vacío el espacio en donde este se encontraba, quisiera agregar el patrón observer para hacer que cuando la bala impacte con el objeto el observer detecte ese cambio y haga un repaint en donde ya no esté el objeto "destruido", está gran parte del código hecho, lo único que me falta es:

-la parte de disparar la bala
-el patron observer

Les agradecería mucho su ayuda, sería de gran apoyo para mí.
Les dejo mi proyecto anexado a esta pregunta

Muchas gracias de antemano.
Valora esta pregunta
Me gusta: Está pregunta es útil y esta claraNo me gusta: Está pregunta no esta clara o no es útil
0
Responder
Imágen de perfil de Kabuto
Val: 3.428
Oro
Ha mantenido su posición en Java (en relación al último mes)
Gráfica de Java

Ayuda en la creación de un juego

Publicado por Kabuto (1385 intervenciones) el 11/05/2020 21:08:08
Hola.
No estoy familiarizado con el patrón Observer. De todos modos, se considera obsoleto desde Java 9, por lo que, aunque se pueda usar, no se "debería" hacerlo.

Por otra parte, me ha parecido interesante el juego que propones, así que he intentado hacerlo, a ver como se me ocurría a mí resolverlo, porque no había hecho nada similar antes.
Quiero compartir por aquí mi código por si te sirve para tomar ideas u otros enfoques.
No está terminado, se dibujan "tanques" y se disparan proyectiles, pero falta detectar cuando un proyectil colisiona contra un tanque enemigo.

Para crear los tanques y los proyectiles he usado la clase Polygon para dibujarlos. Nada del otro mundo, son gráficos tipo consola Atari 2600 ja ja

tanques

Esta es la clase Tanque, que como digo, hereda de Polygon.
Para construir el polígono se le pasan dos arrays de enteros, uno son las coordenadas X y el otro son las coordenadas Y que situan los puntos/vertices que conformarán el polígono. Además de los dos arrays, se le pasa otro entero indicando la cantidad de vertices que tendrá el polígono.

Solo tiene un método, que se encarga de hacer una transformación, en este caso, una rotación de 180 grados.
Así puedo usar el mismo polígono para el jugador como para los enemigos. No tengo que recalcular yo las posiciones de los vértices para dibujar la figura invertida, con ese código, lo calcula el sistema.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Tanque extends Polygon{
 
	private static int[] coordX = {0, 66, 66, 36, 36, 38, 38, 28, 28, 30, 30, 0};
	private static int[] coordY = {65, 65, 25, 25, 10, 10, 0, 0, 10, 10, 25, 25};
 
	public Tanque() {
		super(coordX, coordY, 12);
	}
 
	public void rotar() {
 
		// Crea una lista de pares Point2D
		ArrayList<Point2D> list = new ArrayList<Point2D>();
		for(int j = 0; j < ypoints.length; j++){
			list.add(new Point2D.Double(xpoints[j], ypoints[j]));
		}
 
		// Nuevo array donde se guardaran los puntos rotados
		Point2D[] rotatedPoints = new Point2D[list.size()];
 
		// Rotamos los puntos 180 grados
		AffineTransform transform = AffineTransform.getRotateInstance(Math.toRadians(180),
				getBounds2D().getCenterX(), getBounds2D().getCenterY());
		transform.transform(list.toArray(new Point2D[0]), 0, rotatedPoints, 0, rotatedPoints.length);
 
		// actualizamos los puntos x e y, a partir de los pares Point2D rotados
		for(int j = 0; j < rotatedPoints.length; j++){
			xpoints[j] = (int)rotatedPoints[j].getX();
			ypoints[j] = (int)rotatedPoints[j].getY();
		}
	}
}

Y ya está, esta clase no tiene más. Básicamente es un polígono que luego podremos dibujar en pantalla. Nada más.

Ahora muestro la clase Bala. También es un polígono, más sencillo de dibujar, pues es un rombo de 4 vértices.
Pero tiene dos peculiaridades.

Una es que, a diferencia del Tanque, primero lo inicializo con vértices en posiciones 0. Esto se debe a que lo primero que tengo que indicarle al polígono son sus vértices invocando la instrucción super(). Estos vértices no solo determinan su forma, si no también su posición.
Pero yo al instanciar una Bala, no se de antemano cuál será su posición (depende de la posición del tanque del jugador al disparar), tengo que calcularla, pero no me deja hacer cálculos previos a la invocación del super(). Nada más empezar tengo que darle unas coordenadas, las que sean.
Así que eso hago, le doy unas coordenadas con valor 0, indicando eso sí que serán 4 vértices.
Luego calculo las coordenadas correctas según la posición x e y del tanque del jugador, y entonces paso a actualizar los vértices del polígono con las coordenadas correctas.

Esto no me pasaba con el Tanque, porque si sé de antemano donde va a estar cada uno en el escenario. Así que TODOS los construyo a partir de la posición 0,0 de la pantalla, y luego son trasladados a sus lugares correspondientes. Esto lo veremos luego.

Otra peculiaridad es que uso unas variables para alterar el color de la Bala durante el juego. Uso tres int para representar los canales RGB del color y lo que hago es que varío el color verde (canal G) entre unos límites determinados.
Así cada vez que se redibuja la bala en pantalla el color va cambiando entre unos tonos rojos y amarillos.
Esto no es importante, es algo estético, y por experimentar cosas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Bala extends Polygon{
 
	//Canales de color RGB
	private final int colorR = 200;
	private int colorG = 40; //Solo cambiara el verde, de 40 a 200
	private final int colorB = 39;
 
	private int variacionG = 10; //Comienza positivo, para incrementar 10 unids
 
	public Bala (int x, int y) {
		//Inicializamos superclase con un poligono de 4 puntos con coordenadas cualquiera
		super(new int[]{0,0,0,0}, new int[]{0,0,0,0}, 4);
 
		//Calculamos coordenadas en base a las X e Y recibidas
		int[] coordX = {x, x+15, x+30, x+15};
		int[] coordY = {y+15, y, y+15, y+30};
 
		//Actualizamos los puntos de coordeandas para recrear el poligono deseado
		for (int j = 0; j < 4; j++) {
			xpoints[j] = coordX[j];
			ypoints[j] = coordY[j];
		}
 
	}
 
	/**
	 * Retorna el color actual de la Bala, que irá variando
	 * su canal verde únicamente.<br>
	 * A cada petición de Color, el canal verde varía en
	 * 10 unidades.
	 * @return Color de Bala
	 */
	public Color getColor() {
		if (colorG == 200) //Color verde alcanza tope de 200
			variacionG = -10; //Ahora decrementamos
		else if (colorG == 40) //Color verde al minimo de 40
			variacionG = 10; //Pasamos a incrementar
		colorG += variacionG;
		return new Color(colorR, colorG, colorB);
	}
 
}

Quedan dos clases más por ver.
La siguiente la he llamado CampoBatalla y es un JPanel donde va a transcurrir la "acción".

Tiene dos atributos, un Vector con los tanques enemigos, y un Tanque que será el jugador.

Mediante métodos, construyo 6 enemigos usando un bucle, donde los roto 180 grados y los traslado a una determinada distancia dejando un espacio entre ellos.
Trasladar es facilísimo usando el método translate() que ofrece la clase Polygon. Solo le indicamos el desplazamiento que queremos en los ejes Y, X, y el solo se encarga de recalcular la posición de cada vértice.

También construyo el tanque del jugador y los traslado a la parte baja del escenario.

Hay dos métodos llamados getTanqueX() y getTanqueY(), se encargan de informar de la posición del tanque del jugador, que nos servirá luego para determinar donde ha de aparece la Bala cuando dispare. Estos valores, ya los retorno con una pequeña variación para que la Bala parezca que salga del cañón.

Los dos siguientes métodos son moverIzq() y moverDer().
Lo que hacen es trasladar el Tanque 5 pixeles a un lado o a otro, según la tecla que pulse el jugador. Antes de mover, compruebo si estoy cerca de los límites de la ventana, para evitar que el Tanque se salga y desaparezca.
Después de trasladar el Tanque, pido repintar el escenario con la nueva posición, pero fíjate que solo repinto el cuadrante inferior por donde se desplaza el tanque.
Evito repintar toda la ventana, porque si hay Balas en pantalla (he puesto que se muevan lentos) empiezan a parpadear si movemos el Tanque.
Así que solo repinto la parte necesaria.

El último método es el que "pinta" los Tanques en pantalla.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class CampoBatalla extends JPanel{
 
	private Vector<Tanque> enemigos;
	private Tanque tanque;
 
	public CampoBatalla() {
		crearEnemigos();
		crearTanque();
	}
 
	private void crearEnemigos() {
		enemigos = new Vector<Tanque>();
		for (int i = 0; i < 7; i++) {
			Tanque t = new Tanque();
			t.rotar(); //Giramos la figura original 180 grados
			if (i == 0)
				t.translate(5, 5);
			else {
				int xAnterior = (int) enemigos.get(i - 1).getBounds2D().getX();
				int anchoTanque = (int) t.getBounds2D().getWidth();
				t.translate(xAnterior + anchoTanque + 35 , 5);
			}
			enemigos.add(t);
		}
	}
 
	private void crearTanque() {
		tanque = new Tanque();
		tanque.translate(5, 450 - (int)(tanque.getBounds2D().getHeight()));
	}
 
	public int getTanqueX() {
		return (int) tanque.getBounds2D().getCenterX() - 15;
	}
 
	public int getTanqueY() {
		return (int) tanque.getBounds2D().getCenterY() - 65;
	}
 
	public void moverIzq() {
		int posX = (int) tanque.getBounds2D().getX();
		if (posX > 5) //Limite son 5 px desde el borde izquierdo
			tanque.translate(-10, 0);
		//Solo repintamos el cuadrante por donde se desplaza el tanque
		Rectangle bounds = tanque.getBounds();
		repaint(0, bounds.y, 600, bounds.height);
	}
 
	public void moverDer() {
		int ancho = (int) tanque.getBounds2D().getWidth();
		int xLimite = 575 - ancho;
		int posX = (int) tanque.getBounds2D().getX();
 
		if (posX < xLimite)
			tanque.translate(10, 0);
		//Solo repintamos el cuadrante por donde se desplaza el tanque
		Rectangle bounds = tanque.getBounds();
		repaint(0, bounds.y, 600, bounds.height);
	}
 
	@Override
	public void paint(Graphics g) {
		super.paint(g);
		Graphics2D g2 = (Graphics2D) g;
		g2.setColor(new Color(175, 200, 39));
		for (Tanque enemigo: enemigos)
			g2.fillPolygon(enemigo);
 
		g2.setColor(new Color(72, 140, 54));
		g2.fillPolygon(tanque);
	}
 
}

Por útlimo, la clase principal con el método main().

Es un JFrame que va a contener el CampoBatalla (que es un JPanel).

Incorpora un KeyListener para detectar la pulsación de las flechas izquierda y derecha para mover el tanque. Espacio para disparar.

Lo interesante aquí, es ¿en que consiste disparar?

Para disparar he creado una clase interna que hereda de la clase Thread.
Es decir, cada vez que se dispara, el programa crea un hilo de ejecución paralelo.
Este hilo se encarga de ir desplazando la Bala disparada.

Usando Thread, pueden haber varias Balas en pantalla, cada una moviéndose y cambiando de color, de forma independiente del programa principal. Así podemos seguir moviendo el Tanque y disparando más Balas sin problema.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class Juego extends JFrame implements KeyListener{
 
	CampoBatalla campo;
 
	public Juego() {
 
		campo = new CampoBatalla();
		add(campo);
 
		addKeyListener(this);
 
		setTitle("Tanques");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setResizable(false);
		setSize(600, 500);
		setLocationRelativeTo(null);
		setVisible(true);
	}
 
	public static void main(String[] args) {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				new Juego();
			}
		});
 
	}
 
 
	private class Disparar extends Thread {
 
		Bala bala;
 
		public Disparar(int x, int y) {
			bala = new Bala(x, y);
			Graphics2D g2D = (Graphics2D) campo.getGraphics();
			g2D.setColor(bala.getColor());
			g2D.fillPolygon(bala);
			g2D.dispose();
		}
 
		@Override
		public void run() {
			Graphics2D g2D = (Graphics2D) campo.getGraphics();
			while(bala.getBounds().y > 0) {
				//Borramos la bala
				g2D.setColor(campo.getBackground());
				g2D.fillPolygon(bala);
				//La desplazamos
				bala.translate(0, -1);
				//La pintamos de nuevo
				g2D.setColor(bala.getColor());
				g2D.fillPolygon(bala);
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			g2D.dispose();
		}
	}
 
 
	//Metodos de la interface KeyListener
	@Override
	public void keyTyped(KeyEvent e) { /*Nada que hacer aqui*/ }
 
	@Override
	public void keyPressed(KeyEvent e) {
		int id = e.getKeyCode();
 
        if (id == KeyEvent.VK_RIGHT) {
        	campo.moverDer();
        }
        if (id == KeyEvent.VK_LEFT) {
        	campo.moverIzq();
        }
        if (id == KeyEvent.VK_SPACE) {
            Disparar pum = new Disparar(campo.getTanqueX(), campo.getTanqueY());
            pum.start();
        }
	}
 
	@Override
	public void keyReleased(KeyEvent e) { /*Nada que hacer aqui*/ }
 
}

Y eso es todo por ahora.
Seguramente no es la mejor forma de hacer un juego, pero funciona.
Solo falta hacer que se detecte la colisión entre bala y enemigo. Y que desaparezcan bala y enemigo.
Ahora mismo las balas no desaparecen y el enemigo se borra un poco, pero porque la bala va borrando allá por donde pasa.

A ver si en los próximos días lo completo.

Espero que te sirva de algo, y pregunta cualquier cosa que no entiendas.

Un saludo.
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
1
Comentar
Imágen de perfil de Kabuto
Val: 3.428
Oro
Ha mantenido su posición en Java (en relación al último mes)
Gráfica de Java

Ayuda en la creación de un juego

Publicado por Kabuto (1385 intervenciones) el 12/05/2020 19:12:21
Hola de nuevo.
Pues ya he completado lo que faltaba. Más sencillo de lo que parece.
De nuevo, usar la clase Polygon ayuda mucho. Ofrece un método boolean llamado intersects() que indica si un polígono intersecta con otro.
Como Bala y Tanque son polígonos, podemos valernos de este método.

A la clase CampoBatalla le he añadido este método.
Recorre la lista de Enemigos y comprueba si la Bala (el Rectángulo que ocupa su figura en realidad) que recibe por parámetro intersecta con alguno de ellos.
En caso afirmativo, elimina el enemigo de la pantalla y luego del Vector de enemigos.
Y retorna true para informar de la colisión.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean testColision(Rectangle2D bala) {
    int tanqueTocado = -1;
 
    for (int i = 0; i < enemigos.size() && tanqueTocado == -1; i++)
        if (enemigos.get(i).intersects(bala)) //Bala ha tocado tanque enemigo
            tanqueTocado = i;
 
    if (tanqueTocado != -1) { //Si tenemos un tanque tocado, lo eliminamos.
        Graphics2D g2 = (Graphics2D) getGraphics();
        g2.setColor(getBackground());
        g2.fillPolygon(enemigos.get(tanqueTocado));
        g2.dispose();
        enemigos.remove(tanqueTocado);
        return true;
    }
    else
        return false;
}

¿Y desde donde invocamos este método?

Pues desde la clase interna Disparar, que está dentro de la clase principal Juego.

La clase Disparar es el hilo (Thread) que dibuja y desplaza Balas en pantalla.
Pues tras cada desplazamiento, pregunta a CampoBatalla si ha habido colisión.
En caso afirmativo, pone fin al bucle que desplaza a la Bala.
Y además, ahora la Bala se borra al terminar el bucle, ya no se queda su polígono en pantalla como antes.

Marco en negrita los cambios respecto al código anterior.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private class Disparar extends Thread {
 
    Bala bala;
 
    public Disparar(int x, int y) {
        bala = new Bala(x, y);
        Graphics2D g2D = (Graphics2D) campo.getGraphics();
        g2D.setColor(bala.getColor());
        g2D.fillPolygon(bala);
        g2D.dispose();
    }
 
    @Override
    public void run() {
        Graphics2D g2D = (Graphics2D) campo.getGraphics();
        boolean impacto = false;
        while(bala.getBounds().y > 0 && !impacto) {
            //Borramos la bala
            g2D.setColor(campo.getBackground());
            g2D.fillPolygon(bala);
            //La desplazamos
            bala.translate(0, -1);
            //La pintamos de nuevo
            g2D.setColor(bala.getColor());
            g2D.fillPolygon(bala);
 
            if (campo.testColision(bala.getBounds2D()))
                impacto = true; //La clase CampoBatalla informa de un impacto de Bala
            else {
                try {
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
		//Bala finaliza recorrido. Borramos
        g2D.setColor(campo.getBackground());
        g2D.fillPolygon(bala);
        g2D.dispose();
    }
}

Y ya está. Tanque jugador puede disparar y eliminar Tanques enemigos.
Y no ha sido necesario ningún Observer.

De hecho, se necesita muy poco código. Al principio gasté mucho tiempo intentando usar la clase Canvas en lugar de JPanel.
La clase Canvas parecía más apropiada para esto, pero se producían muchos parpadeos (flickering) al repintar el frame.
Intenté aplicar double buffering siguiendo algunos ejemplo, pero no me sirvieron y gasté mucho tiempo dándole vueltas a eso.

Al final pasé a JPanel, que parece ser que ya trae double buffer de serie, y los parpadeos se han reducido a casi nada.

Curiosamente, si se probáis el código, se ve como al repintar la Bala durante su trayectoria hay un ligerísimo parpadeo, que desaparece si desplazamos el Tanque.

No sabría explicar el motivo.

En fin, ha sido divertido intentar esta práctica.
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
1
Comentar
Imágen de perfil de Rodrigo
Val: 2.041
Plata
Ha mantenido su posición en Java (en relación al último mes)
Gráfica de Java

Ayuda en la creación de un juego

Publicado por Rodrigo (623 intervenciones) el 12/05/2020 19:54:30
Felicitaciones!

Una curiosidad mia.
Cual es la fuente de la frase "De todos modos, se considera obsoleto desde Java 9", respecto al patron Observer?
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
0
Comentar
Imágen de perfil de Kabuto
Val: 3.428
Oro
Ha mantenido su posición en Java (en relación al último mes)
Gráfica de Java

Ayuda en la creación de un juego

Publicado por Kabuto (1385 intervenciones) el 13/05/2020 19:53:41
Lo vi en StackOverflow al querer informarme sobre esto, porque como dije, no estoy familiarizado con esta interface

Lo corrobora la docu de Oracle, tanto Observer como Observable están deprecated desde Java 9
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
0
Comentar
sin imagen de perfil
Val: 47
Ha aumentado su posición en 2 puestos en Java (en relación al último mes)
Gráfica de Java

Ayuda en la creación de un juego

Publicado por Pedro (25 intervenciones) el 12/05/2020 20:27:09
Muchísimas gracias!
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
0
Comentar
Imágen de perfil de Kabuto
Val: 3.428
Oro
Ha mantenido su posición en Java (en relación al último mes)
Gráfica de Java

Ayuda en la creación de un juego

Publicado por Kabuto (1385 intervenciones) el 13/05/2020 19:56:37
De nada.
Si tienes dudas, o si consigues implementarlo de otro modo, compártelo por aquí.
Un saludo.
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
0
Comentar