Phaser 3 y Tiled: creación de un juego de plataformas

Phaser 3 nos permite crear juegos rápidamente en nuestro navegador con JavaScript. Algunos de nuestros juegos 2D favoritos son los juegos de plataformas: piensa en juegos como Mario, Sonic, S...

Introducción

Phaser 3 nos permite crear juegos rápidamente en nuestro navegador con JavaScript. Algunos de nuestros juegos 2D favoritos son los juegos de plataformas: piensa en juegos como Mario, Sonic, Super Meat Boy o Cuphead.

embaldosado es un editor de mapas 2D que se utiliza para crear mundos de juegos. Exploraremos cómo crear un nivel de plataforma con Tiled, integrarlo con Phaser y animar sprites para crear una rica experiencia de plataforma 2D.

En este artículo, crearemos un juego de plataformas básico, donde nuestro jugador puede moverse y saltar en nuestro mundo. Si el jugador golpea un pico, restablecemos la posición del jugador. Se puede encontrar una demostración jugable de este juego aquí.

Este tutorial está escrito para aquellos familiarizados con Phaser 3. Si no lo estás, familiarízate con el marco con uno de nuestros artículos anteriores sobre Phaser .

Comenzando

Para seguir mejor este tutorial, descargue y descomprima el proyecto [wikihtp-plataforma.zip](https://wikihtp.s3.amazonaws.com/files/phaser-3-and-tiled-building-a-platformer-assets .zip) en su espacio de trabajo. La carpeta debe incluir los siguientes recursos:

  • index.html: Carga Phaser 3.17 y nuestro archivo game.js
  • game.js: Contiene la lógica de nuestro juego
  • activos/imágenes:
    • background.png
    • kenney_player.png
    • kenney_player_atlas.json
    • spike.png
  • assets/tilemaps: carpeta vacía, se usará para guardar archivos en mosaico
  • activos/conjuntos de fichas:
    • platformPack_tilesheet.png

Nota: si lo prefiere, también puede seguirlo viendo el código del proyecto en nuestro [repositorio de GitHub](https://github.com/wikihtp/creating-a-platformer-with -fase-3).

No olvide ejecutar un servidor en la carpeta de su proyecto, con su IDE o incluso con Python: python3 -m http.server. Esto es necesario para que Phaser pueda cargar estos activos a través de HTTP. Nuevamente, para obtener más información, consulte nuestro artículo anterior sobre el tema (vinculado arriba).

Todos los recursos del juego fueron creados y compartidos por Kenney. El archivo atlas fue creado con Empacadora Atlas Phaser.

Editor de mapas en mosaico

Tiled es un software gratuito y de código abierto para crear niveles de juego. Está disponible en todos los principales sistemas operativos de escritorio, así que visite el sitio web y descárguelo para continuar.

Creación de un mapa de mosaicos

Abra Tiled y haga clic en "Nuevo mapa". En el indicador, cambie el formato de capa de mosaico a "Base64 (sin comprimir)", el ancho a 14 mosaicos y la altura a 7, y el tamaño de mosaico a 64 px cada uno.

New Map

Guarde el archivo como "level1.tmx" en "assets/tilemaps".

Creación de un conjunto de mosaicos

En el panel derecho, haga clic en "Nuevo conjunto de fichas...". En la ventana emergente, nombre el conjunto de fichas "kenny_simple_platformer". Asegúrese de que la opción "Incrustar en el mapa" esté seleccionada. Sin esa opción, Phaser puede tener problemas para cargar su mapa correctamente. En la propiedad "Fuente", seleccione "platformPack_tilesheet.png" del directorio "assets/tilesets".

El ancho de la imagen de la hoja de mosaico es de 896 px y la altura es de 448 px. Contiene 98 imágenes en total del mismo tamaño, todas caben en 7 filas y 14 columnas. Con matemáticas básicas podemos deducir que cada mosaico tiene 64 px de ancho y alto. Asegúrese de que el ancho y la altura del mosaico sea de 64 px:

New Tileset

Diseñando nuestro nivel

Los mapas en Tiled se componen de capas. Cada capa almacena algún diseño del mundo del juego. Las capas que están en la parte superior muestran sus mosaicos sobre las capas que están debajo. Obtenemos profundidad usándolos. Este juego básico tendrá solo dos capas:

  • Plataforma: contiene el mundo con el que interactúa el jugador
  • Picos: contiene los peligrosos picos que pueden lastimar al jugador.

La capa de plataforma

Antes de agregar nuestros mosaicos al mapa, primero cambiemos el nombre de la capa. Se hará referencia a los nombres de las capas en nuestro código Phaser, así que cambiemos "Tiled Layer 1" a "Platforms":

Change Layer Name

Para crear un nivel, simplemente seleccione un mosaico de su conjunto de mosaicos y haga clic en el lugar donde le gustaría colocarlo en el mapa. Vamos a crear/agregar todas nuestras plataformas:

Add Platforms

Picos en la capa de objetos

En el panel Capas a la derecha de la pantalla, haga clic en el botón "Nueva capa" y seleccione "Capa de objetos". Nombra la capa "Spikes".

Add Spike Layer

En la barra de herramientas superior, seleccione la opción "Insertar objeto":

Insert Object

Ahora podemos agregar los mosaicos de picos del conjunto de mosaicos:

Add Spikes

¡Hemos creado nuestro nivel de juego! Ahora necesitamos integrarlo con Phaser.

Carga de un mapa en mosaico

Phaser no puede leer el archivo .tmx que creó Tiled. Primero, exportemos nuestro mapa a JSON. Haga clic en "Archivo -> Exportar como", seleccione JSON como formato y asígnele el nombre "level1.json" en la carpeta tilemaps. Al igual que con todos los proyectos de Phaser, nuestros recursos deben cargarse en nuestra función preload():

1
2
3
4
5
6
7
8
9
function preload() {
  this.load.image('background', 'assets/images/background.png');
  this.load.image('spike', 'assets/images/spike.png');
  // At last image must be loaded with its JSON
  this.load.atlas('player', 'assets/images/kenney_player.png','assets/images/kenney_player_atlas.json');
  this.load.image('tiles', 'assets/tilesets/platformPack_tilesheet.png');
  // Load the export Tiled JSON
  this.load.tilemapTiledJSON('map', 'assets/tilemaps/level1.json');
}

Nota: Es posible que se pregunte por qué tenemos que cargar la imagen del pico por separado si está incluida en el mapa de mosaicos. Desafortunadamente, este bit de duplicación es necesario para que los objetos se muestren correctamente.

En nuestra función create(), primero agreguemos el fondo y lo escalemos para nuestra resolución:

1
2
const backgroundImage = this.add.image(0, 0,'background').setOrigin(0, 0);
backgroundImage.setScale(2, 0.8);

Entonces agreguemos nuestro mapa:

1
const map = this.make.tilemap({ key: 'map' });

La clave coincide con el nombre dado en la función preload() cuando cargamos el Tiled JSON. También tenemos que agregar la imagen del conjunto de fichas a nuestro objeto mapa de Phaser:

1
const tileset = map.addTilesetImage('kenney_simple_platformer', 'tiles');

El primer argumento de addTilesetImage es el nombre del conjunto de mosaicos que usamos en Tiled. El segundo argumento es la clave de la imagen que cargamos en la función preload().

Ahora podemos agregar nuestra capa de plataforma:

1
const platforms = map.createStaticLayer('Platforms', tileset, 0, 200);

Y debería ver esto:

Platforms Added

De forma predeterminada, Phaser no gestiona las colisiones de nuestras capas en mosaico. Si agregáramos nuestro reproductor ahora, caería completamente a través de los mosaicos de la plataforma. Digamos a Phaser que la capa puede chocar con otros objetos:

1
platforms.setCollisionByExclusion(-1, true);

Cada mosaico en nuestro mapa recibió un índice de Tiled para hacer referencia a lo que debería mostrarse allí. Un índice de nuestra plataforma solo puede ser mayor que 0. setCollisionByExclusion le dice a Phaser que habilite las colisiones para cada mosaico cuyo índice no sea -1, por lo tanto, todos los mosaicos.

Atlas de texturas

La animación de nuestro reproductor se almacena en un atlas de texturas, una imagen que contiene imágenes más pequeñas. Al igual que las hojas de sprites, reducen la actividad de la red al cargar un archivo. La mayoría de los atlas de texturas contienen mucho más que solo información de sprites.

Echemos un vistazo a nuestro archivo de imagen: "kenney_player.png":

Kenney Player

Nuestro atlas contiene 8 cuadros: los cuadros 0 a 3 están arriba y los cuadros 4 a 7 están abajo. Por sí mismo, esto no es tan útil para Phaser, por eso viene con un archivo JSON: "kenney_player_atlas.json".

El archivo tiene una matriz de “marcos” que contiene información sobre cada imagen individual que compone el atlas.

Para usar el atlas, necesitará conocer la propiedad filename de los marcos que está usando.

Adición de un jugador

Con nuestro mundo configurado, podemos agregar al jugador y hacer que interactúe con nuestras plataformas. En nuestra función crear agreguemos lo siguiente:

1
2
3
4
this.player = this.physics.add.sprite(50, 300, 'player');
this.player.setBounce(0.1);
this.player.setCollideWorldBounds(true);
this.physics.add.collider(this.player, platforms);

De forma predeterminada, Phaser usa el primer cuadro del atlas, si quisiéramos comenzar en un cuadro diferente, podríamos haber agregado un argumento siguiente al método sprite con la propiedad nombre de archivo de la imagen del atlas, p. robo_jugador_3.

La propiedad de rebote solo agrega un poco de vivacidad cuando nuestro jugador salta y aterriza. Y configuramos al jugador para que colisione con nuestro mundo de juego y las plataformas. Ahora deberíamos ver a nuestro jugador parado en nuestras plataformas:

Player added

El cuadro morado existe alrededor de nuestro reproductor porque el modo depuración está habilitado para nuestros motores de física. El modo de depuración muestra los límites que determinan cómo colisionan nuestros sprites.

Adición de animaciones

Recuerde que nuestro atlas de texturas tenía 8 marcos para el movimiento del jugador. Phaser nos permite crear animaciones a partir de los fotogramas de una imagen de atlas. Vamos a crear una animación para caminar usando los dos últimos fotogramas de la primera fila del atlas a través de nuestra función create():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
this.anims.create({
  key: 'walk',
  frames: this.anims.generateFrameNames('player', {
    prefix: 'robo_player_',
    start: 2,
    end: 3,
  }),
  frameRate: 10,
  repeat: -1
});

La propiedad key es la cadena que usamos para reproducir la animación más tarde. La propiedad frames es una matriz de fotogramas en nuestro archivo JSON de atlas que contiene la animación. La animación comienza en el primer cuadro de la matriz y termina en el último. Usamos la función auxiliar generateFrameNames() para crear la lista de nombres de marcos para nosotros, una función muy útil para archivos grandes de atlas.

El valor predeterminado de frameRate es 24 fotogramas por segundo, lo que puede ser demasiado rápido para nuestro reproductor, por lo que lo configuramos en 10. Cuando configuramos repeat en -1, le estamos diciendo a Phaser que ejecute esta animación infinitamente.

Agreguemos las animaciones para nuestro sprite inactivo, el primer cuadro del atlas:

1
2
3
4
5
this.anims.create({
  key: 'idle',
  frames: [{ key: 'player', frame: 'robo_player_0' }],
  frameRate: 10,
});

Nuestra animación inactiva es simplemente un cuadro. Agreguemos una animación para cuando nuestro jugador salte, que también es solo un cuadro:

1
2
3
4
5
this.anims.create({
  key: 'jump',
  frames: [{ key: 'player', frame: 'robo_player_1' }],
  frameRate: 10,
});

Con nuestras animaciones agregadas, debemos habilitar las teclas del cursor para poder mover nuestro reproductor:

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

Animando a nuestro jugador

Si nuestro jugador se mueve hacia la izquierda o hacia la derecha, entonces queremos caminar. Si presionamos la barra espaciadora o hacia arriba, queremos saltar. De lo contrario, permaneceremos en nuestra posición inactiva. Implementemos esto en nuestra función update():

 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
// Control the player with left or right keys
if (this.cursors.left.isDown) {
  this.player.setVelocityX(-200);
  if (this.player.body.onFloor()) {
    this.player.play('walk', true);
  }
} else if (this.cursors.right.isDown) {
  this.player.setVelocityX(200);
  if (this.player.body.onFloor()) {
    this.player.play('walk', true);
  }
} else {
  // If no keys are pressed, the player keeps still
  this.player.setVelocityX(0);
  // Only show the idle animation if the player is footed
  // If this is not included, the player would look idle while jumping
  if (this.player.body.onFloor()) {
    this.player.play('idle', true);
  }
}

// Player can jump while walking any direction by pressing the space bar
// or the 'UP' arrow
if ((this.cursors.space.isDown || this.cursors.up.isDown) && this.player.body.onFloor()) {
  this.player.setVelocityY(-350);
  this.player.play('jump', true);
}

Animar un sprite es tan fácil como establecer la animación en verdadero. Si estaba atento, notará que nuestro atlas solo tiene movimientos orientados hacia la derecha. Si nos estamos moviendo hacia la izquierda, ya sea caminando o saltando, queremos voltear el sprite en el eje x. Si nos movemos hacia la derecha, queremos voltearlo hacia atrás.

Podemos lograr este objetivo con el siguiente fragmento de código:

1
2
3
4
5
6
if (this.player.body.velocity.x > 0) {
  this.player.setFlipX(false);
} else if (this.player.body.velocity.x < 0) {
  // otherwise, make them face the other side
  this.player.setFlipX(true);
}

¡Ahora nuestro jugador se mueve por el juego con un estilo bien animado!

Player Moving

Adición de picos

Phaser nos proporciona muchas formas de obtener sprites de nuestra capa de objetos. Los picos se almacenan dentro de una matriz en nuestro objeto de mapa en mosaico. Cada pico obligaría a nuestro jugador a comenzar de nuevo si lo golpea. Tiene sentido para nosotros poner todos los picos en un grupo de sprites y establecer colisiones entre el jugador y el grupo. Cuando se configura una colisión con un grupo de sprites, se aplica a todos los sprites.

En la función create() agrega lo siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Create a sprite group for all spikes, set common properties to ensure that
// sprites in the group don't move via gravity or by player collisions
 this.spikes = this.physics.add.group({
    allowGravity: false,
    immovable: true
  });
  
// Let's get the spike objects, these are NOT sprites
// We'll create spikes in our sprite group for each object in our map
map.getObjectLayer('Spikes').objects.forEach((spike) => {
    // Add new spikes to our sprite group
    const spikeSprite = this.spikes.create(spike.x, spike.y + 200 - spike.height, 'spike').setOrigin(0);
});

Deberíamos obtener esto:

Spikes Added

El límite de colisión del sprite de picos es mucho más alto que los picos mismos. Si no se modifica, puede crear una mala experiencia de juego. ¡Los jugadores restablecerían su posición sin tocar el sprite! Ajustemos los cuerpos de las púas para que sean más pequeños en tamaño, particularmente en altura. Reemplace forEach con esto:

1
2
3
4
map.getObjectLayer('Spikes').objects.forEach((spike) => {
    const spikeSprite = this.spikes.create(spike.x, spike.y + 200 - spike.height, 'spike').setOrigin(0);
    spikeSprite.body.setSize(spike.width, spike.height - 20).setOffset(0, 20);
});

Para mantener el cuadro delimitador que abarca correctamente los picos, agregamos un desplazamiento que coincida con la reducción de altura. Ahora tenemos sprites de picos más apropiados:

Spikes Added

Colisión con el jugador

Si nuestro jugador choca con un pincho, su posición se reinicia. Es común en los juegos de plataformas que los jugadores tengan una animación de “perder”. Agreguemos una animación parpadeante cuando nuestro reproductor se reinicie. Primero, en create() agreguemos la colisión:

1
this.physics.add.collider(this.player, this.spikes, playerHit, null, this);

La lógica para el restablecimiento del jugador estará en la función playerHit(). Cada vez que el jugador colisione con un sprite del grupo de sprite de picos, se llamará a esta función. Al final del archivo agregue lo siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function playerHit(player, spike) {
  player.setVelocity(0, 0);
  player.setX(50);
  player.setY(300);
  player.play('idle', true);
  player.setAlpha(0);
  let tw = this.tweens.add({
    targets: player,
    alpha: 1,
    duration: 100,
    ease: 'Linear',
    repeat: 5,
  });
}

Aquí están pasando bastantes cosas. Tomemos cada instrucción línea por línea:

  • Establezca la velocidad del jugador en 0. Es mucho más predecible (y más seguro) detener el movimiento del jugador al reiniciar
  • Establecer las coordenadas X e Y en la primera posición del jugador
  • Use la animación inactiva, tal como estaba cuando comenzó el jugador
  • La propiedad alpha controla la opacidad de un sprite. Es un valor entre 0 y 1 donde 0 es completamente transparente y 1 es completamente opaco
  • Crear una interpolación: una 'animación' de una propiedad de un objeto del juego. La interpolación se aplica al objeto jugador que colisionó con el pico. Establece la propiedad alfa en 1 (es decir, hace que nuestro reproductor sea completamente visible). Esta interpolación dura 100 ms y la opacidad aumenta linealmente, como lo indica la propiedad facilidad. También se repite 5 veces, por lo que parece que parpadea.

Ahora nuestro juego se ve así:

Complete game

Nota: asegúrese de eliminar la propiedad debug: true de la configuración del juego antes de compartirlo con amigos, ¡nunca deje el modo de depuración en producción!

Conclusión

Con Tiled podemos diseñar mundos de juegos 2D pequeños y grandes. Es una buena práctica crear capas de profundidad dentro de nuestro mundo de juego. Luego tomamos el mundo que construimos en Tiled y lo agregamos a nuestro juego Phaser.

Agregamos la capa de la plataforma como una capa estática, haciéndola inamovible cuando el jugador choca. Luego creamos un grupo de sprites para los picos y creamos una función para manejar las colisiones entre cada pico y el jugador.

Además de crear un mundo de juego vibrante, aprendimos a animar a nuestro personaje usando un atlas: una imagen grande que contiene varias imágenes más pequeñas, acompañada de un archivo JSON que detalla qué imagen se encuentra en cada cuadro. También usamos una interpolación para cambiar una propiedad de nuestro sprite durante un período de tiempo determinado.

¡Con estas técnicas, depende de ti crear el próximo mejor juego de plataformas con Phaser!

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