Juego de plataformas en Java (10): Trampas

Lamentablemente para nuestro personaje tenemos que agregar algunas trampas en el camino, para que no sea tan sencillo. Comenzaremos incorporando fuego.

Como existen diferentes tipos de trampas disponibles en la galería de imágenes, y quiero mantener la estructura del juego mas o menos similar; crearemos una clase llamada Trap que implementa Element de la siguiente forma:

package gsampallo.traps;

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

import gsampallo.Element;

public class Trap implements Element {

    public static int FIRE = 0;

    protected int width;
    protected int height;

    protected int trapType = 0;
    protected Point position;
    protected boolean visible = true;

    protected boolean on = true;


    public Trap(int type,Point initialPoint) {
        this.trapType = type;
        this.position = initialPoint;
    }

    /**
     * @return the trapType
     */
    public int getTrapType() {
        return trapType;
    }

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

    @Override
    public int getY() {
        return position.y;
    }

    @Override
    public int getWidth() {
        return width;
    }

    @Override
    public int getHeight() {
        return height;
    }

    @Override
    public boolean isVisible() {
        return visible;
    }

    @Override
    public BufferedImage getImage() {
        // TODO Auto-generated method stub
        return null;
    }

    public void updateTrap(boolean move) {
        return;
    }

    public boolean isOn() {
        return on;
    }

    public void setOn(boolean isOn) {
        this.on = isOn;
    }

}

Incorporamos una variable boolean llamada on; nos permite saber si la trampa esta activa o no; a partir de ello podremos determinar que imagen devolveremos y si hace daño al jugador o no.

Cada tipo de trampa extenderá de la clase Trap, especializándose en la forma en que trabaja. Crearemos la clase Fire:

package gsampallo.traps;

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

import javax.imageio.ImageIO;

public class Fire extends Trap {

    private boolean on = true;

    public Fire(int type, Point initialPoint) {
        super(type, initialPoint);

        this.width = 16;
        this.height = 32;

        loadImages();

    }

    private BufferedImage imageOff;
    private BufferedImage imageOn;

    private void loadImages() {

        try {

            imageOff = ImageIO.read(new File("image/Traps/Fire/Off.png"));
            imageOn = ImageIO.read(new File("image/Traps/Fire/On (16x32).png"));

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

Hasta acá es más o menos lo que veníamos trabajando, cambiamos el nombre del package, en esta oportunidad esta dentro de traps; de esa forma podemos comenzar a agrupar las clases y organizamos un poco.

El metodo updateTrap(boolean) debe no solo actualizar el indice de la imagen que se va a devolver, para el caso que este encendido, sino que también debe llevar un segundo ciclo para saber cuanto tiempo mostraremos la trampa apagada.

    private int imageNumber = 0;
    private int firePeriodOn = 6;
    private int firePeriodOff = 9;
    private int firePeriodNumber = 0;

    public void updateTrap(boolean move) {

        if(on) {

            if(firePeriodNumber < firePeriodOn) {
                if(imageNumber < (imageOn.getWidth()/width)-1) {
                    imageNumber++;
                } else {
                    imageNumber = 0;
                    firePeriodNumber++;
                }
            } else {
                on = false;
                imageNumber = 0;
                firePeriodNumber = 0;
            }


        } else {

            if(imageNumber < firePeriodOff) {
                imageNumber++;
            } else {
                
                //Cicle complete, change period number
                firePeriodNumber = 0;
                imageNumber = 0;
                on = true;

            }

        }

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

    }

    public BufferedImage getImage() {
        if(on) {
            int x = imageNumber*width;
            return imageOn.getSubimage(x,0,width,height);
        } else {
            return imageOff;
        }
    }

Modificando las variables firePeriodOn y firePeriodOff; podemos alterar el tiempo que se encuentran encendidos o apagadas cada una de las trampas. Incorporaremos dos métodos para poder hacerlo:

    public void setPeriodFireOn(int period) {
        this.firePeriodOn = period;
    }
    
    public void setPeriodFireOff(int period) {
        this.firePeriodOff = period;
    }

De igual manera que lo hicimos con las Box y Fruit, crearemos un arrayList para poder llevar el control de las Traps:

private ArrayList<Trap> listTraps;

public RunnerOne() {
  /** resto del codigo **/
  Fire fire = new Fire(Trap.FIRE,new Point(270,410));

  listTraps = new ArrayList<Trap>();
  listTraps.add(fire);  

}

Modificaremos el método paint() de la misma manera:

public void paint(Graphics g) {

  /** resto del codigo **/
  drawList(listTraps, g);

}

Incorporamos el bloque de código siguiente en updateGame() para actualizar nuestras trampas:

public void updateGame() {
  /** resto del codigo **/
  if(!listTraps.isEmpty()) {
    Iterator it = listTraps.iterator();
    while(it.hasNext()) {
      Trap trap = (Trap)it.next();

      trap.updateTrap(moved);
      if(!trap.isVisible()) {        
        it.remove();
      }
    }
  }
}

Si compilamos y ejecutamos obtendremos el siguiente resultado:

<insertar gif>

Nos queda incorporar el daño que causa las trampas a nuestro jugador, esto lo realizaremos más adelante.

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.