> Manuales > Manual de Phaser

En este artículo veremos cómo implementar una mejora del breakout con Phaser 3, para darle al jugador varias vidas y permitir que la partida sea un poco más sencilla y jugable.

Cómo implementar varias vidas en un juego Phaser 3

Hemos desarrollado un juego con Javascript y el motor Phaser 3, lo que nos ha dado pie a aprender muchas cosas sobre el framework, a lo largo de diversos artículos en el Manual de Phaser.

En esta ocasión vamos a seguir mejorando el juego de nuestra práctica, aportando una facilidad importante, como es la gestión de varias vidas. A decir verdad, esta mejora resulta más de lógica de programación que de manejo del propio framework, por lo que tu pericia como desarrollador será lo más importante. Aunque, como sabes, al resolver los problemas siempre se aprenden cosas nuevas, y esta vez no va a ser distinto.

Crear un componente de gestión de vidas

La lógica de nuestra escena del juego ya es lo suficientemente compleja como para parchearla con nuevas características, agregando unas decenas de líneas de código extra aquí y allá. Es importante que desde el principio separemos el código por responsabilidades, haciendo componentes (o llamémosle simplemente clases) que se encargan de implementar alguna de las áreas del programa. De hecho, este consejo no lo hemos llevado siempre a la práctica a lo largo del desarrollo realizado hasta el momento, por no complicarnos demasiado, pero ya es hora que nos lo tomemos en serio.

Así pues, toda la lógica de la gestión de las diversas vidas en el juego la vamos a sacar fuera de la escena, a un componente nuevo en el que nos encargaremos de diversas operativas:

Nuestro componente se llamará LiveCounter y lo crearemos en una clase independiente, que pondremos en la carpeta "components".

export class LiveCounter {
  // Código de la clase para la gestión de vidas del jugador
}

Cuando construyamos el componente le vamos a pasar la escena, porque la necesitaremos en diversos lugares del código. Además pasaremos también en el constructor el número de vidas con el que queremos iniciar el juego.

constructor(scene, initialLives) {
  this.relatedScene = scene;
  this.initialLives = initialLives;
}

Guardaremos la escena en una propiedad del componente, para poder usarla más adelante. Guardamos también el número de vidas iniciales con el que queremos iniciar el juego.

Crear el display de vidas

Este display, que muestra las vidas restantes, será como el del juego clásico del Arkanoid, donde podíamos ver las vidas representadas con una miniatura de las plataformas que maneja el jugador. Así pues, cada vida que nos quede, además de la que estamos usando en un momento dado, requerirá una imagen en la pantalla.

Marcador de vidas en el juego

A medida que nos quiten vidas, esas imágenes irán desapareciendo progresivamente, por lo tanto, me interesa tenerlas en un grupo, para poder acceder a ellas en su debido momento y eliminarlas.

Vamos a implementar la creación de ese grupo en un método create() del componente que se encarga de mostrar el display de las vidas.

Ese método create tiene que ir creando todas las imágenes de las vidas que tengamos que mostrar en el marcador. Recuerda que, si tenemos 3 vidas al inicio (por ejemplo), mostraría 2 vidas el marcador, porque la tercera es la que se está jugando en ese instante.

Luego me di cuenta que hubiera sido mucho más sencillo colocar simplemente el número de vídas en un dígito de texto, porque al final se lió un poco el código para mostrar las vidas con distintas imágenes de la plataforma, más que nada por calcular la posición donde se van a colocar las vidas.

Como podemos iniciar el juego con un número de vidas parametrizado, tendremos que hacer un poco de programación para saber la posición del display (a más vidas más a la izquierda debe comenzar a colocarse las imágenes).

Usaremos las funciones de crear grupos que nos ofrece Phaser para que las cosas sean más sencillas. El método create() tendrá esta forma.

create() {
    let displacement = 60;
    let firstPosition = 800 - ((this.initialLives - 1) * displacement);
    this.liveImages = this.relatedScene.physics.add.staticGroup({
      setScale: { x: 0.5, y: 0.5 },
      key: 'platform',
      frameQuantity: this.initialLives-1,
      gridAlign: {
        width: this.initialLives - 1,
        height: 1,
        cellWidth: displacement,
        cellHeight: 30,
        x: firstPosition,
        y: 30
      }
    });
  }

Básicamente, creo una variable displacement para indicar la cantidad de píxeles que hay entre cada imagen de cada vida. Luego veo la posición donde se colocaría la primera imagen, que tengo que calcular en función del número de vidas a mostrar en el display, por el desplazamiento entre ellas. Luego realizo el grupo, que mediante su configuración permite colocar las imágenes en los lugares correctos.

La gestión de grupos ya la vimos en un artículo anterior, cuando colocamos los ladrillos del juego. En este caso además estamos haciendo un "setScale" para que las imágenes del grupo se muestren con la mitad de su tamaño normal.

Método para restar una vida del jugador

Ahora veamos el método que hemos creado para el objeto contador de vidas, que nos permite restarle una vida al jugador.

liveLost() {
  if (this.liveImages.countActive() == 0) {
    this.relatedScene.endGame();
    return false;
  }
  let currentLiveLost = this.liveImages.getFirstAlive();
  currentLiveLost.disableBody(true, true);
  return true;
}

Este método hace uso del API de la clase Group de Phaser, que nos permite saber el número de elementos que hay activos y nos permite acceder a ellos.

En un primer momento comprobamos si nos quedan vidas para restar todavía. Si no quedaban vidas, avisamos a la escena que usa este componente, para que de por finalizado el juego.

Si aún quedaban vidas en el contador, entonces accedemos a la primera disponible que esté viva, con getFirstAlive(). Luego hacemos que ese elemento del grupo desaparezca del juego.

Este método devuelve true si el juego está en funcionamiento y false en caso que no pudiera quitar vida alguna, en cuyo caso el juego estaba acabado.

Hasta aquí el código de la clase LiveCounter. Ahora se trata de usarla dentro del juego.

Cómo usar el contador de vidas del juego

Veamos paso por paso cómo se debe modificar el juego para habilitar las vidas del jugador. La mayor complicación será llevar el juego a un estado inicial cada vez que se pierda una vida y aún tengamos otras vidas disponibles para seguir jugando.

El primer paso será crear una instancia del contador de vidas. Lo hacemos en el método init().

init() {
  this.score = 0;
  this.liveCounter = new LiveCounter(this, 3);
}

Esto permite que, cada vez que la escena del juego se resetea, se cree un nuevo contador de vidas.

Método loader()

El contador de vidas usa la misma imagen de la plataforma para mostrar cada vida, por lo que no hay que recargar nada. Sin embargo, hemos creado un nuevo sonido para cuando el usuario pierde la vida.

this.load.audio('livelostsample', 'sounds/live-lost.ogg');

Recuerda que en el pasado capítulo del manual explicamos la gestión de sonidos en Phaser.

Método create()

En el método create() ahora tenemos que invocar el método create() del contador de vidas. Para mostrar el display de las vidas.

this.liveCounter.create();

Además también añadimos el sonido de perder una vida a la escena.

this.liveLostSample = this.sound.add('livelostsample');

Cómo perder una vida

Cuando se pierde una vida, tenemos que pedirle al contador de vidas que la descuente. El contador de vidas nos devuelve un boleano para saber si el juego no había acabado, que usamos en un condicional para llevar el juego a un estado inicial, en el que la bola está pegada a la plataforma.

if (this.ball.y > 500 && this.ball.active) {
  let gameNotFinished = this.liveCounter.liveLost();
  if (!gameNotFinished) {
    this.setInitialPlatformState();
  }
}

Cómo llevar el juego al estado inicial

Ahora la dificultad está en llevar el juego al estado inicial, donde la bola se encuentra pegada a la plataforma.

setInitialPlatformState() {
    this.liveLostSample.play();
    this.platform.x = 400;
    this.platform.y = 460;
    this.ball.setVelocity(0,0);
    this.ball.x = 385;
    this.ball.y = 430;
    this.ball.setData('glue', true);
}

Básicamente cambiamos las propiedades necesarias en la bola y la plataforma, para conseguir ese estado inicial. Además reproducimos el sonido de una vida perdida.

También hay un pequeño cambio en el método que implementa el final del juego, para conseguir que ahora se encargará también de reproducir el sonido de game over.

endGame(completed = false) {
  if(! completed) {
    this.gameOverSample.play();
    this.scene.start('gameover');
  } else {
    this.scene.start('congratulations');
  }
}

Conclusión

Esos son los detalles que hemos tenido que cambiar para adaptar el juego para que permita disfrutar de varias vidas. Como has visto, se trata más de un problema de lógica del juego, que otra cosa, que se resuelve con un poco de programación.

Lo más interesante aquí es ver cómo hemos separado la mayor parte del código a una clase aparte, de modo que no ha sido necesario agregar excesiva complejidad a la escena. De hecho hemos tocado mínimamente el código del juego principal y sin embargo, la mejora ha sido importante. Este trabajo realizado para separar el código en diversas clases es esencial para que el desarrollo pueda crecer sin llegar a volverse un caos.

Puedes ver el código de este juego, tal como lo hemos dejado en este punto, en este enlace. https://github.com/deswebcom/ball-game-phaser/tree/lives

Estaba prácticamente decidido a dejar este proyecto por aquí, pero mi hijo me motivó para crear distintos niveles, para que puedas pasar por pantallas diferentes, con distintas colocaciones de los ladrillos. Esta mejora la explicaremos en el próximo artículo.

Miguel Angel Alvarez

Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...

Manual