Introducción a Phaser 3: Desglose de edificios

El desarrollo de juegos es una rama única del desarrollo de software que puede ser tan gratificante como compleja. Cuando pensamos en crear juegos, solemos pensar en una aplicación...

Introducción

El desarrollo de juegos es una rama única del desarrollo de software que puede ser tan gratificante como compleja. Cuando pensamos en crear juegos, solemos pensar en una aplicación para instalar y jugar en nuestros ordenadores o consolas. La especificación HTML5 introdujo muchas API para permitir el desarrollo de juegos en la web, lo que permite que nuestros juegos lleguen a muchos usuarios en diferentes dispositivos informáticos. Phaser es un marco de juego popular que nos permite crear rápidamente juegos para la web.

La mejor manera de dominar el desarrollo de juegos es crear juegos. Usaremos Phaser para crear un clon de Breakout, una versión del clásico y eterno lanzamiento del juego Atari en 1976.

Este tutorial contiene algunos HTML y CSS muy básicos. Deberá sentirse cómodo con las funciones y los objetos de JavaScript. Hace un uso ligero de las funciones de ES2015.

El bucle del juego {#el bucle del juego}

Todos los juegos se ejecutan dentro de un bucle. Después de configurar nuestro mundo del juego, ingresamos al bucle del juego que realiza las siguientes tareas:

  1. Entrada del proceso
  2. Actualiza el mundo del juego.
  3. Renderiza los cambios

Veamos cómo funciona el bucle de juego en un juego como Megaman. Después de revisar el menú para comenzar un nivel, el juego decide dónde colocar las plataformas y carga la música que se reproducirá. Esa configuración generalmente ocurre durante la pantalla de carga.

Cuando comienza el juego, tienes el control de Megaman en un mundo con plataformas, enemigos y una canción particular para ese nivel. Puedes usar tu joystick para mover a Megaman y presionar un botón para saltar o disparar. El bucle del juego procesa la entrada, actualiza la posición de Megaman y genera esos cambios muchas veces en un segundo.

¿Qué es Phaser?

fáser es un marco de juego HTML5. Utiliza muchas API de HTML5 como Canvas, WebGL, Audio, Gamepad, etc. y agrega una lógica útil como administrar el ciclo del juego y proporcionarnos motores de física. Con Phaser, podemos crear juegos 2D con nada más que HTML, CSS y JavaScript.

Reglas de ruptura {#reglas de ruptura}

Antes de usar Phaser para construir nuestro clon Breakout, primero definamos el alcance del juego:

  • Este juego para un jugador tiene un nivel con 30 ladrillos, una paleta y una pelota.
  • El objetivo es conseguir que la bola destruya todos los ladrillos, asegurándose de que no salga de la parte inferior de la pantalla del juego.
  • El jugador controlará una paleta, que puede moverse hacia la izquierda y hacia la derecha.
  • El juego está diseñado para usuarios de la web de escritorio, el teclado se utilizará para la entrada

Configuración de Phaser

Phaser es una biblioteca de JavaScript, para desarrollar y jugar nuestro juego necesitaremos algo de HTML básico para cargar el JS. Cree un directorio llamado breakout en uno de sus espacios de trabajo. Cree los siguientes archivos y carpetas en su directorio:

  • Un archivo index.html
  • Un archivo breakout.js
  • Una carpeta llamada assets
  • Dentro de su carpeta assets, cree una carpeta images

Los activos del juego son arte, sonido, video y otros datos utilizados por el juego. Para este sencillo clon de Breakout, no hay muchos recursos que requieran organizarse con carpetas. Sin embargo, es una buena práctica mantener sus activos separados de su código y separar sus activos por su tipo.

Agrega el siguiente código a tu archivo index.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!doctype html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
  <title>Breakout</title>
  <style>
    html,
    body {
      margin: 0 auto;
      padding: 0;
      width: 100%;
      height: 100%;
    }

    #game {
      margin: 10px auto;
      padding: 0;
      width: 800px;
      height: 640px;
    }
  </style>
</head>

<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="game"></div>
  <script src="//cdn.jsdelivr.net/npm/[correo electrónico protegido]/dist/phaser.min.js"></script>
  <script src="breakout.js"></script>
</body>

</html>

Este código HTML básico hace lo siguiente:

  • Elimina los márgenes del navegador y el relleno de html y la etiqueta del cuerpo.
  • Agrega un elemento div game que contendrá nuestro clon Breakout
  • Carga Phaser v3.17 a través de su CDN
  • Carga nuestro archivo breakout.js que actualmente no hace nada pero contendrá nuestra lógica de juego

Para desarrollar juegos con Phaser de manera efectiva, necesitaremos que un servidor web sirva estos archivos. Sin un servidor web, nuestro navegador no permitirá que nuestro script de juego cargue nuestros activos por razones de seguridad.

Afortunadamente, no es necesario configurar Apache o Nginx para obtener un servidor web en ejecución. Si usa el código de VisualStudio como yo, puede instalar la extensión [servidor en vivo] (https://ritwickdey.github.io/vscode-live-server/). La mayoría de los IDE y editores de texto tienen un complemento con una funcionalidad similar. Si tiene instalada la versión 3 de Python, puede ir a su espacio de trabajo a través de la terminal e ingresar python3 -m http.server. Hay otras herramientas CLI que proporcionan servidores web simples, elija la que le proporcione el tiempo más rápido para desarrollar su juego.

Por último, descarga los activos de imagen que hemos creado para este juego . Copie y pegue los archivos PNG en la carpeta de imágenes.

Consejo de desarrollo: cuando estás desarrollando un juego, probablemente quieras tener la consola de JavaScript visible para que puedas ver cualquier error que aparezca. Si estás usando Chrome o Firefox, haz clic derecho en la página y selecciona "Inspeccionar elemento". Debería aparecer un cuadro en la parte inferior o lateral de la ventana de su navegador. Seleccione la pestaña "Consola" para ver actualizaciones de errores o registros de nuestro código JavaScript.

Creando nuestro mundo del juego

Con nuestro HTML y CSS configurados, editemos nuestro archivo breakout.js para configurar nuestro mundo de juego.

Iniciando Phaser

Primero, necesitamos configurar Phaser y crear nuestra instancia de Juego. La Instancia de juego es el controlador central para un juego de Phaser, hace toda la configuración y comienza el ciclo del juego por nosotros.

Agregue lo siguiente para configurar y crear nuestra instancia de Juego:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// This object contains all the Phaser configurations to load our game
const config = {
  type: Phaser.AUTO,
  parent: 'game',
  width: 800,
  heigth: 640,
  scale: {
    mode: Phaser.Scale.RESIZE,
    autoCenter: Phaser.Scale.CENTER_BOTH
  },
  scene: {
    preload,
    create,
    update,
  },
  physics: {
    default: 'arcade',
    arcade: {
      gravity: false
    },
  }
};

// Create the game instance
const game = new Phaser.Game(config);

La propiedad type le dice a Phaser qué renderizador usar. Phaser puede renderizar nuestro juego utilizando el elemento WebGL o Canvas de HTML5. Al establecer el tipo en Phaser.AUTO, le estamos diciendo a Phaser que primero intente renderizar con WebGL y, si eso falla, renderice usando Canvas.

La propiedad parent indica el ID del elemento HTML en el que se jugará nuestro juego. Definimos las dimensiones de nuestro juego en píxeles con ancho y alto. El objeto escala hace dos cosas por nosotros:

  • mode le dice a Phaser cómo usar el espacio de nuestro elemento principal, en este caso nos aseguramos de que el juego se ajuste al tamaño del div principal
  • autoCenter le dice a Phaser cómo centrar nuestro juego dentro de nuestro div principal, si así lo deseamos. En este caso, centramos nuestro juego vertical y horizontalmente dentro del div principal. Esta propiedad es más útil cuando el juego no ocupa todo el espacio del div principal. Se muestra aquí porque es una pregunta frecuente.

En Phaser, nuestra lógica de juego se define en Escenas. Piense en las escenas como varios estados en nuestro juego: la pantalla de título es una escena, cada nivel de un juego sería su propia escena, una escena de corte sería su propia escena. Phaser proporciona un Objeto de escena pero también puede funcionar con un objeto JavaScript normal que contiene preload, create y ` funciones de actualización definidas.

La última configuración le dice a Phaser qué motor de física usar. Phaser puede usar 3 motores físicos diferentes: Arcada, [Impacto](https://photonstorm.github.io/phaser3 -docs/Phaser.Physics.Impact.html) y Asunto. Arcade es el más simple para comenzar y es suficiente para nuestras necesidades de juego.

Breakout no necesita la gravedad para funcionar, por lo que deshabilitamos la propiedad. Si estuviéramos construyendo un juego de plataformas, probablemente habilitaríamos la gravedad, de modo que cuando nuestros jugadores salten, caerán de forma natural al suelo.

Para garantizar que la configuración de nuestro juego funcione, debemos agregar las funciones precargar, crear y actualizar, agregue las siguientes funciones en blanco después de crear nuestra instancia de juego:

1
2
3
4
5
function preload() { }

function create() { }

function update() { }

Con su servidor web ejecutándose, navegue a la página donde se ejecuta su juego. Deberías ver una pantalla en blanco como esta:

Configuración del juego

Cargando activos

Los recursos de este juego consisten en 5 imágenes. En otros juegos que puedes crear, tus activos pueden ser enormes. Las imágenes de alta definición, los archivos de audio y video pueden ocupar megabytes de espacio. Cuanto más grande es el activo, más tarda la carga. Por esa razón, Phaser tiene una función de precarga donde podemos cargar todos los activos antes de comenzar a ejecutar el juego. Nunca es una buena experiencia para el usuario estar jugando un juego y de repente se ralentiza porque está tratando de cargar nuevos activos.

Cambie la función preload a lo siguiente para que podamos cargar nuestras imágenes antes de que comience el ciclo del juego:

1
2
3
4
5
6
7
function preload() {
  this.load.image('ball', 'assets/images/ball_32_32.png');
  this.load.image('paddle', 'assets/images/paddle_128_32.png');
  this.load.image('brick1', 'assets/images/brick1_64_32.png');
  this.load.image('brick2', 'assets/images/brick2_64_32.png');
  this.load.image('brick3', 'assets/images/brick3_64_32.png');
}

El primer argumento es la clave que usaremos más adelante para hacer referencia a la imagen, el segundo argumento es la ubicación de la imagen.

Con las imágenes cargadas, queremos colocar sprites en la pantalla. En la parte superior de breakout.js, agregue estas variables que contendrán nuestros datos de sprite:

1
let player, ball, violetBricks, yellowBricks, redBricks;

Una vez definidas globalmente, todas nuestras funciones podrán utilizarlas.

Adición de sprites

Un sprite es cualquier imagen 2D que forma parte de la escena de un juego. En Phaser, un sprite encapsula una imagen junto con su posición, velocidad, propiedades físicas y otras propiedades. Comencemos por crear nuestro sprite de jugador. En la función crear agregue lo siguiente:

1
2
3
4
5
player = this.physics.add.sprite(
  400, // x position
  600, // y position
  'paddle', // key of image for the sprite
);

Ahora debería poder ver una paleta en la pantalla:

Jugador en pantalla

El primer argumento del método sprite es la Coordenada X para colocar el sprite. El segundo argumento es la coordenada Y, y el último argumento es clave para el recurso de imagen agregado en la función de “precarga”.

Es importante entender cómo Phaser y la mayoría de los frameworks de juegos 2D usan coordenadas. Los gráficos que aprendimos en la escuela generalmente colocan el origen, es decir, el punto (0, 0) en el centro. En Phaser, el origen está en la parte superior izquierda de la pantalla. A medida que aumenta x, esencialmente nos estamos moviendo hacia la derecha. A medida que y aumenta, nos movemos hacia abajo. Nuestro juego tiene un ancho de 800 píxeles y una altura de 640 píxeles, por lo que las coordenadas de nuestro juego se verían así:

Coordenadas del juego

Agreguemos la pelota para que se siente sobre el jugador. Agregue el siguiente código a la función crear:

1
2
3
4
5
ball = this.physics.add.sprite(
  400, // x position
  565, // y position
  'ball' // key of image for the sprite
);

Como la pelota está arriba de nuestro jugador, el valor de la Coordenada Y es inferior que la Coordenada Y del jugador.

Adición de grupos de sprites

Si bien Phaser facilita la adición de sprites, rápidamente se volvería tedioso si cada sprite tuviera que definirse individualmente. Los ladrillos en Breakout son prácticamente idénticos. Las posiciones son diferentes, pero sus propiedades, como el color y la forma en que interactúan con la pelota, son las mismas. En lugar de crear 30 objetos de sprites de ladrillo, podemos crear grupos de sprites para administrarlos mejor.

Agreguemos la primera fila de ladrillos violetas. En su función crear agregue lo siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Add violet bricks
violetBricks = this.physics.add.group({
  key: 'brick1',
  repeat: 9,
  setXY: {
    x: 80,
    y: 140,
    stepX: 70
  }
});

En lugar de this.physics.add.sprite, usamos this.physics.add.group y pasamos un objeto JavaScript. La propiedad clave hace referencia a la clave de imagen que utilizarán todos los sprites del grupo de sprites. La propiedad repetir le dice a Phaser cuántas veces más debe crear un sprite. Cada grupo de sprites crea un sprite. Con repetir establecido en 9, Phaser creará 10 sprites en ese grupo de sprites. El objeto setXY tiene tres propiedades interesantes:

  • x es la Coordenada X del primer sprite
  • y es la coordenada Y del segundo sprite
  • stepX es la longitud en píxeles entre sprites repetidos en el eje x.

También hay una propiedad stepY pero no necesitamos usarla para este juego. Agreguemos los otros dos grupos de sprites restantes para ladrillos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Add yellow bricks
yellowBricks = this.physics.add.group({
  key: 'brick2',
  repeat: 9,
  setXY: {
    x: 80,
    y: 90,
    stepX: 70
  }
});

// Add red bricks
redBricks = this.physics.add.group({
  key: 'brick3',
  repeat: 9,
  setXY: {
    x: 80,
    y: 40,
    stepX: 70
  }
});

Nuestro juego ya se está armando, tu pantalla debería verse así:

Todos los sprites añadidos

Ganar y perder

Es una buena práctica de desarrollo de juegos (y programación) mantener el final a la vista. En Breakout, podemos perder un juego si nuestra bola cae al fondo de la pantalla. En Phaser, para que la pelota esté debajo de la pantalla, la coordenada Y de la pelota es mayor que la altura del mundo del juego. Vamos a crear una función que verifique esto, agregue la parte inferior de breakout.js y agregue lo siguiente:

1
2
3
function isGameOver(world) {
  return ball.body.y > world.bounds.height;
}

Nuestra función toma el objeto mundial de la propiedad física de la escena, que estará disponible en la función actualizar. Comprueba si la coordenada Y del sprite de la bola es mayor que la altura de los límites del mundo del juego.

Para ganar el juego tenemos que deshacernos de todos los ladrillos. Los sprites en Phaser tienen una propiedad activa. Podemos usar esa propiedad para determinar si ganamos o no. Los grupos de sprites pueden contar la cantidad de sprites activos que contienen. Si no hay sprites activos en cada uno de los grupos de sprites de ladrillos, es decir, hay 0 sprites de ladrillos activos, entonces el jugador ganó el juego. En la parte inferior de su breakout.js agregue la siguiente función:

1
2
3
function isWon() {
  return violetBricks.countActive() + yellowBricks.countActive() + redBricks.countActive() == 0;
}

Aceptamos cada uno de los grupos de sprites como parámetros, agregamos la cantidad de sprites activos dentro de ellos y verificamos si es igual a 0.

Ahora que hemos definido nuestras condiciones ganadoras y perdedoras, queremos que Phaser las verifique cada comienzo del ciclo del juego. Tan pronto como el jugador gane o pierda, el juego debería detenerse. Cambie la función actualizar a lo siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function update() {
  // Check if the ball left the scene i.e. game over
  if (isGameOver(this.physics.world)) {
    // TODO: Show "Game over" message to the player
  } else if (isWon()) {
    // TODO: Show "You won!" message to the player
  } else {
    // TODO: Logic for regular game time
  }
}

No hay cambios visuales para esa actualización de código.

Nota: cuando usamos this en nuestras funciones de precarga, creación y actualización, nos referimos a la escena ejecutada por la instancia del juego que se creó anteriormente.

Mover el reproductor con la entrada del teclado

El movimiento del jugador depende de la entrada del teclado. Para poder rastrear la entrada del teclado, agreguemos una variable global llamada cursores en la parte superior de breakout.js:

1
let player, ball, violetBricks, yellowBricks, redBricks, cursors;

Y en la parte inferior de nuestra función crear, agregue lo siguiente:

1
cursors = this.input.keyboard.createCursorKeys();

Las teclas de cursor en Phaser rastrean el uso de 6 teclas del teclado: arriba, derecha, abajo, izquierda, shift y espacio.

Ahora necesitamos reaccionar al estado de nuestro objeto cursores para actualizar la posición de nuestro jugador. En la cláusula else de nuestra función update agregue lo siguiente:

1
2
3
4
5
6
7
8
// Put this in so that the player stays still if no key is being pressed
player.body.setVelocityX(0);

if (cursors.left.isDown) {
  player.body.setVelocityX(-350);
} else if (cursors.right.isDown) {
  player.body.setVelocityX(350);
}

¡Ahora podemos mover nuestro reproductor de izquierda a derecha!

Jugador en movimiento

Notarás que el sprite del jugador puede salir de la pantalla del juego, idealmente no debería. Abordaremos eso más adelante cuando manejemos las colisiones.

Esperando para comenzar

Antes de agregar lógica para mover la pelota, ayudaría si el juego esperara la entrada del usuario antes de moverse. No es una buena experiencia cargar un juego y ser forzado inmediatamente a jugar, ¡el jugador no tendría tiempo suficiente para reaccionar!

Vamos a mover la pelota hacia arriba después de que el jugador presione la barra espaciadora. Si el usuario mueve la paleta hacia la izquierda o hacia la derecha, la pelota también se moverá para que siempre esté en el centro de la paleta.

Primero, necesitamos nuestra propia variable para rastrear si un juego se inició o no. En la parte superior de breakout.js, después de la declaración de nuestras variables de juego, agregue lo siguiente:

1
let gameStarted = false;

Como sugiere el nombre, esas variables rastrean si nuestro juego ha comenzado o no. Ahora, en la cláusula else de nuestra función de actualización, agregue lo siguiente:

1
2
3
4
5
6
7
8
if (!gameStarted) {
  ball.setX(player.x);

  if (cursors.space.isDown) {
    gameStarted = true;
    ball.setVelocityY(-200);
  }
}

Si el juego no ha comenzado, coloque la Coordenada X o nuestra bola en el centro del jugador. Las coordenadas de un objeto del juego se basan en su centro, por lo que las propiedades x e y de los sprites se relacionan con el centro de nuestros sprites.

Tenga en cuenta que si bien está bien obtener el valor de una propiedad como x al hacer referencia a ella directamente, al establecer propiedades, siempre tratamos de usar la función de establecimiento adecuada. Las funciones de establecimiento pueden incluir lógica para validar nuestra entrada, actualizar otra propiedad o activar un evento. Hace que nuestro código sea más predecible.

Al igual que antes al mover al jugador, comprobamos si se presionó la barra espaciadora. Si se presionó, cambiamos el indicador gameStarted a true para que la pelota ya no siga la posición horizontal del jugador, y establecemos la velocidad Y de la pelota en -200. Las velocidades y negativas envían objetos hacia arriba. Para velocidades positivas, los valores más grandes mueven los objetos hacia abajo más rápido. Para velocidades negativas, los valores más pequeños mueven los objetos hacia arriba más rápido.

Ahora cuando movemos al jugador, la pelota sigue y si presionamos la barra espaciadora la pelota sale disparada hacia arriba:

Presione para comenzar

Observarías algunas cosas de cómo se comporta nuestro juego hasta ahora:

  1. La pelota se renderiza detrás de los ladrillos.
  2. El jugador puede salir de los límites de la pantalla.
  3. El balón puede salir de los límites de la pantalla

La pelota se representa detrás de los ladrillos porque se agregó al juego en nuestra función de creación antes de los grupos de sprites de ladrillos. En Phaser, y generalmente con el elemento canvas de HTML5, la imagen agregada más recientemente se dibuja encima de las imágenes anteriores.

Los dos últimos problemas se pueden resolver agregando alguna colisión mundial.

Manejo de colisiones

Colisión mundial

Todas nuestras interacciones con sprites se definen en la función crear. Habilitar la colisión con la escena mundial es muy fácil con Phaser, agregue lo siguiente al final de la función crear:

1
2
player.setCollideWorldBounds(true);
ball.setCollideWorldBounds(true);

Debería darnos una salida como esta:

Colisión mundial

Si bien el movimiento del jugador está bien, la pelota parece atascada en la parte superior. PARA rectificar esto, necesitamos establecer la propiedad rebote del sprite de la bola. La propiedad rebote le diría a Phaser cuánta velocidad mantener después de chocar con un objeto. Agregue esto al final de su función crear:

1
ball.setBounce(1, 1);

Esto le dice a Phaser que la pelota debe mantener toda su velocidad X e Y. Si soltamos la pelota con la barra espaciadora, la pelota debería estar rebotando arriba y abajo del mundo del juego. Necesitamos deshabilitar la detección de colisiones desde la parte inferior del mundo del juego. Si no lo hacemos, nunca sabremos cuándo se acabó el juego. Deshabilite la colisión con la parte inferior del mundo del juego agregando esta línea al final de la función crear:

1
this.physics.world.checkCollision.down = false;

Ahora deberíamos tener un juego como este:

La pelota cae al fondo del mundo del juego

Colisión de ladrillos {#colisión de ladrillos}

Ahora que nuestros sprites en movimiento chocan correctamente con nuestro mundo de juego, trabajemos en la colisión entre la pelota y los ladrillos y luego la pelota y el jugador.

En nuestra función de creación en las siguientes líneas de código hasta el final:

1
2
3
this.physics.add.collider(ball, violetBricks, hitBrick, null, this);
this.physics.add.collider(ball, yellowBricks, hitBrick, null, this);
this.physics.add.collider(ball, redBricks, hitBrick, null, this);

El método del colisionador le dice al sistema de física de Phaser que ejecute la función ‘hitBrick’ cuando la ‘bola’ choca con varios grupos de sprites de ladrillos.

Cada vez que pulsamos la barra espaciadora, la pelota sale disparada hacia arriba. No hay velocidad X, por lo que la pelota regresaría directamente a la paleta. Sería un juego aburrido. Por lo tanto, cuando golpeemos un ladrillo por primera vez, estableceremos una velocidad X. En la parte inferior de breakout.js, defina hitBrick a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function hitBrick(ball, brick) {
  brick.disableBody(true, true);

  if (ball.body.velocity.x == 0) {
    randNum = Math.random();
    if (randNum >= 0.5) {
      ball.body.setVelocityX(150);
    } else {
      ball.body.setVelocityX(-150);
    }
  }
}

La función hitBrick acepta los dos argumentos anteriores que se usaron en el método collider, por ejemplo, ball y violetBricks. La llamada disableBody(true, true) en el bloque le dice a Phaser que lo desactive y lo oculte de la pantalla. Si la velocidad X de la pelota es 0, entonces le damos a la pelota una velocidad que depende del valor de un número aleatorio.

Si una pelota pequeña rueda hacia su pie a un ritmo lento, al chocar se detendría. El motor Arcade Physics modela el impacto que tiene la colisión en la velocidad de forma predeterminada. Para nuestro juego, no queremos que la pelota pierda velocidad cuando golpea un ladrillo. Necesitamos establecer la propiedad inamovible para nuestros grupos de sprites en true. Actualice las definiciones de violetBricks, yellowBricks y redBricks a lo siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Add violet bricks
violetBricks = this.physics.add.group({
  key: 'brick1',
  repeat: 9,
  immovable: true,
  setXY: {
    x: 80,
    y: 140,
    stepX: 70
  }
});

// Add yellow bricks
yellowBricks = this.physics.add.group({
  key: 'brick2',
  repeat: 9,
  immovable: true,
  setXY: {
    x: 80,
    y: 90,
    stepX: 70
  }
});

// Add red bricks
redBricks = this.physics.add.group({
  key: 'brick3',
  repeat: 9,
  immovable: true,
  setXY: {
    x: 80,
    y: 40,
    stepX: 70
  }
});

Nuestra colisión de ladrillos ahora está completa y nuestro juego debería funcionar así:

Colisión de ladrillos

Sugerencia de desarrollo: al desarrollar la física de su juego, es posible que desee habilitar el modo de depuración para ver los cuadros de límites de sus sprites y cómo chocan entre sí. En el objeto config de tu juego, dentro de la propiedad arcade donde definimos gravedad, puedes habilitar la depuración agregando esto al objeto:

1
debug: true

Colisión de jugadores {#colisión de jugadores}

Manejar las colisiones entre la pelota y el jugador es un esfuerzo similar. Primero, asegurémonos de que el jugador sea inamovible. Al final de la función crear agregue lo siguiente:

1
player.setImmovable(true);

Y luego agregamos un colisionador entre la pelota y el jugador:

1
this.physics.add.collider(ball, player, hitPlayer, null, this);

Cuando la pelota golpea al jugador, queremos que sucedan dos cosas:

  • La pelota debe moverse un poco más rápido, para aumentar gradualmente la dificultad del juego
  • La dirección horizontal de la pelota depende del lado del jugador que golpee - si la pelota golpea el lado izquierdo del jugador, entonces debe ir hacia la izquierda, si golpea el lado derecho del jugador, entonces debe ir hacia la derecha .

En la parte inferior de su breakout.js agregue la función hitPlayer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function hitPlayer(ball, player) {
  // Increase the velocity of the ball after it bounces
  ball.setVelocityY(ball.body.velocity.y - 5);

  let newXVelocity = Math.abs(ball.body.velocity.x) + 5;
  // If the ball is to the left of the player, ensure the X Velocity is negative
  if (ball.x < player.x) {
    ball.setVelocityX(-newXVelocity);
  } else {
    ball.setVelocityX(newXVelocity);
  }
}

Nota: un sprite puede chocar con otro, un sprite puede chocar con un grupo de sprite y los grupos de sprite pueden chocar entre sí. Phaser es lo suficientemente inteligente como para usar la función de colisión que definimos apropiada en el contexto.

Ahora nuestro juego tiene colisión de jugador y ladrillo:

Colisión de jugadores

Adición de texto

Si bien nuestro juego funciona completamente, alguien que juegue este juego no tendría idea de cómo comenzar o saber cuándo termina el juego. Agregue 3 variables globales de noticias que almacenarán nuestros datos de texto después de la declaración gameStarted en la parte superior de breakout.js:

1
let openingText, gameOverText, playerWonText;

Texto de apertura

Agreguemos algo de texto cuando el juego esté cargado para decirle al jugador que presione espacio. En la función crear agrega el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
openingText = this.add.text(
  this.physics.world.bounds.width / 2,
  this.physics.world.bounds.height / 2,
  'Press SPACE to Start',
  {
    fontFamily: 'Monaco, Courier, monospace',
    fontSize: '50px',
    fill: '#fff'
  },
);

openingText.setOrigin(0.5);

Los dos primeros argumentos del método texto son las coordenadas X e Y del cuadro de texto. Usamos el ancho y la altura de la escena del juego para determinar dónde se coloca: en el centro. El tercer argumento es el texto a mostrar. El cuarto argumento es un objeto JS que contiene datos relacionados con la fuente.

A diferencia de los sprites, las coordenadas X e Y de los objetos de texto se refieren a su punto superior izquierdo del objeto, no a su centro. Es por eso que usamos el método setOrigin para hacer que el sistema de coordenadas funcione como sprites, en este caso hace que sea más fácil posicionarlo en el centro.

Cuando estamos jugando, ya no queremos ver el texto de apertura. En la función actualizar, cambie la instrucción if que verifica si se presionó la barra espaciadora a lo siguiente:

1
2
3
4
5
if (cursors.space.isDown) {
  gameStarted = true;
  ball.setVelocityY(-200);
  openingText.setVisible(false);
}

Los objetos de texto no son sprites, no podemos desactivar sus cuerpos. Podemos hacerlos invisibles cuando no necesitamos verlos. Nuestro juego ahora comienza así:

El texto de apertura inicia el juego

Game Over y Game Won Text

Como antes, necesitamos agregar los objetos de texto en la función crear y hacerlos invisibles para que no se vean cuando se inicie el juego:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Create game over text
gameOverText = this.add.text(
  this.physics.world.bounds.width / 2,
  this.physics.world.bounds.height / 2,
  'Game Over',
  {
    fontFamily: 'Monaco, Courier, monospace',
    fontSize: '50px',
    fill: '#fff'
  },
);

gameOverText.setOrigin(0.5);

// Make it invisible until the player loses
gameOverText.setVisible(false);

// Create the game won text
playerWonText = this.add.text(
  this.physics.world.bounds.width / 2,
  this.physics.world.bounds.height / 2,
  'You won!',
  {
    fontFamily: 'Monaco, Courier, monospace',
    fontSize: '50px',
    fill: '#fff'
  },
);

playerWonText.setOrigin(0.5);

// Make it invisible until the player wins
playerWonText.setVisible(false);

Ahora que están definidos, tenemos que cambiar su visibilidad en la función actualizar:

1
2
3
4
5
6
7
8
9
// Check if the ball left the scene i.e. game over
if (isGameOver(this.physics.world)) {
  gameOverText.setVisible(true);
  ball.disableBody(true, true);
} else if (isWon()) {
  playerWonText.setVisible(true);
  ball.disableBody(true, true);
} else {
  ...

Deshabilitamos el cuerpo de la pelota para que deje de actualizarse y mostrarse porque ya no es necesario.

Si perdemos el juego, veremos esto:

Game Over

Si ganamos el juego, veremos esto:

Juego ganado

¡Nuestro clon Breakout está completo!

Conclusión

Phaser es un marco de desarrollo de juegos HTML5 que nos permite crear rápidamente videojuegos en la web. Además de abstraerse de las API de HTML5, también nos brinda utilidades útiles como motores de física y administra el bucle del juego: el ciclo de vida de ejecución de todos los juegos.

Construimos un clon de Breakout por:

  • Cargando nuestros activos, en este caso nuestras imágenes
  • Creación de sprites para nuestro jugador y pelota, y grupos de sprites para nuestros ladrillos
  • Escuchar los eventos del teclado para poder mover al jugador
  • Usar el motor de física para manejar las colisiones entre los objetos sprites y el mundo del juego
  • Mostrar texto para instruir al jugador e informarle si ganó o perdió

Se puede utilizar un enfoque de desarrollo similar para crear otros juegos en Phaser.

La mejor manera de mejorar tus habilidades de desarrollo de juegos es seguir creando juegos. Si desea obtener más información sobre el desarrollo de juegos con Phaser, eche un vistazo al [tutorial] introductorio del sitio web oficial (http://phaser.io/tutorials/making-your-first-phaser-3-game ).

Puedes ver el código fuente anotado del juego aquí. -breakout).