Detección de colisiones

En este tutorial aprenderemos como detectar cuando un sprite choca con otro. En nuestro juego haremos que la pelota rebote contra la raqueta. Además haremos que el juego termine si la pelota alcanza el limite inferior del lienzo mostrando una ventana popup con el clásico mensaje "Game Over".

Game Over

A continuación vemos nuestra clase Game que es idéntica a la anterior con la sola diferencia de que se ha agregado el método gameOver();

package com.edu4java.minitennis6;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

@SuppressWarnings("serial")
public class Game extends JPanel {

	Ball ball = new Ball(this);
	Racquet racquet = new Racquet(this);

	public Game() {
		addKeyListener(new KeyListener() {
			@Override
			public void keyTyped(KeyEvent e) {
			}

			@Override
			public void keyReleased(KeyEvent e) {
				racquet.keyReleased(e);
			}

			@Override
			public void keyPressed(KeyEvent e) {
				racquet.keyPressed(e);
			}
		});
		setFocusable(true);
	}
	
	private void move() {
		ball.move();
		racquet.move();
	}

	@Override
	public void paint(Graphics g) {
		super.paint(g);
		Graphics2D g2d = (Graphics2D) g;
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		ball.paint(g2d);
		racquet.paint(g2d);
	}
	
	public void gameOver() {
		JOptionPane.showMessageDialog(this, "Game Over", "Game Over", JOptionPane.YES_NO_OPTION);
		System.exit(ABORT);
	}

	public static void main(String[] args) throws InterruptedException {
		JFrame frame = new JFrame("Mini Tennis");
		Game game = new Game();
		frame.add(game);
		frame.setSize(300, 400);
		frame.setVisible(true);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		while (true) {
			game.move();
			game.repaint();
			Thread.sleep(10);
		}
	}
}

El método gameOver() lanza un popup usando JOptionPane.showMessageDialog con el mensaje "Game Over" y un solo botón "Aceptar". Después del popup, System.exit(ABORT) hace que se termine el programa. El método gameOver() es público ya que será llamado desde el sprite Ball cuando detecte que ha llegado al límite inferior del lienzo.

Colisión de Sprites

Para detectar la colisión entre la pelota y la raqueta usaremos rectángulos. El caso de la pelota crearemos un cuadrado alrededor de la pelota como se ve el la figura 2.

La clase java.awt.Rectangle tiene un método intersects(Rectangle r) que retorna true cuando dos rectángulos ocupan el mismo espacio como en el caso de la figura 3 o 4. Cabe destacar que este método no es exacto ya que en la figura 4 la pelota no toca a la raqueta pero para nuestro ejemplo será más que suficiente.

A continuación vemos la clase Racquet donde el único cambio funcional es que se ha agregado el método getBounds() que retorna un objeto de tipo rectángulo indicando la posición de la raqueta. Este método será usado por el sprite Ball para saber la posición de la raqueta y así detectar la colisión.

package com.edu4java.minitennis6;

import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;

public class Racquet {
	private static final int Y = 330;
	private static final int WITH = 60;
	private static final int HEIGHT = 10;
	int x = 0;
	int xa = 0;
	private Game game;

	public Racquet(Game game) {
		this.game = game;
	}

	public void move() {
		if (x + xa > 0 && x + xa < game.getWidth() - WITH)
			x = x + xa;
	}

	public void paint(Graphics2D g) {
		g.fillRect(x, Y, WITH, HEIGHT);
	}

	public void keyReleased(KeyEvent e) {
		xa = 0;
	}

	public void keyPressed(KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_LEFT)
			xa = -1;
		if (e.getKeyCode() == KeyEvent.VK_RIGHT)
			xa = 1;
	}

	public Rectangle getBounds() {
		return new Rectangle(x, Y, WITH, HEIGHT);
	}

	public int getTopY() {
		return Y - HEIGHT;
	}
}

Otro cambio que funcionalmente no afecta pero que es una buena práctica de programación es la inclusión de constantes:

	private static final int Y = 330;
	private static final int WITH = 60;
	private static final int HEIGH = 20;

Como antes mencionamos el valor de posición "y" estaba fijo en 330. Este valor es usado tanto en el método paint como en getBounds. Si queremos cambiarlo ahora sólo tenemos que cambiarlo en un sólo lugar evitando el posible error que se produciría si lo cambiáramos en un método y en otro no.

La forma de definir una constante en java es declarando una propiedad "static final" y en mayúsculas. El compilador permite usar minúsculas pero el estándar dice que se deben usar mayúsculas para los nombres de las constantes.

Por último la clase Ball:

package com.edu4java.minitennis6;

import java.awt.Graphics2D;
import java.awt.Rectangle;

public class Ball {
	private static final int DIAMETER = 30;
	int x = 0;
	int y = 0;
	int xa = 1;
	int ya = 1;
	private Game game;

	public Ball(Game game) {
		this.game= game;
	}

	void move() {
		if (x + xa < 0)
			xa = 1;
		if (x + xa > game.getWidth() - DIAMETER)
			xa = -1;
		if (y + ya < 0)
			ya = 1;
		if (y + ya > game.getHeight() - DIAMETER)
			game.gameOver();
		if (collision()){
			ya = -1;
			y = game.racquet.getTopY() - DIAMETER;
		}
		x = x + xa;
		y = y + ya;
	}

	private boolean collision() {
		return game.racquet.getBounds().intersects(getBounds());
	}

	public void paint(Graphics2D g) {
		g.fillOval(x, y, DIAMETER, DIAMETER);
	}
	
	public Rectangle getBounds() {
		return new Rectangle(x, y, DIAMETER, DIAMETER);
	}
}

De forma similar a la clase Racquet se ha incluido el método getBounds() y la constante DIAMETER.

Más interesante es la aparición de un nuevo método llamado collision() que retorna true (verdadero) si el rectángulo ocupado por la raqueta "game.racquet.getBounds()" intersecta al rectángulo que encierra a la pelota "getBounds()".

	private boolean collision() {
		return game.racquet.getBounds().intersects(getBounds());
	}

Si la colisión se produce, además de cambiar la dirección ajustaremos la posición de la pelota. Si la colisión es por el lado (figura 1), la pelota podría estar varios pixeles por debajo de la cara superior de la raqueta. En el siguiente game loop aunque la pelota se movería hacia arriba (figura 2) podría todavía estar en colisión con la raqueta.

Para evitar esto colocamos a la pelota sobre la raqueta (figura 3) mediante:

 	y = game.racquet.getTopY() - DIAMETER;

El método getTopY() de Racquet nos da la posición en el eje y de la parte superior de la raqueta y restando DIAMETER conseguimos la posición y exacta donde colocar la pelota para que esté sobre la raqueta.

 

Por último es el método move() de la clase Ball el que usa los nuevos métodos collision() y gameOver() de la clase Game. El rebote al alcanzar el lÍmite inferior ha sido reemplazado por una llamada a game.gameOver().

		if (y + ya > game.getHeight() - DIAMETER)
			game.gameOver();

Y poniendo un nuevo condicional usando el método collision() logramos que la pelota rebote hacia arriba si esta colisiona con la raqueta:

		if (collision())
			ya = -1;
     

Si ejecutamos el ejemplo podemos ver:

<< Anterior Siguiente >>