Juego de plataforma en Java (8): Un poco de optimización

Es momento de revisar un poco el código que tenemos escrito y reordenar un poco, hacerlo un poco más «elegante».

Si vemos en el método paint(), tenemos tres bloques de código que son idénticas; es la iteración que recorren las tres listas (box,weapon y fruit) para ir dibujando en la ventana, si recordamos sabemos que las clases Box, Weapon y Fruit implementan la interface Element; es decir que con muy poquitos cambios podemos mejorar.

En la interface Element incorporaremos dos metodos:

public interface Element {
  /** resto del codigo **/
    public boolean isVisible();

    public BufferedImage getImage();
}

Será necesario renombrar los métodos que devuelven BufferedImage de las clases: Box, Weapon, Fruit y tambien Player, puesto que implementa la interface.

En Player debemos implementar isVisible(),solo hacemos que devuelva true:

    public boolean isVisible() {
        return true;
    }

Dentro de RunnerOne definimos un nuevo método:

    private void drawList(ArrayList itemList,Graphics g) {
        if(!itemList.isEmpty()) {
            Iterator it = itemList.iterator();
            while(it.hasNext()) {
                Element el = (Element)it.next();   
                if(el.isVisible())  {
                    g.drawImage(el.getImage(),el.getX(),el.getY(), null);
                }
            }
        }        
    }

Si vemos el bloque de código que ejecuta, es similar a lo que tenemos escrito en paint(), cambia el hecho que en lugar de castear el objeto a un Box, Fruit o Weapon lo hacemos a Element; el resto es idéntico.

Por último modificamos paint() para invocar al método drawList() por cada una de las listas que tenemos:

public void paint(Graphics g) {
  /** resto del codigo **/

  drawList(listFruit, g); 
  drawList(listBox, g);
  drawList(listWeapon, g);

  /** resto del codigo **/

}

De esta forma hacemos uso de la interfaz para reutilizar codigo.

Juego de plataformas en Java (7): Destruir las cajas

Nuestro juego ya tiene un fondo, un personaje (que podemos cambiar), frutas para sumar puntos y cajas.

A continuación incorporaremos una nueva acción a nuestro héroe, podrá disparar afiladas sierras para abrirse paso.

El gif puede demorar en cargar.

Comenzaremos definiendo una clase llamada Weapon que implemente la interfaz de Element:

package gsampallo;

import java.awt.Point;
import java.awt.image.BufferedImage;
import java.io.File;

import javax.imageio.ImageIO;

public class Weapon implements Element {

    public int width = 19;
    public int height = 19;

    private Point position;

    public Weapon(Point initialPosition) {

        this.position = initialPosition;

        loadImages();
    }

    private BufferedImage imageWeapon;

    private void loadImages() {

        try {

            imageWeapon = ImageIO.read(new File("image/Traps/Saw/On (19x19).png"));

        } catch (Exception e) {
            System.err.println("No se pudieron cargar imagenes de Box");
            System.err.println(e.getMessage());
        }
    }

Como se implementa la interfaz Element, debemos definir los siguientes métodos:

    public int getX() {
        return this.position.x;
    }

    public int getY() {
        return this.position.y;
    }
    public int getWidth() {
        return this.width;
    }
    
    public int getHeight() {
        return this.height;
    }
     
    private boolean visible = true;

    public boolean isVisible(){
        return visible;
    }

    public void setVisible(boolean visible) {
      this.visible = visible;
    } 

De paso definimos isVisible() para determinar si es visible o no.

También debemos definir el método updateWeapon() para poder actualizar los parámetros y getImage() para obtener la imagen que será dibujada:


    private int imageNumber = 0;

    public void updateWeapon(boolean move) {

        if(imageNumber < (imageWeapon.getWidth()/width)-1) {
            imageNumber++;
        } else {
            imageNumber=0;
        }        
        
        position.x = position.x + 3;
        visible = visible && (position.x < RunnerOne.FRAME_WIDTH);
        
    }

    public BufferedImage getImage() {
        int x = imageNumber*width;
        return imageWeapon.getSubimage(x, 0, this.width,this.height);
    }

En esta ocasión el disparo siempre actualizara su posición; en lugar de disminuir en 1 a position.x, que lo acercaría al limite izquierda de la ventana, sumamos 3 de manera que se desplace hacia la derecha a mayor velocidad.

Al igual que con las frutas y las cajas, utilizaremos un ArrayList para mantener la lista de disparos activos; estos a diferencia de las anteriores serán generados por un evento del jugador: cuando presione la tecla SPACE.

Incorporamos en RunnerOne entonces:

private ArrayList<Weapon> listWeapon;

public RunnerOne() {
  /** resto del codigo **/
  listWeapon = new ArrayList<Weapon>();
}

Definiremos un método llamado shootWeapons() que será invocado cuando se presione la tecla espacio; de esa manera incorporamos:

    public void shootWeapons() {
        Weapon weapon = new Weapon(new Point(player.getX(),player.getY()+6));
        listWeapon.add(weapon);
    }

Toma el punto (x,y+6) como punto de partida para dibujar la sierra en el plano; luego lo incorpora a la lista.

En GameKeys.keyPressed( ) incorporamos:

public class GameKeys extends KeyAdapter {

  public void keyPressed(KeyEvent e) {

    /** resto del codigo **/
    } else if(e.getKeyCode() == KeyEvent.VK_SPACE) {
      shootWeapons();
    }

  }
}

Debemos actualizar el método updateGame() para que recorra la lista de weapons y actualice el estado de cada una:

public void updateGame() {

  /** resto del codigo **/
  if(!listWeapon.isEmpty()) {
    Iterator it = listWeapon.iterator();
    while(it.hasNext()) {
      Weapon weapon = (Weapon)it.next();
      weapon.updateWeapon(moved);
      if(!weapon.isVisible()) {
        it.remove();
      }
    }
  }  

}

También debemos actualizar es paint(Graphics g) para que dibuje los disparos:

public void paint(Graphics g) {

  /** resto del codigo **/
  if(!listWeapon.isEmpty()) {
    Iterator it = listWeapon.iterator();
    while(it.hasNext()) {
      Weapon weapon = (Weapon)it.next();   
      if(weapon.isVisible())  {
        g.drawImage(weapon.getImage(),weapon.getX(),weapon.getY(),null);
      }
    }
  } 
}

Si en este punto ejecutamos el juego, al presionar la tecla espacio se efectuaran los disparos:

El gif puede demorar en cargar.

Debemos determinar el comportamiento que tendrá la sierra ante los objetos con los que colisione, por el momento solo tenemos dos: cajas (de tres tipos) y las frutas; con las frutas no tendrá ningún efecto; pero las cajas, dependiendo su tipo las destruirá.

Debemos modificar el método updateGame(), en el bloque de código que actualiza los weapons, para que recorra la lista de cajas y las actualice según corresponda, para ello debemos hacer algunos cambios:

public void updateGame() {

  /** resto del codigo **/
  if(!listWeapon.isEmpty()) {
    Iterator it = listWeapon.iterator();
    while(it.hasNext()) {
      Weapon weapon = (Weapon)it.next();
      weapon.updateWeapon(moved);
      if(weapon.isVisible()) {

	  if(!listBox.isEmpty()) {
            Iterator<Box> it1 = listBox.iterator();
	    while(it1.hasNext()) {
              Box box = (Box)it1.next();
              if(isHorizontalColision(weapon, box, 0)){
                box.setBreak();
		weapon.setVisible(false);
		break;
	      }
	    }
          }

      } else {
        it.remove();
      }
    }
  }  

}

Recorremos la lista de cajas, si se cumple la condición que que existe una colisión horizontal (por ahora no vamos a complicarnos con el otro eje), entonces indicamos que la caja se rompe, weapon ya no es visible y salimos del bucle.

El gif puede demorar en cargar.

La ultima caja, la del tipo BOX3, quiero que sea a prueba de disparos; modificaremos el método setBreak() de la clase Box:

public void setBreak() {
  isBreak = (this.boxNumber<BOX3);
}

Con este cambio solo se romperán las cajas de tipo BOX1 y BOX2.