En Mi Local Funciona

Technical thoughts, stories and ideas

Phaser 3: Mi primer juego HTML5/JS (Parte 2)

Publicado por Jesús García Navarro el

UI DevelopmentPhaser 3HTML5

Retomamos la serie de posts relacionados con la creación de videojuegos para HTML5/JS usando la librería Phaser 3. En el post anterior detallamos cómo montar la arquitectura del proyecto, explicamos determinados conceptos de los juegos como los sprites, escenas, etc., así como el control de nuestro personaje.

¿Qué vamos a hacer en esta segunda entrega?

En esta parte abarcaremos lo siguiente:

  • Creación de enemigos.
  • Rebotes.
  • Sistemas de colisiones.
  • Añadir sonidos.

Creando enemigos

alt

Vector de Personaje creado por brgfx - www.freepik.es

Ahora vamos a añadirle a la escena un conjunto de enemigos que nuestro personaje deberá eliminar. Lo primero, como hablamos en el anterior post, es precargar nuestro sprite:

  • this.load.image("virus", "assets/virus.png");

Quedando la carga de imágenes de la siguiente forma:

    this.load.image('background', 'assets/background.png')
             .image("bullet", "assets/bullet.png")
             .image("virus", "assets/virus.png")
             .spritesheet('doggysprite', 'assets/doggysprite.png',
                  { frameWidth: 50, frameHeight: 66 }
    );

Nota: Como podrás ver a partir de ahora iremos optimizando el código.

Tal y como sucedía con el disparo de nuestro personaje, no solamente habrá un enemigo, sino que la idea es que vayan apareciendo aleatoriamente en pantalla enemigos cada cierto tiempo. Para ello debemos crear en el método create() un grupo con este sprite de la siguiente manera:

this.virus = this.physics.add.group({
        defaultKey: 'virus'
    });    

Ahora ya podremos generar de una manera fácil varias instancias de ese sprite.

El siguiente paso es hacer que los enemigos aparezcan en pantalla cada cierto tiempo en un posición del eje X aleatoria. La finalidad es hacer que los enemigos caigan desde la parte superior de la pantalla y aleatoriamente tengan un movimiento de izquierda o derecha y sigan rebotando como si se tratase de una pelota, con la única condición de que nunca perderá altura en los rebotes. En este caso, nuestros enemigos deberán colisionar primero con los bordes de la pantalla y luego con los disparos y con nuestro personaje principal, donde (por ahora) se terminará el juego.

Para conseguirlo, nos crearemos una función donde se creen los enemigos y se establezcan sus propiedades deseadas:

newVirus() {

   var oneVirus= this.virus.get(Phaser.Math.Between(0, this.game.config.width), 20);
    if (oneVirus) {
        oneVirus.setActive(true)
              .setVisible(true)
              .setGravityY(300)
              .setCollideWorldBounds(true)
              .setCircle(45)
              .setBounce(1, 1)
              .setVelocityX(
                              (Phaser.Math.Between(0, 1) ? 100 : -100)
                         );
             }
}

Esta función es fácilmente entendible, primero creamos una instancia del grupo "virus" otorgándole una posición en pantalla aleatoria en el eje X, sin embargo, la posición del eje Y será fija 20.

Luego si la instancia "oneVirus" se ha creado satisfactoriamente estableceremos sus propiedades, como la gravedad, la forma de la máscara de colisiones, los rebotes, su velocidad, etc...

Ahora solo tendremos que ir generando los enemigos en pantalla, para ello en el método update() iremos llamando a la función "newVirus()". Pero ¿Cómo lo hacemos cada X segundos? Fácil, usando los parámetros time y delta del método update():

Primero inicializamos en el método init() una variable, al que llamaremos respawn:

  • this.respawn = 0;

y ya de esta manera en el método update() hacemos que cada 3000ms se genere un enemigo:

    if (time > this.respawn) {
        this.newVirus();
        this.respawn += 3000;
    }

El resultado es el siguiente:

alt

Colisiones

A continuación debemos crear las colisiones tanto de los enemigos con el personaje principal como con el disparo (las burbujas). Por suerte en Phaser 3 se realiza de una manera bastante sencilla a través del método collider. En el método create() indicaremos qué elementos pueden colisionar entre ellos de la siguiente forma:

 this.physics.add.collider(this.player, this.virus, this.hitPlayer, null, this);
    this.physics.add.collider(this.bullets, this.virus, this.hitvirus, null, this);

Este método recibirá los 2 elementos a colisionar, callback una vez hayan colisionado, callback para decidir si hay colisión o no y el contexto de la escena. Esto último es muy importante para que dentro del código del callback, la variable this siga siendo igual a la escena sobre la que estamos trabajando, sino no podríamos hacer referencia a sus propiedades.

Nota: Sería interesante activar el modo "debug" en nuestro juego para poder ver las máscaras de colisiones de los sprites. En nuestro main.js cambiamos el valor de la propiedad "debug":

physics: {
    default: 'arcade',
    arcade: {
        debug: true
    }

alt

Ya solamente nos quedaría crear las funciones a ejecutar una vez se detecten las colisiones:

hitPlayer(player, virus) {
    this.scene.pause();
}

hitvirus(bullet, virus) {
    virus.destroy();
    bullet.destroy();
}

Si el enemigo nos alcanza, por ahora simplemente pararemos la escena, más adelante estableceremos la lógica de perder vidas o perder el juego. Si el disparo alcanza al enemigo, ambos desaparecerán, igualmente en el próximo post, haremos un marcador de puntuación.

Añadiendo sonidos

El sonido y la música es una parte fundamental de cualquier juego, hace que prestemos atención a los acontecimientos que van sucediendo en pantalla. Añadir sonidos a nuestro proyecto se realiza de una forma muy similar a la incorporación de sprites.

Primero cargaremos en preload() los sonidos de nuestra escena:

    this.load.audio('pop',['assets/pop.wav'])
             .audio('shot',['assets/shot.wav'])
             .audio('killed',['assets/killed.wav'])
             .audio('rebound',['assets/rebound.wav']);

Al igual que pasaba con las imágenes, es interesante nombrar los sonidos igual que el nombre del archivo.

Nota: Estos sonidos los hemos descargado de un banco de sonidos gratuitos: https://mixkit.co/

Ahora creamos en create() los atributos con esos sonidos:

    this.popSound = this.sound.add('pop');
    this.shotSound = this.sound.add('shot');
    this.killedSound = this.sound.add('killed');
    this.reboundSound = this.sound.add('rebound');

De esta manera ya están dispuestos para ser utilizados. En nuestro caso los utilizaremos al disparar (shot), al explotar el virus (pop) y rebotar (rebound) y cuando un virus nos alcance (reobound).

Modificamos las funciones para que ejecute el sonido:

hitPlayer(player, virus) {
    this.killedSound.play();
    this.scene.pause();
}

hitvirus(bullet, virus) {
    virus.destroy();
    bullet.destroy();
    this.popSound.play();
}

En el método update(), cada vez que se genere un enemigo ejecutará también su sonido:

    this.virus.children.each(function(enemy) {
        if(enemy.body.y == 0){
            this.reboundSound.play();
        }
      }, this);

Y por último cuando disparemos:

fire(object) {
    var bullet = this.bullets.get(object.x + 17, object.y - 30);
    if (bullet) {
        bullet.setActive(true)
              .setVisible(true)
              .body.velocity.y = -200;
    }
    bullet.outOfBoundsKill = true;
    this.shotSound.play();
}

Los sonidos lógicamente se pueden parar, reanudar, etc. os enlazo más documentación sobre el tema: https://rexrainbow.github.io/phaser3-rex-notes/docs/site/audio/

Música de fondo

Al igual que hemos añadido los efectos sonoros de los elementos en pantalla, ahora añadiremos una música de fondo que se vaya repitiendo mientras estemos vivos en la escena.

Para ello precargamos también el nuevo audio, lo llamaremos bgmusic:

    this.load.audio('pop',['assets/pop.wav'])
             .audio('shot',['assets/shot.wav'])
             .audio('killed',['assets/killed.wav'])
             .audio('rebound',['assets/rebound.wav'])
             .audio('bgmusic',['assets/bgmusic.mp3']);

Nota: La música es gratuita y se ha obtenido de https://www.chosic.com/free-music/games/?sort=&attribution=no

Creamos el atributo backgroundMusic en create() y le establecemos la propiedad loop a true:

    this.backgroundMusic = this.sound.add('bgmusic');
    this.backgroundMusic.loop = true;
    this.backgroundMusic.play();

Y ya para terminar, haremos que la música se detenga cuando el enemigo alcance al personaje principal, para ello modificaremos la siguiente función:

hitPlayer(player, virus) {
    this.killedSound.play();
    this.backgroundMusic.stop();
    this.scene.pause();
}

Por ahora el juego quedaría como el siguiente GIF (aunque con sonido):

alt

Continuará...

Damos por concluida esta segunda parte de la serie de tutoriales de Phaser 3. En las siguientes entregas veremos:

  • Marcador de puntuación.
  • Vidas del personaje.
  • Recarga del disparo.
  • Reinicio del juego.
  • Powers Up.
  • Aumento de la dificultad.
  • Otros enemigos.

El código actual del juego lo tenéis en GitHub:

¡Síguenos en Twitter para estar al día de próximas entregas!

Enlaces de interés