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.

Juego de plataformas en Java (4): Acciones

En el articulo anterior incorporamos a nuestro personaje en el juego de manera muy básica solo actualiza las imagen del sprite para dar la apariencia que esta corriendo.

Incluiremos una nueva clase en el juego que permite realizar acciones antes los eventos producidos con el teclado.

Para ello dentro de la clase RunnerOne incorporaremos una clase interna llamada GameKeys que sera como la siguiente:

    public class GameKeys extends KeyAdapter {

		public void keyPressed(KeyEvent e) {

			if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
				System.exit(0);
			}
			
		}
    }

Basicamente creamos una clase que extiende de KeyAdapter, para no tener que definir todos los métodos de la interface KeyListener. El metodo keyPressed() se ejecutara cada vez que presionamos una tecla.

En el bloque de código anterior en principio solo atenderemos al evento que propio de la tecla ESCAPE, al presionarla saldremos del juego.

Será necesario también instanciar la clase GameKeys y agregarla como KeyListener a la clase principal, entonces en el constructor agregamos la siguiente linea:

public RunnerOne() {
  /* resto del codigo */
  this.addKeyListener(new GameKeys());
}

Si compilamos y ejecutamos el juego, al presionar la tecla escape saldremos del juego.

Pausar el Juego

Agregaremos algo de código para poder pausar el juego, comenzaremos definiendo una variable global del tipo boolean llamada pause en RunnerOne:

private boolean pause = false;

Modificaremos GameKeys agregamos el siguiente bloque:

			if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                System.exit(0);
            } else if(e.getKeyCode() ==  KeyEvent.VK_P) {
                pause = !pause;
			}

Por ultimo en el método updateGame() incorporaremos esta linea al comienzo:

    public void updateGame() {
        if(pause) {
            return;
        }
   /* resto del codigo */
}

Lo que hace es al pulsar la tecla P cambia el valor de pause a true (se pausa el juego) y cuando se invoca a updateGame() simplemente devuelve el método sin ejecutar las instrucciones posteriores.

Correr y detenerse

Hasta ahora nuestro personaje no respondía a ninguna evento del teclado, vamos a cambiar eso; solo se va a desplazar cuando presionemos las teclas W o la fecha derecha, al soltarlo se va a detener.

Para ello agregaremos un par de métodos a la clase Player:

    public int getState() {
        return state;
    }

    public void idle() {
        state = Player.STATE_IDLE;
    }

    public void run() {
        state = Player.STATE_RUN;
    }

Básicamente son para determinar cual es el estado actual de nuestro personaje y para cambiar el estado (run o idle).

Incorporaremos algunos cambios en la clase GameKeys:

    public class GameKeys extends KeyAdapter {

		public void keyPressed(KeyEvent e) {
            
			if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                System.exit(0);
            } else if(e.getKeyCode() ==  KeyEvent.VK_P) {
                pause = !pause;

            /*
             * RIGHT
             */
            } else if(e.getKeyCode() ==  KeyEvent.VK_RIGHT) {
                player.run();
            } else if(e.getKeyCode() ==  KeyEvent.VK_D) {
                player.run();
			}
			
        }
        
        public void keyReleased(KeyEvent e) {

            /*
             * RIGHT
             */            
            if(e.getKeyCode() ==  KeyEvent.VK_RIGHT) {
                player.idle();
            } else if(e.getKeyCode() ==  KeyEvent.VK_D) {
                player.idle();
            }
        }
    }

Como se puede ver, definimos las acciones que se van a tomar cuando se libere la tecla presionada, en este caso al soltar las teclas VK_RIGHT (flecha derecha) y D, se cambia el estado del player a IDLE.

Modificaremos el metodo updateGame() para que actualice el fondo solo si el estado del player es IDLE:

        if(player.getState() != Player.STATE_IDLE) {
            background.updateBackground();
        }

Cuando lo ejecutamos el personaje solo se va a mover mientras se mantenga presionada las teclas W o RIGHT.

El gif puede demorar en cargar.

Jump!

Una acción fundamental en casi todos los juego de plataformas es que nuestro personaje pueda saltar.

Primero debemos definir dentro de GameKeys.keyPressed() la acción:

            /*
             * JUMP
             */
            } else if(e.getKeyCode() ==  KeyEvent.VK_UP) {
                player.jump();

Será necesario implementar algunos cambios en Player, comenzando por definir el método jump():

Nuestro personaje al saltar no solo debe cambiar la imagen que se utiliza para graficarlo, sino que también debemos ir modificando el valor de position.y para que cambie su ubicación.

Definiremos una variable llamada jumpDistance, que sera la responsable de determinar cuan «alto» saltara nuestro personaje. También definiremos una variable llamada baseLine, que mantendrá el valor original de position.y, sera el limite inferior o el «piso».


    public static int STATE_IDLE = 2;
    public static int STATE_JUMP = 3;
    public static int STATE_FALL = 4;

    private int jumpDistance = height;
    private int baseLine = 0;
    private int previousState = 0;
    

Antes de continuar debemos hacer un breve análisis de como sera el comportamiento de nuestro personaje respecto a sus estados, en que momento podrá cambiar entre ellos:

Esto nos sirve de guía para definir algunos métodos nuevos en Player y modificar otros:

    public void idle() {
        if(state == Player.STATE_RUN) {
            state = Player.STATE_IDLE;
        }
    }

    public void run() {
        if(state == Player.STATE_IDLE) {
            state = Player.STATE_RUN;
        }
    }


	public void jump() {
        if((state == Player.STATE_RUN) || (state == Player.STATE_IDLE)) {
            previousState = state;
            state = Player.STATE_JUMP;
            baseLine = position.y;
        }
    }
    
    public void fall() {
        state = Player.STATE_FALL;
    }

El método run() solo podrá ser invocado cuando el estado de nuestro personaje este IDLE, ocurre cuando se genera el evento Release en GameKeys.

En jump() si es valida la condición del estado, almacenamos el estado previo (si estaba corriendo que siga corriendo), luego cambiamos el estado de Player y almacenamos la posición en y actual; serla el punto al que volveremos si no hay ningún obstáculo delante; esto lo veremos en un articulo más adelante.

Modificamos el método updatePlayer() paa que quede de la siguiente manera:

    public void updatePlayer() {
        if(state == STATE_RUN) {
            if(imageReference < ((imageRun.getWidth()/width)-1)) {
                imageReference++;
            } else {
                imageReference = 0;
            }

        } else if(state == STATE_JUMP) {
            if(position.y > (baseLine-jumpDistance)) {
                position.y = position.y - 2;
            } else {
                fall();
            }

        } else if(state == STATE_FALL) {
            if(position.y < baseLine) {
                position.y = position.y + 2;
            } else {
                state = previousState;
            }
            

        } else if(state == STATE_IDLE) {
            if(imageReference < ((imageRun.getWidth()/width)-2)) {
                imageReference++;
            } else {
                imageReference = 0;
            }
        }
    }

Cuando el estado es JUMP disminuiremos position.y tanto como sea el valor de jumpDistance; una vez que sea igual o menor, invocamos al método fall(); es decir, cuando ya alcanzo el punto máximo del salto, debe comenzar a caer.

En el estado FALL hacemos lo opuesto a JUMP, aumentamos el valor de position.y hasta que sea igual a baseLine (el valor y original antes del salto) y volvemos al estado previo.

Por ultimo nos queda modificar el método: getImagePlayer() para que devuelva las imágenes de JUMP y FALL:

    public BufferedImage getImagePlayer() {
        
        if(state == STATE_RUN) {   
            int x = imageReference*width;
            return imageRun.getSubimage(x, 0,width,height);
        } else if(state == STATE_JUMP) {   
            return imageJump;
        } else if(state == STATE_FALL) {   
            return imageFall;
        } else {
            //IDLE
            int x = imageReference*width;
            return imageIdle.getSubimage(x, 0,width,height);
        }
    }

Si compilamos y ejecutamos, cada vez que presionemos la tecla W o UP tendremos:

El gif puede demorar en cargar.

Si ven que parece que faltan frames o se pierde la imagen del salto, es que de verdad ocurre; una deuda técnica que dejamos por ahora.