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 plataformas en Java (9): Puntaje

Cuando definimos las frutas que puede recolectar nuestro personaje, nos quedo pendiente el tema del puntaje. Al recoger cada fruta se debe sumar un puntaje, el cual debe mostrarse en algún lugar de la pantalla.

Comenzaremos con lo más básico, modificando lo que tenemos escrito para que llegue cuenta de los puntos; haremos algunos cambios sobre el método updateGame(), particularmente dentro del bloque de código que determina si se recoge la fruta o no. Definimos cuatro variables que nos van a ayudar en el proceso:

private boolean winCredits = false;
private int numberWinCredits = 0;
private int credits = 0;

public void updateGame() {
  /** resto del codigo **/
  if(isHorizontalColision(player,fruit,-12)) {
    if(!fruit.isCollected()) {
      winCredits = true;
      numberWinCredits = fruit.getCreditsValue();
      credits = credits + fruit.getCreditsValue();
      fruit.setCollected(true);
    }
  }

}

La idea es la siguiente: tendremos el puntaje en la esquina superior derecha de la ventana, solo actualizaremos la imagen del puntaje cada vez que se modifique, para ello utilizamos la variable updateCredits; winCredits nos permite determinar si mostramos el efecto de que el jugador gano puntos. Credits acumula la cantidad de puntos total desde que comenzo la partida y numberWinCredits los puntos ganados en ese momento, en función de la fruta recolectada.

Para mostrar el puntaje acumulado, necesitamos crear una clase que construya números desde el sprite de letras/números; esto permite mantener mas o menos la misma lógica que venimos aplicando y no depender de si tenemos instalada alguna fuente o no.

Crearemos una clase llamada Credits:

package gsampallo;

import java.awt.Dimension;
import java.awt.Point;
import javax.imageio.*;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;

public class Credits {

    public int width = 16;
    public int height = 10;    

    public Credits() {
        loadImages();
    }

    private BufferedImage imageCredits;

    private void loadImages() {
        try {

            imageCredits = ImageIO.read(new File("image/Menu/Text/Text (White) (8x10).png"));

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

Hasta este punto la clase Credits no es muy diferente de las que veníamos definiendo, salvo que no implementa la interfaz Element.

Si estudiamos la imagen que carga, vemos que podemos acceder a cada uno de los caracteres de la misma manera que lo hacemos con los sprites de las animaciones.

De modo que si necesitamos el nro. 1580; el eje Y se mantendrá fijo en 3, e iremos modificando x, donde x=1 para 1, x=5 para 5 y asi; nos resulta muy sencillo.

Incorporaremos un nuevo método a Credits, llamado getPlusOne() que nos proveerá de una imagen para el efecto de cuando el jugador recolecta una fruta:

public BufferedImage getPlusOne() {
  return imageCredits.getSubimage(8,30, 8,10);
}

public BufferedImage getCreditsNumber(int nro) {
  return imageCredits.getSubimage(nro*8,30,8,10);
}

Simplemente devuelve una imagen con el símbolo de la suma (+). No será necesario el punto (x,y) puesto que lo haremos relativo a la posición del jugador; para ello modificaremos el método RunnerOne.paint():

private int showCreditNumber = 0;

public void paint(Graphics g) {

  /** resto del codigo **/
  if(winCredits) {
    if(showCreditNumber < 3) {
 g.drawImage(showCredits.getCreditsNumber(numberWinCredits),player.getX()+player.getWidth(),player.getY()-3,null);
	showCreditNumber++;
} else {
  showCreditNumber = 0;
  winCredits = false;
}

}
}

Si compilamos y ejecutamos obtendremos el siguiente efecto:

El gif puede demorar en cargar

Aún nos queda mostrar el puntaje total en la esquina superior izquierda de la pantalla; para ello necesitamos incorporar un nuevo metodo en Credits:

    private BufferedImage imageScore;
    public void updateScore(int score) {
        String strCredits = ""+score;
        char[] listN = strCredits.toCharArray();

        imageScore = new BufferedImage(listN.length*width,height,imageCredits.getType());

        Graphics g = imageScore.createGraphics();
        for(int i=0; i<listN.length; i++) {
            
            int n = ((int)listN[i]) - 48;
            g.drawImage(getCreditsNumber(n),i*width,0,null);
        }        
    }

    public BufferedImage getScore() {     
        return imageScore;
    }

updateScore() toma la cantidad de créditos ganamos, lo convierte a un array de chars desde el cual obtiene cada una de las imágenes de los números, y las une en una nueva imagen; este proceso lo hacemos solamente cuando se modifica el credito. getScore() devuelve la imagen que formo anteriormente.

En el método RunnerOne.updateGame() incorporamos las siguientes lineas:

public void updateGame() {
  /** resto del codigo **/
  if(isHorizontalColision(player,fruit,-12)) {
    if(!fruit.isCollected()) {
      winCredits = true;
      numberWinCredits = fruit.getCreditsValue();
      credits = credits + fruit.getCreditsValue();
      showCredits.updateScore(credits);
      fruit.setCollected(true);
    }
  }

}

También debemos actualizar RunnerOne.paint() para que dibuje el puntaje, nos conviene hacerlo inmediatamente después de graficar el background:

public void paint(Graphics g) {
  /** resto del codigo **/
  g.drawImage(showCredits.getScore(), 20,50,null);
}

No olvidemos incovar a updateScore(0) en el constructor de Credits, puesto que al comienzo del juego la imagen de los creditos será nula.