Juego de plataformas en Java (13): Aún más enemigos

Anteriormente incorporamos a Rino, un rinoceronte que en determinadas circunstancias sale a patrullar una franja del mapa. En esta ocasión integraremos un nuevo enemigo al juego: Trunk. Un bicho, por no encontrar mejor denominación, que en ciertas oportunidades dispara una munición.

De la misma forma que hicimos con Angry Pig y Rino, crearemos una nueva clase llamada Trunk que extienda de Enemy e implemente los métodos elementales necesarios:

package gsampallo.enemys;

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

import javax.imageio.ImageIO;

public class Trunk extends Enemy {
    
    public static int STATE_ATTACK = 7;

    public Trunk(Point initialPoint) {
        super(Enemy.ENEMY_TRUNK,initialPoint);

        this.width = 64;
        this.height = 32;

        loadImages();

        lifePoint = 2;
    }

    protected BufferedImage imageRun;
    protected BufferedImage imageIdle;
    protected BufferedImage imageAttack;
    protected BufferedImage imageHit;
    protected BufferedImage imageBullet;

    protected void loadImages() {

        String pathRun = "image/Enemies/Trunk/Run (64x32).png";
        String pathIdle = "image/Enemies/Trunk/Idle (64x32).png";
        String pathHit = "image/Enemies/Trunk/Hit (64x32).png";
        String pathAttack = "image/Enemies/Trunk/Attack (64x32).png";
        String pathBullet = "image/Enemies/Trunk/Bullet.png";

        try {

            imageRun = ImageIO.read(new File(pathRun));
            imageIdle = ImageIO.read(new File(pathIdle));
            imageAttack = ImageIO.read(new File(pathAttack));
            imageBullet = ImageIO.read(new File(pathBullet));
            imageHit = ImageIO.read(new File(pathHit));

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

Si los medodos updateEnemy y getImage() los copiamos tal cual desde Angry Pig, ya tendríamos a nuestro enemigo Trunk con el mismo comportamiento que Angry Pig, solo que reemplaza las imágenes, no es lo ideal, queremos que tenga su propio caracter.

Igual que con Rino, integraremos Decisions en el constructor para que pueda tomar decisiones en función de un valor aleatorio; con esto dadas ciertos eventos decidirá si dispara o si realiza un patrullaje igual que Rino.

Uno de los cambios que realizaremos sobre Trunk, es que al recibir un Hit, automáticamente cambiara su estado a ATTACK, y devolvera el ataque con tres disparos.

    public void hit() {
        lifePoint--;
        changeState(STATE_HIT);
        imageNumber = 4;
        
    }

Una de las características de Trunk es que puede disparar pequeños bloques de madera, a los que llamaremos Bullet (por el nombre del archivo de la imagen); estos Bullets tendrán un comportamiento similar a las sierras que lanza nuestro protagonista. Crearemos entonces una clase llamada Bullet que extiende de Weapon:

package gsampallo.enemys;

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

import javax.imageio.ImageIO;
import gsampallo.Weapon;

public class Bullet extends Weapon  {

    public Bullet(Point initialPosition) {
        super(initialPosition);

        this.width = 16;
        this.height = 16;
        friendlyFire = false;
        loadImages();
    }

    private void loadImages() {

        try {

            imageWeapon = ImageIO.read(new File("image/Enemies/Trunk/Bullet.png"));

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

    public void updateWeapon(boolean move) {     
        position.x = position.x - 3;
        visible = visible && (position.x > 0);
    }

    public BufferedImage getImage() {

        return imageWeapon;
    }   

    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 momento solo haremos que Trunk dispare estos Bullet cuando recibe un Hit; será necesario establecer una «bandera» dentro de Trunk que nos indique realizo un nuevo disparo, para ello integramos lo siguiente:

private boolean fired = false;
public boolean shotFire() {
  return fired;
}

Como es una sola imagen y no un sprite, el método getImage() devuelve la imagen. Los Bullets viajan en sentido opuesto a Weapons, es decir hacia el personaje; con lo cual en método updateWeapon(), disminuiremos el valor de x en lugar de sumarlo; esto es para que se acerquen al margen izquierdo de la ventana.

Volviendo a la clase Trunk, nos quedo pendiente definir updateEnemy():

   public void updateEnemy(boolean move) {  
        

        if(state == STATE_RUN) {
            position.x = position.x - 2;
            if(imageNumber < (imageRun.getWidth()/width)-1) {
                imageNumber++;
            } else {
                imageNumber = 0;
            }


        } else if(state == STATE_ATTACK) {

            if(imageNumber < (imageAttack.getWidth()/width)-1) {
                fired = false;
                imageNumber++;
            } else {
                imageNumber = 0;
                if(nFire > 0) {
                    nFire--;
                }
                changeState(STATE_IDLE);
            }
        

        } else if(state == STATE_HIT) {
            if(imageNumber > 0) {
                imageNumber--;
            } else {
                imageNumber = 0;
                if(lifePoint == 0) {
                    visible = false;
                } else {
                    if(previousState != STATE_ATTACK) {
                        initFire();
                    }
                    
                }
            }
        } else {
            /*
             * IDLE
             */
            if(imageNumber < (imageIdle.getWidth()/width)-1) {
                imageNumber++;
            } else {
                imageNumber = 0;
                if(nFire > 0) {
                    shootFire();
                    changeState(STATE_ATTACK);
                }
            }        
        }

        if(move) {
            position.x--;

            visible = (position.x > 0) && visible;
        }
    }

También debemos definir el método initFire() y algunas variables:

    private int nFire = 0;
    private int previousState = 0;
    private int nPatrol = 0;
    private int nIdle = 0;

    private boolean fired = false;

    public void initFire() { 

        nFire = 3; // Will shot three bullets

        System.out.println("Show fire! "+nFire);

        changeState(STATE_ATTACK);
        shootFire();
    }

initFire() será invocado luego que finaliza el ciclo de las imágenes de Hit. La variable nFire define la cantidad de disparos que realizara; entra disparo y disparo cambia al estado IDLE para dar un tiempo intermedio.

Finalmente en RunnerOne, modificamos el método updateGame(), específicamente el bloque de código destinado a actualizar enemigos:

public void updateGame() {

  /** resto del codigo **/
        if(!listEnemy.isEmpty()) {
            Iterator it = listEnemy.iterator();
            while(it.hasNext()) {
                Enemy enemy = (Enemy)it.next();

                enemy.updateEnemy(moved);
                
                if(enemy.isVisible()) {  

                    if(enemy.getType() == Enemy.ENEMY_RINO) {
                        rinoBoxes((Rino)enemy);
                    }

                    if(enemy.getType() == Enemy.ENEMY_TRUNK) {
                        updateTrunk((Trunk)enemy);
                    }

                } else {      
                    it.remove();
                }
            }
        }

}

En caso que el tipo de enemigo sea Trunk invocamos al método updateTrunk(), esto nos ayuda a no tener un bloque de código tan extenso y dificil de leer.

    private void updateTrunk(Trunk trunk) {
        if(trunk.shotFire()) {
            System.out.println("Show fire");
            Bullet bullet = new Bullet(new Point(trunk.getX(),trunk.getY()+10));
            listWeapon.add(bullet);
        }
    }

El metodo updateTrunk() simplemente determina si se realizo algún disparo durante esa iteración, y de ser así, crea una instancia de Bullet en un punto del plano relativo al enemigo y lo agrega a la lista de Weapons; recordemos que Bullet extiende de Weapon y puede ser utilizada de esa manera.

Si será necesario incorporar un pequeño bloque de codigo en Weapon, para determinar si los disparos son amigos o no, con esto evitamos que los disparos enemigos maten a otros enemigos:

    protected boolean friendlyFire = true;

    public boolean isFriendlyFire() {
        return friendlyFire;
    }

En consecuencia actualizamos el bloque de código de los disparos dentro de updateGame():

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(weapon.isFriendlyFire()) {  // esta es la condicion que se agrega

          /** resto del codigo donde se recorren las listas **/

        } else {
          //aqui es donde debemos ver si impacta al personaje y actuar en consecuencia
        }
     }

  
  /** resto del codigo **/

}

Juego de plataformas en Java (12): Más enemigos

Hasta el ultimo articulo habíamos incorporado dos enemigos: Angry Pig y el Bat; en esta oportunidad sumaremos otro más: Rino.

De igual manera que los anteriores, comenzaremos por definir una clase Rino que extiende de Enemy:

package gsampallo.enemys;

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

import javax.imageio.ImageIO;

public class Rino extends Enemy {

    public static int STATE_HIT_WALL = 5;

    public Rino(Point initialPoint) {
        super(Enemy.ENEMY_RINO,initialPoint);

        this.width = 52;
        this.height = 34;

        loadImages();

        this.state = STATE_IDLE;

        lifePoint = 3;        
    }


    protected BufferedImage imageRun;
    protected BufferedImage imageIdle;
    protected BufferedImage imageHitWall;
    protected BufferedImage imageHit;

    protected void loadImages() {

        String pathRun = "image/Enemies/Rino/Run.png";
        String pathIdle = "image/Enemies/Rino/Idle.png";
        String pathHitWall = "image/Enemies/Rino/HitWall.png";
        String pathHit = "image/Enemies/Rino/Hit.png";

        try {

            imageRun = ImageIO.read(new File(pathRun));
            imageIdle = ImageIO.read(new File(pathIdle));
            imageHitWall = ImageIO.read(new File(pathHitWall));
            imageHit = ImageIO.read(new File(pathHit));

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

Hasta aquí no hay mayores cambios, es una copia de AngryPig, con la diferencia que las variables width y height tienen los valores correspondientes a Rino.

Solamente incorporamos la imagen de Hit Wall, que ocurre cuando Rino impacta contra algún objeto.

Al igual que AngryPig, también necesitamos definir los metodos hit, updateEnemy y getImage:

    private int imageNumber = 0;
    public void updateEnemy(boolean move) {  
        

        if(state == STATE_RUN) {
            position.x = position.x - 2;
            if(imageNumber < (imageRun.getWidth()/width)-1) {
                imageNumber++;
            } else {
                imageNumber = 0;
            }
        } else if(state == STATE_HIT) {
            if(imageNumber > 0) {
                imageNumber--;
            } else {
                imageNumber = 0;
                if(lifePoint == 0) {
                    visible = false;
                } else {
                    state = STATE_RUN;
                }
            }
        } else {
            if(imageNumber < (imageIdle.getWidth()/width)-1) {
                imageNumber++;
            } else {
                imageNumber = 0;
            }        
        }

        if(move) {
            position.x--;

            visible = (position.x > 0) && visible;
        }
    }


    public BufferedImage getImage() {
        int x = imageNumber*width;
        if(state == STATE_RUN) {
            return imageRun.getSubimage(x, 0, width,height);
        } else if(state == STATE_HIT) {
            return imageHit.getSubimage(x, 0, width,height);
        } else {
            return imageIdle.getSubimage(x, 0, width,height);
        }
    }

    public void hit() {
        lifePoint--;
        state = STATE_HIT;
        imageNumber = 4;
    }

Con esto se logra un comportamiento idéntico al del Angry Pig, pero la idea es que Rino se comporte diferente.

La intención es que por momento recorra cierta parte del mapa (siempre en un recorrido horizontal), que las cajas que son del tipo BOX3 no las pueda atravesar.

En determinado momento Rino deberá «decidir» si comienza su patrulla o no, y en que dirección para ello, crearemos una sencilla clase llamada Decisions, que se basa en un valor aleatorio obtenido por medio de la clase Random, para obtener valores y de los cuales se toma una decisión.

package gsampallo;

import java.util.Random;

public class Decisions {

    private Random rnd;

    public Decisions() {
        rnd = new Random();
    }

    public boolean getTrueFalse() {
        return (rnd.nextInt(100) > 50);
    }

}

Por el momento solo tiene un método llamado getTrueFalse() que devuelve verdadero o falso, con eso sera suficiente por ahora para decidir si Rino comienza su patrulla y en que dirección (derecha o izquierda).

Es claro que existen mecanismos mucho más fiables para obtener números aleatorios, pero para nuestro objetivo es más que suficiente. Siempre es posible modificar la clase Decisions para hacerla más «aleatoria».

Definimos como variable global

private Decisions decisions;
private boolean routePatrol = false; // false = left true = right
private boolean isPatrol = false;

public Rino(Point initialPoint) {
  
  /** resto del codigo **/
  decisions = new Decisions();

}
    

Incorporamos dos variables booleans, donde isPatrol determina si Rino esta patrullando una parte del mapa y routePatrol establece que dirección: false izquierda y true derecha; siempre se va a mover en sentido horizontal.

Definimos también un método llamado initPatrol():

    private int nPatrol = 0;
    private int previousState = 0;    

    public void initPatrol() {
        changeState(STATE_RUN);
        isPatrol = true;
        routePatrol = ((new Random().nextInt(10)) > 5);
        nPatrol = 0;
    }


    private void changeState(int newState) {
        previousState = state;
        state = newState;
    }

initPatrol() es llamada cuando se cumple Rino decide comenzar a patrullar; aún no vamos a entrar en detalle de cual es el momento en el que lo hace. Cuando decide patrullar se invoca a initPatrol(); donde cambia al estado de RUN; isPatrol = true, define la ruta: true RIGHT false LEFT y establece en cero una variable llamada nPatrol; esta variable es la que lleva la cantidad de vueltas mínimas que realizar en dicho patrullaje.

changeState() solamente cumple la tarea de mantener el estado previo al cambio de estado en una variable, simplifica un poco.

Modificaremos el metodo updateEnemy() para que incorpore los cambios, tambien incorporaremos un nuevo metodo llamado updateRun() que será llamada cuando debamos actualizar el estado de RUN:

    public void updateEnemy(boolean move) {  

        if(state == STATE_RUN) {
            
            updateRun();

        } else if(state == STATE_HIT) {
            if(imageNumber > 0) {
                imageNumber--;
            } else {
                imageNumber = 0;
                if(lifePoint == 0) {
                    visible = false;
                } else {
                    changeState(STATE_RUN);
                }
            }
        } else if(state == STATE_HIT_WALL) {
            if(imageNumber < (imageHitWall.getWidth()/width)-1) {
                imageNumber++;
            } else {
                imageNumber = 0;
                changeState(STATE_RUN);
                routePatrol = !routePatrol;
            }
        
        } else {
            if(imageNumber < (imageIdle.getWidth()/width)-1) {
                imageNumber++;
            } else {
                imageNumber = 0;
             
            
                /*
                * Se evalua si se va a patrullar o no
                */
                if(nIdle < 12) {
                    nIdle++;
                } else {

                    nIdle = 0;
                    if(decisions.getTrueFalse()) {
                        initPatrol();
                    }
                }
            }
        }

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

El metodo updateRun():

    private void updateRun() {
        if(routePatrol) {
            position.x = position.x + 2;
        } else {
            position.x = position.x - 2;
        }

        if(imageNumber < (imageRun.getWidth()/width)-1) {
            imageNumber++;
        } else {

            if(nPatrol < 4) {
                imageNumber = 0;
                previousState = STATE_RUN;                    
            } else {
                imageNumber = 0;
                changeState(STATE_IDLE);
            }
        }
    }

Es claro que tenemos un nivel de IF anidados que no es deseado, luego buscaremos una solución más elegante para esto.

En RunnerOne será necesario actualizar el metodo updateGame(), en particular el bloque de codigo donde actualizamos la lista de los enemigos, lo haremos de la siguiente manera:

public void updateGame() {
 
  /** resto del codigo **/
  if(!listEnemy.isEmpty()) {
    Iterator it = listEnemy.iterator();
    while(it.hasNext()) {
      Enemy enemy = (Enemy)it.next();

      enemy.updateEnemy(moved);
      if(enemy.isVisible()) {  

        if(enemy.getType() == Enemy.ENEMY_RINO) {
          rinoBoxes((Rino)enemy);
        }

      } else {      
        it.remove();
      }
    }
  }


}

En caso que el tipo de enemigo sera RINO, entonces invocamos al metodo rinoBoxes(); lo definimos a continuación:

private void rinoBoxes(Rino rino) {

  if(!listBox.isEmpty()) {
    Iterator it = listBox.iterator();
    while(it.hasNext()) {
      Box box = (Box)it.next();
      boolean colision = isHorizontalColision(rino,box,0) && isHorizontalColision(box,rino,0);

      if(colision && (box.getType() == Box.BOX3)) {
        rino.hitWall();
      } else if(colision && (box.getType() < Box.BOX3)) {
        box.setBreak();
      } 
    }
  }
}

Rino solo se detendrá si el tipo de BOX es BOX3, para las otras dos las destruirá y seguirá su paso.