> Manuales > Manual de Phaser

Cómo gestionar distintas fases con niveles independientes en el Juego Breakout HTML5 con Phaser 3. Además crearemos distintos niveles con nuevas distribuciones y tipos de ladrillos.

Cómo gestionar distintos niveles en un juego Phaser

Qué bonito sería el juego si tuviera distintos niveles, ¿no?. Ya que hemos trabajado lo suficiente para crear todo un sistema de romper ladrillos, con muy poquito más podemos hacer que el juego pueda tener diversas pantallas y que puedas ir pasando de una a otra cuando has terminado de romper todos los ladrillos. ¿Nos ponemos manos a la obra?

En esta ocasión estamos también ante un requisito que está más cercano a la programación que a otra cosa. No aprenderemos mucho nuevo del framework Phaser 3, pero sí nos servirá para ir ganando pericia como programadores de juegos. Todos los artículos para entender cómo hemos llegado al estado actual en el juego los tienes en el Manual de Phaser.

Organización de las clases para generación de niveles

El reto en el que nos encontramos no es trivial. Si comenzamos a programar sin saber muy bien cómo vamos a conseguir que la aplicación muestre distintos niveles es muy probable que no lleguemos a la mejor solución, una que sea sencilla de entender, pero que sobre todo nos permita agregar tantos niveles como queramos, sin que el juego en sí se vea afectado. Es decir, que podamos escalar el juego hasta el infinito sin que aumente su complejidad, ya que agregar nuevas pantallas sería tan simple como añadir más casillas a un array.

Para ello vamos a pensar en una estructura de clases que nos permita llegar a nuestro objetivo de no compicarnos demasiado a medida que las pantallas aumentan.

Así pues, en nuestro juego vamos a incorporar dos elementos principales:

Piensa en cada fase como un mapa de ladrillos distintos. La fase será la que cree el conjunto de ladrillos que se van rompiendo en cada nivel. Lo que he llamado "generador de niveles" quizás hubiera sido mejor llamarlo intercambiador de niveles, puesto que realmente lo que realiza es la acción de controlar los distintos niveles del juego para pasar de uno a otro.

Además, como todos los niveles del juego tienen algunas funcionalidades comunes, crearemos una clase base para las fases del juego. Cada una de las fases del juego (niveles, pantallas) extenderá esa clase base.

Clase base para las fases del juego

Vamos a comenzar viendo la clase base que implementa las clases del juego, Para poder entenderla te pido que tengas en mente cómo estaba hecha la escena del juego hasta este punto, porque hay algún código de la escena que nos hemos traído para aquí.

export class Phase {
  constructor(scene) {
    this.relatedScene = scene;
  }

  configureColisions() {
    this.relatedScene.physics.add.collider(this.relatedScene.ball, this.bricks, this.relatedScene.brickImpact, null, this.relatedScene);
  }

  isPhaseFinished() {
    return (this.bricks.countActive() === 0)
  }
}

Como todo componente, es necesario recibir la escena donde lo vamos a usar, guardando una referencia en el constructor.

Hemos agregado en esta clase la configuración de las colisiones, que antes teníamos en la escena game.js. Ya que la fase es quien posee el grupo de los ladrillos, lo normal es que ella misma se encargue de implementar las colisiones.

Además con ello conseguimos descargar de código a la escena principal del juego, lo que resulta ideal para un mejor mantenimiento. De hecho, gracias a estas mejoras en la escena Game ahora tendremos que borrar todo el trabajo de generación de los ladrillos y la colisión, que estaba en el método create().

Luego mostraremos y se comentará el detalle sobre cómo queda la escena principal para usar los distintos niveles. Además, al final del artículo encuentras el enlace para ver el código del proyecto hasta este punto, por si tienes cualquier duda.

Las clases de cada una de las fases del juego

Extendiendo la clase "Phase" que acabamos de ver, podremos crear todas las pantallas o niveles del juego. Tendrán un método create() en el que crearemos todos los ladrillos que se vayan a mostrar en esta fase. Es tan sencillo como esto.

Mira este código de una de las fases del juego:

import { Phase } from './phase.js'

export class Phase4 extends Phase {

  create() {
    this.bricks = this.relatedScene.physics.add.staticGroup({
      key: ['bluebrick', 'orangebrick', 'greenbrick', 'yellowbrick'],
      frameQuantity: 10,
      gridAlign: {
        width: 10,
        height: 4,
        cellWidth: 67,
        cellHeight: 34,
        x: 95,
        y: 100
      }
    });

    this.configureColisions();

  }
}

No siempre podremos colocar los ladrillos mediante una configuración del método staticGroup(). Hay veces que la colocación no responde a un patrón fácilmente programable, por lo que los ladrillos también los podemos colocar uno a uno.

Para ilustrar este punto te dejo otro ejemplo de código de otra fase.

import { Phase } from './phase.js'

export class Phase2 extends Phase {

  create() {
    this.bricks = this.relatedScene.physics.add.staticGroup();

    this.bricks.create(400, 270, 'orangebrick');
    this.bricks.create(360, 225, 'orangebrick');
    this.bricks.create(440, 225, 'orangebrick');
    this.bricks.create(480, 180, 'orangebrick');
    this.bricks.create(400, 180, 'orangebrick');
    this.bricks.create(320, 180, 'orangebrick');
    this.bricks.create(280, 135, 'orangebrick');
    this.bricks.create(360, 135, 'orangebrick');
    this.bricks.create(440, 135, 'orangebrick');
    this.bricks.create(520, 135, 'orangebrick');
    this.bricks.create(330, 90, 'orangebrick');
    this.bricks.create(470, 90, 'orangebrick');

    this.configureColisions();
  }
}

En realidad el conjunto de acciones es exactamente el mismo, lo que cambia es cómo generamos los ladrillos en el grupo.

Generador de niveles

Ahora vamos a ver el generador de niveles. Este generador tiene que ser capaz de conocer el orden con el que se van a ir pasando las fases del juego y debe poder pasar de una a otra cuando se le solicite.

import { Phase1 } from './phase1.js'
import { Phase2 } from './phase2.js'
import { Phase3 } from './phase3.js'
import { Phase4 } from './phase4.js'
import { Phase5 } from './phase5.js'
import { Phase6 } from './phase6.js'

export class PhaseConstructor {
  constructor(scene) {
    this.relatedScene = scene;
    this.phases = [
      Phase6,
      Phase5,
      Phase4,
      Phase3,
      Phase2,
      Phase1,
    ];
  }

  create() {
    let CurrenPhaseClass = this.phases.pop();
    this.currentPhase = new CurrenPhaseClass(this.relatedScene);
    return this.currentPhase.create();
  }

  nextLevel() {
    if(this.phases.length == 0) {
      this.relatedScene.endGame(true);
    } else {
      return this.create();
    }
  }

  isPhaseFinished() {
    return this.currentPhase.isPhaseFinished();
  }
}

Con esto, somos capaces de crear el sistema de fases. Lo importante es que, si queremos incluir una nueva fase, no tenemos que tocar nada. Simplemente crear una nueva clase para el nivel que se va a introducir, meterla en el array y listo!

Cómo usar el generador de fases desde la escena del juego

Esta es la parte más sencilla, ya que simplemente es invocar los métodos que hemos dejado preparados en el generador de pantallas del juego. De hecho, en la escena Game lo más que tenemos que hacer es borrar código que ahora está localizado en el generador de pantallas. Fantástico! porque así dejamos el juego más leve y manejable.

Voy a centrarme en las novedades en el juego que resultan remarcables, porque además de usar el generador de fases, hemos introducido alguna mejora extra, como nuevos bloques de otros colores y sonidos para nuevos eventos del juego.

En el método init() instancio el constructor de niveles del juego:

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

En el método create() de la clase Game, todo lo que era la construcción de los ladrillos y las colisiones se resume a invocar el método del constructor de niveles:

this.phaseConstructor.create();

El método brickImpact() tiene un par de novedades. Primero, la responsabilidad de saber si el nivel está terminado o no ahora la tiene el constructor de fases, invocando el método isPhaseFinished(). Además, en el caso que la fase se haya terminado, tenemos que pedirte al constructor de fases que pase al siguiente nivel, con el método nextLevel().

brickImpact(ball, brick) {
  this.brickImpactSample.play();
  brick.disableBody(true, true);
  this.increasePoints(10);
  if (this.phaseConstructor.isPhaseFinished()) {
    this.phaseChangeSample.play();
    this.phaseConstructor.nextLevel();
    this.setInitialPlatformState();
  }
}

Ya está! no hay nada más que haya cambiado. Una vez más, mucha de la complejidad nos la hemos llevado a las nuevas clases que se han generado. De hecho, con todos estos cambios que a priori podrían suponer una complejidad mucho mayor para nuestro juego, en realidad el impacto ha sido justamente inverso! ahora el juego es hasta más simple que antes!!!

Crear unos bricks indestructibles

Hay otra mejora que hemos introducido en esta fase y que me he saltado para hablar de ella al final.

Hacer niveles requiere imaginación y se me estaban acabando las ideas. Por eso se me ocurrió que sería divertido darle alguna novedad en los niveles del juego, insertando algo que sería fácil de introducir, como bloques indestructibles.

La idea para implementarlos es básicamente generar esos bloques en un grupo aparte. Es decir, no forman parte del grupo de ladrillos que hay que romper para acabar el nivel. Es normal, porque el comportamiento de estos bloques es distinto y tampoco es necesario romperlos (ni se puede) para pasar al siguiente nivel.

Vamos a ver una clase que implementa una fase donde hay bloques indestructibles.

import { Phase } from './phase.js'

export class Phase1 extends Phase {
  create() {
    this.bricks = this.relatedScene.physics.add.staticGroup({
      key: ['bluebrick', 'orangebrick', 'greenbrick', 'blackbrick', 'yellowbrick', 'blackbrick', 'yellowbrick', 'bluebrick', 'orangebrick', 'greenbrick'],
      frameQuantity: 1,
      gridAlign: {
        width: 5,
        height: 4,
        cellWidth: 150,
        cellHeight: 100,
        x: 135,
        y: 150
      }
    });

    this.fixedBricks = this.relatedScene.physics.add.staticGroup();
    this.fixedBricks.create(316, 165, 'greybrick');
    this.fixedBricks.create(466, 165, 'greybrick');

    this.configureColisions();
    this.configureColisionsFixed();
  }
}

Simplemente hemos creado un grupo extra, con los bloques nuevos, que serán grises, como imitando a metal.

Luego se configuran las colisiones de una manera diferente. Ese método configureColisions() está en la clase Phase, la que extendemos. Que en verdad tenía este código que ahora te muestro completo:

export class Phase {
  constructor(scene) {
    this.relatedScene = scene;
  }

  configureColisions() {
    this.relatedScene.physics.add.collider(this.relatedScene.ball, this.bricks, this.relatedScene.brickImpact, null, this.relatedScene);
  }

  configureColisionsFixed() {
    this.relatedScene.physics.add.collider(this.relatedScene.ball, this.fixedBricks, this.relatedScene.fixedBrickImpact, null, this.relatedScene);
  }

  deleteFixedBricks() {
    if(this.fixedBricks) {
      this.fixedBricks.getChildren().forEach(item => {
        item.disableBody(true, true);
      })
    }
  }

  isPhaseFinished() {
    return (this.bricks.countActive() === 0)
  }
}

El método deleteFixedBricks() se necesita porque, al pasar de una fase a la otra, se deben borrar los bloques fijos (irrompibles), porque si no, permanecerían al entrar en los próximos niveles.

Por tanto, el codigo para pasar de un nivel a otro, que tenemos en la clase PhaseConstructor, también tiene una pequeña diferencia.

nextLevel() {
  this.currentPhase.deleteFixedBricks();
  if(this.phases.length == 0) {
    this.relatedScene.endGame(true);
  } else {
    return this.create();
  }
}

Había omitido justamente la parte en la que pedimos al nivel actual borrar los bloques fijos, antes de pasar al siguiente nivel.

Conclusión

Creo que con esto he terminado de explicar todas las novedades introducidas en esta mejora del juego, que permite disponer de diferentes niveles o pantallas con distintas distribuciones de bloques.

La verdad es que el juego está teniendo un aspecto que incluso podría superar las expectativas de más de uno ¿no?

El código completo, tal como lo hemos dejado en esta etapa del desarrollo, lo puedes consultar en este repositorio de GitHub. https://github.com/deswebcom/ball-game-phaser/tree/phases

Ya solamente nos queda uno de los detalles más importantes del juego original del Arkanoid: la posibilidad de ganar poderes especiales cuando se rompen ciertos ladrillos, que mejore la jugabilidad, y la diversidad de las partidas. Esto lo veremos seguidamente.

Miguel Angel Alvarez

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

Manual