Juego de plataformas en Java (5): Frutas

Debemos ir completando el juego con otros elementos ademas de nuestro personaje; para ello incorporaremos las frutas, que el personaje podrá ir recolectando para sumar puntos.

La idea es crear una clase genérica Fruit, que a partir de un parámetro en el constructor instance una u otra fruta. También crearemos una interface llamada Element, que nos servirá para determinar si el jugador pudo recolectar las frutas o no, y que utilizaremos más adelante en el juego también:

package gsampallo;

public interface Element {
    public int getX();
    public int getY();
    public int getWidth();
    public int getHeight();
}

Creamos también una clase llamada Fruit que implementara la interface Element:

package gsampallo;

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

import javax.imageio.ImageIO;

public class Fruit implements Element {

    public static int APPLE = 0;
    public static int BANANAS = 1;
    public static int CHERRYS = 2;
    public static int KIWI = 3;
    public static int MELON = 4;
    public static int ORANGE = 5;
    public static int PINEAPPLE = 6;
    public static int STRAWBERRY = 7;

    private String[] imagesPath = {
        "image/Items/Fruits/Apple.png",
        "image/Items/Fruits/Bananas.png",
        "image/Items/Fruits/Cherries.png",
        "image/Items/Fruits/Kiwi.png",
        "image/Items/Fruits/Melon.png",
        "image/Items/Fruits/Orange.png",
        "image/Items/Fruits/Pineapple.png",
        "image/Items/Fruits/Strawberry.png"

    };

    private int width = 32;
    private int height = 32;

    private int fruitNumber;
    private Point position;

    private int creditsValue = 1;

    public Fruit(int fruitNumber,Point initialPosition) {
        this.fruitNumber = fruitNumber;
        this.position = initialPosition;

        this.creditsValue = fruitNumber + 1;

        loadImages();
    }

    private BufferedImage imageFruit;
    private BufferedImage imageCollected;

    private void loadImages() {
        try {

            imageFruit = ImageIO.read(new File(imagesPath[this.fruitNumber]));
            imageCollected = ImageIO.read(new File("image/Items/Fruits/Collected.png"));

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

Como indicamos que implementamos la interface Element, debemos definir los métodos especificados en dicha interfaz:

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

Si vemos las imágenes que utilizamos, más allá de las imágenes de cada fruta, también utilizaremos una llamada Collected.png; será la que invocaremos cuando el personaje recolecte a la fruta.

Teniendo esto en consideración incorporamos lo siguiente en la clase Fruit:

    private boolean collected = false;
    private boolean visible = true;

    public void setCollected(boolean collected) {
        this.collected = collected;
        this.imageNumber = 0;
    }

    public boolean isCollected() {
        return collected;
    }

    public boolean isVisible(){
        return visible;
    }

Cada Fruit comienza estando visible y teniendo en false collected; aún no fue recolectada; cuando el jugador la recolecta cambia el estado de collected a true; se debe mostrar las imágenes correspondientes al sprite de collected y deja de ser visible en el juego; visible pasa a ser false.

Con esto en mente podemos definir el método updateFruit():

    public void updateFruit() {
        if(collected) {
            if(imageNumber < (imageCollected.getWidth()/width)-1) {
                imageNumber++;
            } else {
                visible = false;
            }
        } else {
            
            if(imageNumber < (imageFruit.getWidth()/width)-1) {
                imageNumber++;
            } else {
                imageNumber = 0;
            }            
        }  
    }

Este método actualiza el parámetro requerido para formar el sprite. También determina si la fruta sigue siendo visible, luego que se haya mostrado todas las imágenes del sprite correspondiente.

También necesitaremos definir el método getImageFruit(), en función de si la fruta fue recolectada o no devuelve la imagen correspondiente:

    public BufferedImage getImageFruit() {
        int x = imageNumber*width;
        if(collected) {
            return imageCollected.getSubimage(x, 0, width,height);
        } else {
            return imageFruit.getSubimage(x, 0, width,height);
        }
    }

¿Como incorporamos Fruit al juego?

En este punto tenemos la clase Fruit casi terminada, pero ya podríamos ir viendo como la integramos en el juego.

Es claro que van a existir múltiples frutas presentes en cada nivel y que estarán dispersas en el mapa; para llevar la gestión utilizaremos un ArrayList como estructura de datos, donde iremos agregando y quitando las frutas según sea necesario.

Definiremos un ArrayList llamado listFruit:

ArrayList<Fruit> listFruit;

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

  Fruit fruit = new Fruit(Fruit.BANANAS,new Point(150,410));

  listFruit = new ArrayList<Fruit>();
  listFruit.add(fruit);

  /** resto del codigo */
}

El ArrayList que definimos solo aceptara objetos del tipo Fruit, en el constructor instanciamos una fruta del tipo BANANA y le indicamos cual sera su punto inicial en el plano. Luego instanciamos listFruit y agregamos la fruta a la lista.

Aunque es una metodología estática, nos permite probar los cambios que realizamos, más adelante necesitaremos algún procedimiento que cargue los datos del mapa en forma dinámica.

Será necesario modificar el método updateGame() para que actualice los parámetros de las frutas:

public void updateGame() {
  /** resto del codigo **/

  if(!listFruit.isEmpty()) {
    Iterator it = listFruit.iterator();
    while(it.hasNext()) {

      Fruit fruit = (Fruit)it.next();

      fruit.updateFruit();
      if(!fruit.isVisible()) {
        it.remove();
      }
    }
  }  

}

La tarea que se realice es recorrer el listado de frutas, por medio de un Iterator, actualizar la fruta; si la fruta no es visible se la remueve de la lista.

Será necesario también modificar el método paint() para que dibuje las frutas:

public void paint(Graphics g) {
  /** resto del codigo **/
  if(!listFruit.isEmpty()) {
    Iterator it = listFruit.iterator();
    while(it.hasNext()) {
      Fruit fruit = (Fruit)it.next();   
      if(fruit.isVisible())  {
        g.drawImage(fruit.getImageFruit(),fruit.getX(),fruit.getY(), null);
      }
    }
  }

  /** dibujamos el player **/
}

Si no hay mayores inconvenientes cuando ejecutemos nuestro programa debemos tener lo siguiente:

<insertar gif>

Nuestro personaje parece nunca alcanzar la fruta; esto es porque siempre la dibujamos en el mismo punto; debemos cambiar position.x de la fruta siempre que modifiquemos el fondo; para ello debemos hacer algunos cambios en updateFruit():

public void updateFruit(boolean move) {

  /** resto del codigo **/
  
  if(move) {
    position.x--;
    visible = (position.x > 0);
  }

}

Recibirá como parámetro una variable del tipo boolean, que le indica si corresponde que se mueve o no; puede que nuestro personaje este IDLE y no necesitemos desplazar la fruta; si es true; entonces disminuye position.x en 1 y evalúa que sea mayor a cero, en caso de ser falso, tampoco debería ser visible.

En updateGame() debemos incorporar el siguiente código:

public void updateGame() {
  /** resto del codigo **/

  boolean moved = (player.getState() != Player.STATE_IDLE);
  if(!listFruit.isEmpty()) {
    Iterator it = listFruit.iterator();
    while(it.hasNext()) {

      Fruit fruit = (Fruit)it.next();

      fruit.updateFruit(moved);
      if(!fruit.isVisible()) {
        it.remove();
      }
    }
  } 
}

Si compilamos y ejecutamos, se crea el efecto de que la fruta se acerca a nuestro héroe cuando el mismo esta corriendo o saltando; es decir cuando avanza. Al deternerse la fruta queda en el lugar; pero siempre se actualiza su imagen.

Recolectar la fruta

Todavía nos queda realizar algunos por realizar, debemos lograr que cuando el personaje recolecte la fruta esta cambie de estado.

Necesitamos implementar un método que nos indique si Player colisiona con la fruta; es decir, si en algún momento se superponen las imágenes.

Por el momento solo nos vamos a concentrar en que ocurre si el Player avanza sobre la fruta, y no tendremos en cuenta el eje y.

Sabemos que la imagen del jugador esta definida dentro de los puntos (px,py) y (px+w,py+h); siendo w el ancho (width) y h el alto (height). La fruta esta definida a partir de los puntos (fx,fy).

Entonces cuando (px+w) sea mayor a fx; nuestro personaje habrá avanzado sobre la fruta y debemos recolectarla.

Con esta condición en mente, y sabiendo que implementamos la interface Element en Fruit, podemos definir el siguiente método de manera genérica para que nos sirva para otros elementos:

	private boolean isHorizontalColision(Element elA,Element elB,int tolerance) {
		boolean isColision = false;

		if((elA.getX()+elA.getWidth()+tolerance) > elB.getX()) {
			isColision = true;
		}

		return isColision;
	}

isHorizontalColision() es un método que retornara true cuando los dos Element que recibe como parámetro cumplan la condición que establecimos, incorporamos un tercer parámetro que se llama tolerance; es la cantidad de pixeles de superposición que puede existir. Esto es por si la imagen es transparente y necesitamos que avance algunos pixeles para crear el efecto de que la recolecta.

Para que esto funcione debemos implementar la interface Element en Player.

Por ultimo necesitamos realizar algunos cambios en updateGame():

public void updateGame() {
  /** resto del codigo **/

  boolean moved = (player.getState() != Player.STATE_IDLE);
  if(!listFruit.isEmpty()) {
    Iterator it = listFruit.iterator();
    while(it.hasNext()) {

      Fruit fruit = (Fruit)it.next();
      if(isHorizontalColision(player,fruit,-12)) {
         if(!fruit.isCollected()) {
           fruit.setCollected(true);
         }
      }
      fruit.updateFruit(moved);
      if(!fruit.isVisible()) {
        it.remove();
      }
    }
  } 
}

Si existe una colisión entre el player y la fruta, y la fruta aún no fue recolectada, cambiamos el estado de la fruta a recolectada.

Queda para un próximo cambio, incorporar el esquema de puntos.

https://github.com/gsampallo/runnerone

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *