En este artículo aprenderás a trabajar con varias escenas en un mismo juego Javascript con Phaser, realizando acciones como intercambiar las escenas, pararlas o reiniciarlas.
En un juego habitualmente tenemos varias escenas que se pueden desarrollar de manera independiente. Phaser ayuda mucho a la hora de crear escenas y permite realizar el cambio de una escena a otra de una manera sencilla.
En este artículo del Manual de Phaser aprenderemos a manejar escenas, creando nuevas escenas en nuestro juego y cambiando de unas a otras. Además, al final de este artículo encontrarás un vídeo donde explicamos todo el contenido de esta lección, de manera práctica y visual.
Qué es una escena
Una escena es como una pantalla o una fase del juego independiente, que mantiene su propio flujo de ejecución también de manera independiente de otras escenas del juego.
En un juego podemos representar tantas escenas como queramos. Por ejemplo podemos tener una primera escena para la pantalla inicial, con la carátula del juego y el botón de comenzar. También podemos tener una escena con el desarrollo principal del juego y otra con las opciones, o la pantalla de game over.
En fin, cada vez que veamos que una parte del juego es muy distinta a otra que estábamos desarrollando, es una buena idea programar una escena independiente. Esto facilita el mantenimiento del juego, porque tenemos diversas partes en archivos independientes, y no se nos mezcla todo en una gran escena. Aunque también agrega algo de dificultad por el hecho de tener que cambiar de una escena a otra, pero esta parte Phaser la hace bastante sencilla, por lo que enseguida lo tendremos todo claro.
Phaser puede mantener varias escenas, pasar de una a otra, detenerlas, pausarlas, correr varias escenas al mismo tiempo, etc. Todas las acciones con las escenas están dentro del propio API de una escena, dentro de una propiedad llamada "scene". De hecho, ya habíamos usado este API para pausar una escena cuando mostrábamos la pantalla de Game over. ¿Recuerdas?
this.scene.pause()
Cómo implementar diversas escenas en un juego
Cuando realizamos el objeto de configuración para crear un nuevo juego teníamos un array en una propiedad llamada "scene" donde por el momento sólo habíamos colocado una escena, el propio juego.
Sin embargo ahora vamos a agregar nuevas escenas y ese array se entregará con nuevos elementos.
const config = {
type: Phaser.AUTO,
width: 800,
height: 500,
scene: [Game, Gameover, Congratulations],
physics: {
default: 'arcade',
arcade: {
debug: false
}
}
}
Cada escena la hemos construido en una clase distinta, así mantenemos cada cosa en su sitio y nos ayuda a manejarnos mejor por las pantallas del programa.
Fíjate que ahora, aparte del juego (clase Game), vamos a tener dos escenas nuevas, una con la clase Gameover y otra con la clase Congratulations. No hace falta ser muy imaginativo para suponer qué van a contener esas escenas ¿no?
Estas escenas las vamos a implementar en archivos distintos. Como el juego ya tiene 3 escenas es una buena idea crear una carpeta donde coloquemos todos los archivos de éstas.
Así pues, la estructura de carpetas y archivos del juego ahora pasa a ser así.
Por supuesto, dado que cada escena está en un archivo aparte, las vamos a tener que importar antes de poder agregarlas al array de escenas del juego. Por tanto, el código del archivo index.js quedará así:
import { Game } from './scenes/game.js';
import { Congratulations } from './scenes/congratulations.js';
import { Gameover } from './scenes/gameover.js';
const config = {
type: Phaser.AUTO,
width: 800,
height: 500,
scene: [Game, Gameover, Congratulations],
physics: {
default: 'arcade',
arcade: {
debug: false
}
}
}
var game = new Phaser.Game(config);
Ten en cuenta que la escena que hemos colocado como primera en el array será la escena que comenzará el juego.
Cada escena la implementaremos de manera muy similar a lo que hemos aprendido para la escena principal, con los típicos métodos preload(), create() y todos los que necesites.
Componentes comunes
La escena de "congratulations" es muy parecida a la escena de "gameover". De hecho es casi la misma, solo que va a cambiar la imagen que se muestra en una y otra. Lo cierto es que las podríamos haber implementado usando una única escena y no habría estado nada mal, pero así podemos tener más escenas en el juego y encontrar soluciones para solucinar los casos en los que tenemos dos o más escenas que usan los mismos elementos.
En nuestro caso, las nuevas escenas de nuestro juego van a tener un elemento en común, que es un botón que sirve para reiniciar el juego. Como sabes, no es ideal hacer copy paste del código del botón, así que vamos a ver cómo crear un componente botón que podríamos usar en ambas escenas.
Este componente lo he creado en un archivo independiente, para que sea un módulo Javascript que puedo usar desde cualquier escena que lo necesite. De momento solo tenemos un componente, pero podrían venir más, por lo que crearemos una carpeta para ellos. Por tanto, la estructura del proyecto cambia una vez más:
La idea de este componente es que encapsule toda la complejidad de hacer un botón, tanto la parte visual como su funcionalidad al hacer clic sobre él. Así que, por facilidad, hemos decidido implementarlo en una clase.
Cuando se instancia el botón necesitamos conocer la escena donde se va a mostrar, porque luego la vamos a necesitar en varios métodos diferentes. Comenzamos entonces viendo cómo sería el constructor.
export class RestartButton {
constructor(scene) {
this.relatedScene = scene;
}
// otros métodos de la clase
}
Es importante ver cómo dentro del constructor recibo la escena en la que estoy y la guardo en una propiedad del componente llamada "relatedScene".
Ahora nuestro botón tendrá los métodos necesarios para hacer las operativas: Precarga de la imagen del botón, método preload() Crear la imagen en la pantalla, método create() Implementar el reinicio del juego cuando se pulsa.
Sprites del botón
Otra gracia de este botón es que lo hemos implementado mediante sprites!. Todavía no habíamos visto los sprites y son muy útiles dentro de Phaser. Este es un ejemplo muy elemental, que no muestra toda la potencia de los sprites ni de lejos, pero nos sirve para introducirnos en ellos.
A falta de introducciones mayores, los sprites son distintos fotogramas de una imagen, que si los pasamos en secuencia producen animaciones, como el movimiento de un personaje. En nuestro botón tenemos algo tan simple como un par de estados, uno el normal y otro que se activará al pasar el ratón por encima.
Los sprites se cargan en el método de preload, que tendrá esta forma:
preload() {
this.relatedScene.load.spritesheet('button', 'images/restart.png', { frameWidth: 190, frameHeight: 49 });
}
Como puedes ver, usamos this.relatedScene para precargar algo en la escena, ya que la propiedad "relatedScene" es donde había guardado la escena relacionada con esta instancia del componente.
Luego usamos el método load.spritesheet() que recibe varios parámetros:
- El identificado que vamos a darle a este sprite.
- La imagen donde se encuentran las distintas imágenes, que simplemente tendrá una secuencia de las distintas alternativas de vistas del botón.
- Las dimensiones del sprite (cada imagen suelta)
Ahora veamos cómo se añaden los sprites a una escena, algo que se haría en el método create().
create() {
this.startButton = this.relatedScene.add.sprite(400, 230, 'button').setInteractive();
}
Un sprite se añade de manera similar a una imagen y, de entrada, mostrará la primera imagen disponible del sprite. Luego las podemos cambiar.
El método setInteractive() simplemente sirve para que podamos hacer el botón interactivo, con lo que responderá a diversos eventos del usuario.
Esos eventos los podemos crear justo a continuación, así que el método create nos quedaría realmente así.
create() {
this.startButton = this.relatedScene.add.sprite(400, 230, 'button').setInteractive();
this.startButton.on('pointerover', () => {
this.startButton.setFrame(1);
});
this.startButton.on('pointerout', () => {
this.startButton.setFrame(0);
});
this.startButton.on('pointerdown', () => {
this.relatedScene.scene.start('game');
});
}
Fíjate que gracias a setInteractive() el botón podrá responde a eventos como 'pointerover', 'pointerout' o 'pointerdown' y que para asociar los manejadores a cada evento usamos el método on sobre el propio sprite que hemos hecho interactivo.
El manejador asociado a 'pointerover' y 'pointerout' simplemente intercambian el sprite visible en el botón.
Por su parte, el manejador 'pointerdown' es el que se encargará de cambiar la escena, para volver a reiniciar el juego.
Este cambio de escena se tiene que hacer por medio del método start() pasando por parámetro el nombre de la escena a la que queremos cambiar. Fíjate que start() pertenece a un objeto "scene" que está dentro de "relatedScene".
El método start() produce que la escena a la que nos dirigimos se reinicie, que es justamente lo que necesitamos. Sin embargo, si fuera el caso también es posible volver a la escena anterior recuperando el estado en el que la hubiéramos dejado, en cuyo caso usaríamos el método switch().
Nota: Para que switch() pueda recuperar verdaderamente el estado donde se había quedado la escena es importante que esa escena a la que volvemos no se haya detenido. Si salimos de la escena a la que intentamos volver con un método que finalice la escena actual, como start(), por mucho que intentemos volver con switch() la escena se habrá tenido que reiniciar de nuevo. Es decir, si hacemos un start() para irnos a otra escena, la escena en la que estamos se detendrá, con lo que si volvemos a ella no la veremos en el estado que estaba antes.
Código de las escenas nuevas
Ahora el código de las escenas "gameover" y "congratulations" ha quedado muy simple, ya que la mayor parte del trabajo lo hemos delegado en el botón que acabamos de separar a un componente.
Nota: Siempre es bueno separar código a otros componentes, incluso aunque esos componentes solamente los vayamos a usar en una escena determinada. Así nuestros archivos se mantendrán pequeños y manejables. En nuestro juego no hemos realizado esta práctica porque estábamos comenzando, pero perfectamente podríamos haber creado un componente para los ladrillos, otro para la plataforma, la bola, etc. Así habíamos separado el código en varias partes y el juego principal no sería tan grande. Esta separación de código será fundamental en el momento que las cosas comiencen a complicarse, porque manejar escenas con cientos de líneas de código es una auténtica locura.
La escena de game over tendrá este código.
import { RestartButton } from "../components/restart-button.js";
export class Gameover extends Phaser.Scene {
constructor() {
super({ key: 'gameover' });
this.restartButton = new RestartButton(this);
}
preload() {
this.load.image('gameover', 'images/gameover.png');
this.restartButton.preload();
}
create() {
this.add.image(410, 250, 'background');
this.restartButton.create();
this.gameoverImage = this.add.image(400, 90, 'gameover');
}
}
Es importante fijarse que se le ha asignado un nombre a la escena en el constructor. Además que en el constructor hemos instanciado el botón.
El preload y el create llaman al preload y el create del botón, realizando el trabajo necesario para que ese botón se muestre y se le asocie la funcionalidad.
El código de la clase de congratulations quedará así.
import { RestartButton } from "../components/restart-button.js";
export class Congratulations extends Phaser.Scene {
constructor() {
super({ key: 'congratulations' });
this.restartButton = new RestartButton(this);
}
preload() {
this.load.image('congratulations', 'images/congratulations.png');
this.restartButton.preload();
}
create() {
this.add.image(410, 250, 'background');
this.restartButton.create();
this.congratsImage = this.add.image(400, 90, 'congratulations');
}
}
Es prácticamente lo mismo, solamente cambia el identificador de la escena, que siempre se tiene que dar único en el constructor y la imagen que da las felicitaciones.
Cómo pasar de una escena a otra en Phaser
Ahora vamos a centrarnos en el flujo de pasar de una escena a otra. No tiene mucho misterio y algo ya hemos visto en el propio botón anterior.
En la clase Game hemos creado un método que sirve para finalizar el juego. Puede ser finalizado porque has perdido o porque has completado la pantalla rompiendo todos los ladrillos.
endGame(completed = false) {
if(! completed) {
this.scene.start('gameover');
} else {
this.scene.start('congratulations');
}
}
En ambos casos lo que hacemos es iniciar una nueva escena. Fíjate que la escena se inicia con el método start() indicando el indentificador de la escena que quieres iniciar.
Como estamos iniciando las nuevas escenas con el método start() se produce también la parada de la escena actual, por lo tanto, si volvemos a la escena Game más adelante, simplemente se verá reiniciada.
Ahora podemos llamar a endGame() cada vez que deseamos finalizar el juego.
Por ejemplo, cuando habíamos encontrado que la bola se perdía por los límites inferiores, llamamos a endGame() sin pasarle parámetros:
if (this.ball.y > 500 && this.ball.active) {
console.log('fin', this.ball.y, this.ball, '--');
this.endGame();
}
Y cuando el juego se acaba porque hemos roto todos los ladrillos, llamamos a endGame() pasándole true como parámetro.
brickImpact(ball, brick) {
brick.disableBody(true, true);
this.increasePoints(10);
if (this.bricks.countActive() === 0) {
this.endGame(true);
}
}
Conclusión
Con esto hemos terminado el juego y hemos obtenido un resultado bastante atractivo, con relativamente poco esfuerzo y un código bastante sencillo de escribir, todo gracias a las posibilidades de Phaser 3.
Para darle un toque final nos deberíamos entretenernos en darle algún sonido, algo que haremos en el próximo artículo.
Hasta el momento tienes todo el código y las imágenes del juego en este enlace. https://github.com/deswebcom/ball-game-phaser/tree/scenes
Videotutorial sobre Escenas en Phaser
Hemos publicado un vídeo que resume el contenido de este artículo, que puedes ver si te gusta el aprendizaje visual.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...