Phaser 3 y Tiled: construyendo un juego de plataformas

P

Introducción

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

Azulejos es un editor de mapas 2D que se utiliza para crear mundos de juegos. Exploraremos cómo crear un nivel de plataformas con Tiled, integrarlo con Phaser y animar sprites para crear una rica experiencia de plataformas 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, entonces reiniciamos 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á, familiarícese con el marco con uno de nuestros artículos anteriores sobre Phaser.

Empezando

Para seguir mejor este tutorial, descargue y descomprima el proyecto Pharos.sh-platformer.zip en su espacio de trabajo. La carpeta debe incluir los siguientes activos:

  • index.html: Carga Phaser 3.17 y nuestro game.js archivo
  • 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 utilizará para guardar archivos en mosaico
  • activos / conjuntos de mosaicos:
    • platformPack_tilesheet.png

Nota: Si lo prefiere, también puede seguir leyendo el código del proyecto en nuestra Repositorio de GitHub.

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 activos del juego fueron creados y compartidos por Kenney. El archivo atlas fue creado con Empaquetadora 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árgalo para continuar.

Crear un mapa de mosaicos

Abra Tiled y haga clic en “Nuevo mapa”. En el mensaje, cambie el formato de la 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.

Guarde el archivo como “level1.tmx” en “assets / tilemaps”.

Crear un conjunto de mosaicos

En el panel derecho, haga clic en “Nuevo conjunto de mosaicos …”. En la ventana emergente, nombre el conjunto de mosaicos “kenny_simple_platformer”. Asegúrese de que la opción “Insertar en mapa” esté seleccionada. Sin esa opción, Phaser puede experimentar problemas al 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 896 px y la altura es 448 px. Contiene 98 imágenes en total de igual tamaño, todas caben en 7 filas y 14 columnas. Con matemáticas básicas podemos deducir que cada mosaico tiene 64 píxeles de ancho y alto. Asegúrese de que el ancho y la altura del conjunto de mosaicos sea de 64 px:

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 tienen sus mosaicos que se muestran 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 picos peligrosos que pueden dañar al jugador.

La capa de plataforma

Antes de agregar nuestros mosaicos al mapa, primero cambiemos el nombre de la capa. Los nombres de las capas serán referenciados en nuestro código de Phaser, así que cambiemos “Tiled Layer 1” a “Platforms”:

Para crear un nivel, simplemente seleccione un mosaico de su conjunto de mosaicos y haga clic en el lugar donde desea colocarlo en el mapa. Creemos / agreguemos todas nuestras plataformas:

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 objeto”. Nombra la capa “Spikes”.

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

Ahora podemos agregar las fichas de picos del conjunto de fichas:

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

Carga de un mapa en mosaico

Phaser no puede leer el .tmx archivo 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 el tilemaps carpeta. Al igual que con todos los proyectos de Phaser, nuestros activos deben cargarse en nuestro preload función:

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: Puede que se pregunte por qué tenemos que cargar la imagen de picos por separado si está incluida en el mapa de mosaicos. Desafortunadamente, esta pequeña duplicación es necesaria para que los objetos se muestren correctamente.

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

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

Entonces agreguemos nuestro mapa:

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

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

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 el preload función.

Ahora podemos agregar nuestra capa de plataforma:

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

Y debería ver esto:

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

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 información de sprites.

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

Nuestro atlas contiene 8 cuadros: los cuadros 0 a 3 están en la parte superior y los cuadros 4 a 7 están debajo. Por sí solo, esto no es tan útil para Phaser, por eso vino con un archivo JSON: “kenney_player_atlas.json”.

El archivo tiene un frames matriz que contiene información sobre cada imagen individual que forma el atlas.

Para utilizar el atlas, necesitará conocer el filename propiedad de los marcos que está utilizando.

Agregar un jugador

Con nuestro mundo configurado, podemos agregar el jugador e interactuar con nuestras plataformas. En nuestro create función agreguemos lo siguiente:

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);

Por defecto, Phaser usa el primer fotograma del atlas, si quisiéramos comenzar en un fotograma diferente, podríamos haber agregado un next argumento a la sprite método con el filename propiedad de la imagen del atlas, por ejemplo robo_player_3.

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

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

Agregar animaciones

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

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

los key property es la cadena que usamos para reproducir la animación más tarde. los frames La propiedad es una matriz de fotogramas en el archivo JSON de nuestro atlas que contiene la animación. La animación comienza en el primer fotograma de la matriz y termina en el último. Usamos la función de ayuda generateFrameNames para crearnos la lista de nombres de fotogramas, una función muy útil para archivos de atlas grandes.

los frameRate el valor predeterminado 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 a -1 le estamos diciendo a Phaser que ejecute esta animación infinitamente.

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

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:

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

Con nuestras animaciones agregadas, necesitamos habilitar las teclas del cursor para que podamos mover nuestro jugador:

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 pulsamos la barra espaciadora o hacia arriba, queremos saltar. De lo contrario, permaneceremos en nuestra posición inactiva. Implementemos esto en nuestro update función:

// 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 objeto es tan fácil como configurar la animación para true. Si fue un observador, notará que nuestro atlas solo tiene movimientos hacia la derecha. Si nos movemos hacia la izquierda, ya sea caminando o saltando, queremos voltear el sprite en el eje x. Si nos movemos hacia la derecha, queremos darle la vuelta.

Podemos lograr este objetivo con el siguiente código:

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!

Agregar 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 empezar de nuevo si los golpea. Tiene sentido para nosotros poner todos los picos en un grupo de sprites y configurar 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 el create función agregue lo siguiente:

// 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
const spikeObjects = map.getObjectLayer('Spikes')['objects'];

// Now we create spikes in our sprite group for each object in our map
spikeObjects.forEach(spikeObject => {
  // Add new spikes to our sprite group, change the start y position to meet the platform
  const spike = this.spikes.create(spikeObject.x, spikeObject.y + 200 - spikeObject.height, 'spike').setOrigin(0, 0);
});

Deberíamos conseguir esto:

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 objeto! Ajustemos los cuerpos de las púas para que sean más pequeños, particularmente de altura. Reemplace la forEach con este:

spikeObjects.forEach(spikeObject => {
  const spike = this.spikes.create(spikeObject.x, spikeObject.y + 200 - spikeObject.height, 'spike').setOrigin(0, 0);
  spike.body.setSize(spike.width, spike.height - 20).setOffset(0, 20);
});

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

Colisión con el jugador

Si nuestro jugador choca con un pico, 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 el create agreguemos la colisión:

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

La lógica para el reinicio del jugador estará en el playerHit función. Cada vez que el jugador choca con un sprite del grupo de sprites spike, se llamará a esta función. Al final del archivo, agregue lo siguiente:

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 sucediendo bastantes cosas. Tomemos cada instrucción línea por línea:

  • Establece la velocidad del jugador en 0. Es mucho más predecible (y más seguro) detener el movimiento del jugador al reiniciar
  • Establece las coordenadas X e Y en la primera posición del jugador.
  • Use la animación inactiva, tal como estaba cuando comenzó el reproductor
  • los alpha La propiedad controla la opacidad de un objeto. Es un valor entre 0 y 1 donde 0 es completamente transparente y 1 es completamente opaco
  • Crea una interpolación: una ‘animación’ de una propiedad de un objeto de juego. La interpolación se aplica al objeto del jugador que chocó con el pico. Establece la propiedad alpha 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 el ease propiedad. También se repite 5 veces, de ahí que parezca que parpadea.

Ahora nuestro juego se ve así:

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

Conclusión

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

Agregamos la capa de 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 cómo 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 objeto 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 Aquí.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad