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.

Juego de plataformas en Java (6): Cajas

Necesitamos comenzar a definir algunos obstáculos en el camino de nuestro personaje; utilizaremos algunas cajas para ayudar a crear también los mapas de los niveles.

La imagen puede demorar en cargar.

De forma similar a como lo realizamos con Fruit, creamos una clase llamada Box:

package gsampallo;

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

import javax.imageio.ImageIO;

public class Box implements Element {

    public static int BOX1 = 0;
    public static int BOX2 = 1;
    public static int BOX3 = 2;

    public int width = 28;
    public int height = 24;

    private int boxNumber;
    private Point position;

    private boolean isBreak = false;

    public Box(int boxNumber,Point initialPosition) {
        this.boxNumber = boxNumber;
        this.position = initialPosition;

        loadImages();
    }

    private BufferedImage imageBoxIdle;
    private BufferedImage imageBoxBreak;

    private void loadImages() {

        String[] imagesPath = {
            "image/Items/Boxes/Box1/",
            "image/Items/Boxes/Box2/",
            "image/Items/Boxes/Box3/"
        };

        String pathIdle = imagesPath[this.boxNumber]+"Idle.png";
        String pathBreak = imagesPath[this.boxNumber]+"Break.png";

        try {

            imageBoxIdle = ImageIO.read(new File(pathIdle));
            imageBoxBreak = ImageIO.read(new File(pathBreak));

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

Las cajas trabajan de manera similar a Fruit, la diferencia es que no son recolectadas y presentan un obstáculo para nuestro jugador. Si, en cambio, pueden ser destruidas, pero sobre esto más adelante.

Queda por implementar updateBox(), para que actualice los parámetros de la caja y getImageBox() para obtener la imagen que se utilizara para dibujar:

    private int numberImageBreak = 0;
    public void updateBox(boolean move) {
        if(isBreak) {
            if(numberImageBreak <= (imageBoxBreak.getWidth()/width)) {
                numberImageBreak++;
            } else {
                visible = false;
            }

        } 

        if(move) {
            position.x--;
            visible = (position.x > 0);
        }
    }

    public BufferedImage getBoxImage() {
        if(isBreak) {
            int x = numberImageBreak*width;
            return imageBoxBreak.getSubimage(x, 0, width,height);  
        } else {
            return imageBoxIdle;
        }
    }

Como Box implementa la interface Element, debemos definir los metodos indicados en la misma, aprovechamos y definimos los necesarios para poder establecer que la caja se rompio y que es visible.

    private boolean visible = true;
    
    public boolean isVisible() {
        return visible;
    }

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

    public boolean isBreak() {
        return isBreak;
    }

    public void setBreak() {
        isBreak = true;
    }

También debemos crear una lista en RunnerOne para almacenar las cajas, de la misma manera que lo hicimos con Fruit; entonces agregamos las siguientes lineas en el constructor de RunnerOne:

private ArrayList<Box> listBox;

public RunnerOne() {
  /** resto del codigo **/

  Box box = new Box(Box.BOX1,new Point(180,410));
  Box box1 = new Box(Box.BOX2,new Point(210,410));
  Box box2 = new Box(Box.BOX3,new Point(240,420));

  listBox = new ArrayList<Box>();
  
  listBox.add(box);
  listBox.add(box1);
  listBox.add(box2);

}

Mostramos tres cajas, una de cada tipo.

Tendremos que modificar updateGame() para que actualice la lista de cajas:

public void updateGame() {

  /** resto del codigo **/

  if(!listBox.isEmpty()) {
    Iterator it = listBox.iterator();
    while(it.hasNext()) {
      Box box = (Box)it.next();

      box.updateBox(moved);
      if(!box.isVisible()) {
        it.remove();
      }
    }
  }

}

Recordemos que la variable moved la definimos antes de evaluar el bloque de código correspondiente a la lista de Fruit.

También debemos actualizar el método paint():

public void paint(Graphics g) {
  if(!listBox.isEmpty()) {
    Iterator it = listBox.iterator();
    while(it.hasNext()) {
      Box box = (Box)it.next();   
      if(box.isVisible())  {
        g.drawImage(box.getBoxImage(),box.getX(),box.getY(),null);
      }
    }
  }   
}

Cuando compilemos y ejecutemos nos va a mostrar el personaje junto con las tres cajas, una de cada tipo:

En el siguiente articulo veremos como destruir las cajas.