Juego de plataformas en Java (11): Enemigos

Un juego de plataforma sin enemigos no estaría completo; en la galería encontramos varios grupos de imágenes que podemos utilizar para crear varios enemigos que atacaran a nuestro héroe.

El gif puede demorar en cargar

De la misma forma que realizamos con Trap, crearemos una clase que Enemy:

package gsampallo.enemys;

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

import gsampallo.Element;

public class Enemy implements Element {

    public static int ENEMY_ANGRYPIG = 0;
    public static int ENEMY_BAT = 1;
    public static int ENEMY_BEE = 2;
    public static int ENEMY_FATBIRD = 3;
    public static int ENEMY_RINO = 4;
    public static int ENEMY_TURTLE = 5;

    public static int STATE_IDLE = 0;
    public static int STATE_RUN = 1;
    public static int STATE_JUMP = 2;
    public static int STATE_FALL = 3;
    public static int STATE_HIT = 4;

    protected Point position;

    protected int width = 32;
    protected int height = 32;

    protected boolean visible = true;

    protected int type;
    protected int lifePoint = 0;

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

    public void updateEnemy(boolean move) {     
    }

    public void hit() {

    }

    public int getType() {
        return type;
    }

}

No estoy incluyendo todos los métodos que se deben definir porque implementa la interfaz Element.

Como existen varios tipos de enemigos, utilizaremos una variable type para determinar cual es, también definimos varios parámetros estáticos para usarlos de referencia.

Al igual que nuestro protagonista, los enemigos tendrán puntos de vida, lifePoint que disminuirán a medida que reciban un disparo, lo cual se define por medio del método hit(), particular para cada tipo de enemigo.

También tendrán diferentes estados, que nos permitirán determinar que imagen es la que se devolverá.

Angry Pig

Crearemos una nueva clase llamada AngryPig:

package gsampallo.enemys;

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

import javax.imageio.ImageIO;

public class AngryPig extends Enemy {
    

    public AngryPig(Point initialPoint) {
        super(Enemy.ENEMY_ANGRYPIG,initialPoint);

        this.width = 36;
        this.height = 30;

        loadImages();

        lifePoint = 2;
    }

    protected BufferedImage imageRun;
    protected BufferedImage imageIdle;
    protected BufferedImage imageWalk;
    protected BufferedImage imageHit;

    protected void loadImages() {

        String pathRun = "image/Enemies/AngryPig/Run.png";
        String pathIdle = "image/Enemies/AngryPig/Idle.png";
        String pathWalk = "image/Enemies/AngryPig/Walk.png";
        String pathHit = "image/Enemies/AngryPig/Hit.png";

        try {

            imageRun = ImageIO.read(new File(pathRun));
            imageIdle = ImageIO.read(new File(pathIdle));
            imageWalk = ImageIO.read(new File(pathWalk));
            imageHit = ImageIO.read(new File(pathHit));

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

Hasta acá es similar a los elementos del juego que veníamos integrando, en el constructor se hace la llamada al constructor de la clase padre, y también especificamos widht/height y los puntos de vida que tendrá el enemigo.

Será necesario definir el método updateEnemy(boolean) para actualizar los parámetros:

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

Solo especificamos tres estados: HIT, IDLE y RUN. HIT a diferencia del resto debemos tomar las imágenes desde atrás hacia adelante, comenzamos con imageNumber = 4 y vamos descendiendo.

Del bloque de código anterior, lo diferente a lo que veníamos trabajando es que cuando esta en el estado hit, luego de hacer el recorrido por todas las imágenes (imageNumber = 0), evaluamos si lifePoint es igual a cero, si se cumple esa condición cambiamos la visibilidad del enemigo.

Definimos el método getImage() para retornar la imagen:

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

Por ultimo nos queda definir el método hit(), el cual será invocado cuando el enemigo sea alcanzado por un disparo:

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

De la misma manera que en otras ocasiones lo integramos con RunnerOne:

private ArrayList<Enemy> listEnemy;

public RunnerOne() {
  /** resto del codigo **/
  AngryPig pig = new AngryPig(new Point(340,410));

  listEnemy = new ArrayList<Enemy>();
  listEnemy.add(pig);
}

También actualizamos el método paint():

public void paint(Graphics g) {
  /** resto del codigo **/
  drawList(listEnemy, g);
}

Por ultimo nos queda actualizar el método updateGame(), donde además de actualizar los parámetros del listado de enemigos, tenemos que actualizar el bloque de código de los disparos para que los afecte si son impactados.


  /** 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(!listEnemy.isEmpty()) {
        Iterator<Enemy> it1 = listEnemy.iterator();
        while(it1.hasNext()) {
          Enemy enemy = (Enemy)it1.next();
          if(isHorizontalColision(weapon,enemy,1)) {
            enemy.hit();
            weapon.setVisible(false);
            break;
          }
        }
       }

      }

Tambien debemos agregar el recorrido al listado de enemigos para actualizar los parametros:

        if(!listEnemy.isEmpty()) {
            Iterator it = listEnemy.iterator();
            while(it.hasNext()) {
                Enemy enemy = (Enemy)it.next();

                enemy.updateEnemy(moved);
                if(!enemy.isVisible()) {        
                    it.remove();
                }
            }
        }

Finalmente, si lo ejecutamos obtendremos el AngryPig, cuando recibe un disparo se enoja y nos persigue. Quita el resto de los elementos para que sea mas sencillo de verlo.

El gif puede demorar en cargar.

Bat

Incorporamos un segundo enemigo, el murciélago; el cual suma dos estados propios a Enemy, además de otras dos imagenes para dichos estados. Definimos la clase Bat:

package gsampallo.enemys;

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

import javax.imageio.ImageIO;

public class Bat extends Enemy {
    
    public static int STATE_CEILING_IN = 5;
    public static int STATE_CEILING_OUT = 6;

    public Bat(Point initialPoint) {
        super(Enemy.ENEMY_BAT,initialPoint);

        this.width = 46;
        this.height = 30;

        loadImages();

        lifePoint = 3;
    }

    protected BufferedImage imageRun;
    protected BufferedImage imageIdle;
    protected BufferedImage imageHit;
    protected BufferedImage imageCeilingIn;
    protected BufferedImage imageCeilingOut;

    protected void loadImages() {

        String pathRun = "image/Enemies/Bat/Run.png";
        String pathIdle = "image/Enemies/Bat/Idle.png";
        String pathCeilingIn = "image/Enemies/Bat/Ceiling In (46x30).png";
        String pathCeilingOut = "image/Enemies/Bat/Ceiling Out (46x30).png";
        String pathHit = "image/Enemies/Bat/Hit.png";

        try {

            imageRun = ImageIO.read(new File(pathRun));
            imageIdle = ImageIO.read(new File(pathIdle));
            imageCeilingIn = ImageIO.read(new File(pathCeilingIn));
            imageCeilingOut = ImageIO.read(new File(pathCeilingOut));
            imageHit = ImageIO.read(new File(pathHit));

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

Bat es muy similar a AngryPig, cambia un poco en la definición de los métodos. Incorporamos los métodos restantes:

    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_CEILING_OUT) {
            if(imageNumber < (imageCeilingOut.getWidth()/width)-1) {
                imageNumber++;
            } else {
                imageNumber = 0;
                state = STATE_RUN;
            }
        } else if(state == STATE_HIT) {
            if(imageNumber > 0) {
                imageNumber--;
            } else {
                imageNumber = 0;
                if(lifePoint == 0) {
                    visible = false;
                } else {
                    if(previusState == STATE_IDLE) {
                        state = STATE_CEILING_OUT;
                    } else {
                        state = STATE_RUN;
                    }
                }
            }
        } else {
            if(imageNumber < (imageIdle.getWidth()/width)-1) {
                imageNumber++;
            } else {
                imageNumber = 0;
            }        
        }

        if(move) {
            position.x--;

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

En el estado HIT, tenemos varios niveles de IF anidados; por el momento lo vamos a dejar así.

    public BufferedImage getImage() {
        int x = imageNumber*width;
        if(state == STATE_RUN) {
            return imageRun.getSubimage(x, 0, width,height);
        } else if(state == STATE_CEILING_OUT) {
            return imageCeilingOut.getSubimage(x, 0, width,height);
        } else if(state == STATE_CEILING_IN) {
            return imageCeilingIn.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);
        }
    }

    private int previusState = 0;

    public void hit() {
        lifePoint--;

        previusState = state;
        state = STATE_HIT;
        imageNumber = 4;
    }

Bat cuando recibe un disparo, cambia al estado HIT de la misma manera que lo hace AngryPig, pero al finalizar la transición de imágenes de HIT, evalúa si el estado anterior es IDLE o si estaba en algún otro, caso contrario lo cambia a RUN.

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.