> Manuales > Manual de Phaser

Qué son sprites, cómo usarlos para que algunos elementos del juego desarrollado con Phaser 3 se muestren con animaciones sencillas gracias a la progresión de varias imágenes.

Animaciones con sprites en Phaser

Otra de las claves fundamentales para crear juegos atractivos son las animaciones. En Phaser podemos conseguir animaciones de diversas maneras y una de las más sencillas y típicas la conseguimos a través de los sprites.

En los juegos habitualmente tenemos personajes que al moverse van cambiando su aspecto, para que parezca que están corriendo por ejemplo. Hasta ahora en nuestro juego de demostración, que hemos ido construyendo en el Manual de Phaser3, no habíamos tenido la necesidad de usar sprites, pero ahora vamos a incorporar nuestro primer elemento animado.

La animación la realizaremos para los poderes especiales que salen de vez en cuando al romper algunos ladrillos. Esos elementos aparecerán en pantalla y se moverán de diversas maneras.

La imagen con los distintos fotogramas

Para comenzar una animación basada en sprites tenemos que componer la secuencia de imágenes que forman la animación. Esa imagen tendrá todos los fotogramas de la animación, uno al lado del otro, pero en el mismo archivo gráfico.

Para nuestro juego vamos a realizar una animación de un diamante, que hace un movimiento similar a como si girase sobre sí mismo. Los fotogramas los podemos ver aquí.

imagen con los sprites del diamante

En este artículo te proporcionamos una imagen válida para conseguir la animación con sprites. Puedes hacer tus propias imágenes fácilmente, pero si estás aprendiendo y no te apetece parar mucho con esta parte de crear las imágenes de los fotogramas de la animación, también puedes conseguir en Internet muchas imágenes con sprites que te servirán para hacer todo tipo de animaciones, para tus personajes, enemigos, etc.

Es una secuencia de imágenes muy "casera" y no demasiado bonita, ya que que mi fuerte no es el diseño gráfico y la he hecho yo mismo a mano, pero lo importante es que veamos la progresión de la animación en el sprite que vamos a usar. Además nos servirá perfectamente para este ejercicio y para el estilo de juego que estamos realizando.

Cómo precargar un sprite

Ya en la parte de la programación de la animación, tenemos que comenzar por hacer una precarga del archivo gráfico con los sprices, igual que hacemos con las imágenes del juego, desde el método preload().

this.load.spritesheet('bluediamond',
      'images/blue_diamond-sprites.png',
      { frameWidth: 48, frameHeight: 48 }
);

Al cargar este sprite usamos un método nuevo que no habíamos visto todavía: spritesheet(). En él, indicamos el identificador y el archivo donde tenemos la secuencia de imágenes. Lo que difiere de la carga de imágenes normales es que tenemos que indicar la anchura y la altura de los fotogramas. Como puedes comprobar, todos los fotogramas en una misma animación tendrán las mismas dimensiones, que son 48 píxeles de ancho y de alto.

Todos los fotogramas de una misma animación tendrán idéntico tamaño, pero no necesariamente tienen que ser siempre cuadrados. Obviamente, cada sprite del juego tendrá las dimensiones que hagan falta, unos actores podrán ser mayores que otros, más alargados o más cuadrados, etc.

Cómo asociar un sprite a la escena

Asociar un sprite a la escena es tan sencillo como lo era asociar una imagen común en el juego, solo que usamos add.sprite() en lugar de add.image().

this.add.sprite(40, 40, 'bluediamond');

Este código se insertará desde el método create() de la escena, siendo "this" la propia escena.

Cómo crear la animación

Ahora viene el punto interesante y novedoso para incorporar las animaciones, que es decirle a Phaser cómo debe realizar la animación del elemento. Para ello existe un método especial en la escena llamado anims.create().

this.anims.create({
      key: 'bluediamondanimation',
      frames: this.anims.generateFrameNumbers('bluediamond', { start: 0, end: 7 }),
      frameRate: 10,
      repeat: -1,
      yoyo: true,
});

Existen una serie de configuraciones posibles para la animación. Lo que siempre tendremos será el identificador que le queremos asignar. En este caso se ha puesto 'bluediamondanimation'.

Además, siempre tendremos que indicar qué frames, de entre todos los fotogramas de la imagen, son usados para la animación que estamos creando. Ten en cuenta que en una imágen podrían haber fotogramas para diversas animaciones de un personaje, por ejemplo cuando camina hacia la parte de la derecha, la izquierda, cuando salta, etc.

El framerate es la cantidad de invocaciones en el método update que transcurren entre cada fotograma de la animación. Repeat sirve para indicar las veces que esta animación se repite, aunque en este caso con el valor -1 estamos indicando que se repita de manera constante. El atributo "yoyo" hace que la animación sea de ida y vuelta, es decir, comienza en el primer frame, va hasta el último y, en vez de comenzar de nuevo con el primero, realiza la secuencia de fotogramas en sentido contrario, del último al primero.

Existen muchas configuraciones adicionales que podemos consultar en la documentación.

Cómo asociar la animación al elemento

Una vez "declarada" la animación, podemos asociarla al sprite con una invocación anims.play() del sprite que hemos añadido a la escena.

this.miSprite = this.add.sprite(40, 40, 'bluediamond');
this.miSprite.anims.play('bluediamondanimation');

Así el elemento se mostrará en en juego con una animación, que hemos configurado de manera constante.

Cómo usar el sistema de físicas

Por supuesto, si nuestra animación se aplica a un elemento que necesitamos que procese las colisiones con otros actores del juego, necesitamos crearlo con el sistema de físicas que ya conocemos.

this.miSprite = this.physics.add.sprite(40, 40, 'bluediamond');
this.miSprite.anims.play('bluediamondanimation');
this.physics.add.collider(this.ball, this.miSprite);

Cómo añadir sprites animados a un grupo

También podría ocurrir que tengamos elementos, que queremos que formen parte de un grupo. El trabajo es idéntico a los grupos de imágenes.

Creamos el grupo de diamantes.

this.diamonds = this.relatedScene.physics.add.group();

Añadimos elementos al grupo:

let diamond = this.diamonds.create(x, y, 'bluediamondanimation')

Y lo bueno de esto es que podemos añadir las colisiones que queramos gestionar, todas de una vez para todos los elementos del grupo.

this.relatedScene.physics.add.collider(this.ball, this.diamonds, this.ballImpact, null, this);

Aplicar características individuales a los elementos del grupo

Cada uno de los elementos del grupo puede tener sus propias características específicas, que podemos asignar en cualquier momento, por ejemplo cuando lo creamos.

let diamond = this.diamonds.create(x, y, sprite)
diamond.setScale(0.6);
diamond.anims.play(sprite + 'animation');
diamond.body.setAllowRotation();
diamond.body.setAngularVelocity(100);
diamond.body.setVelocity(Phaser.Math.Between(-100, 100), Phaser.Math.Between(-100, 100));
diamond.setBounce(1);
diamond.setCollideWorldBounds(true);

Al ejecutarse este código, los diamantes que creemos se basarán en un sprite contenido en la variable "sprite". Todos se redimensionarán a un 60% del tamaño (siempre igual), pero la animación cambiará, porque estamos usando la variable "sprite" para indicar su nombre. Además todos los elementos estarán moviéndose en un ángulo, con una misma velocidad, pero la dirección de su movimiento podrá cambiar porque setVelocity() usa valores aleatorios.

Componente Diamonds

Para acabar vamos a ver un componente completo que implementa los diamantes con sus animaciones y comportamientos. Este componente, como sabes, nos permite dejar toda la complejidad de los diamantes localizada en un módulo para facilitar su mantenimiento. Por supuesto, lo vamos a usar en el juego "breakout" para crear diamantes que se moverán por la pantalla. La idea es que, cuando la bola impacte con estos diamantes se asignen poderes especiales al jugador.

Todos los diamantes pertenecerán a un grupo. Cuando se construya un objeto de la clase Diamonds se creará el grupo. Luego se crearán diamantes bajo demanda, con configuraciones parametrizadas. Además los propios diamantes tendrán configuradas en este mismo componente las colisiones con la bola.

El código del componente, aplicando el conocimiento nuevo que has aprendido en este artículo, lo tenemos por aquí.

export class Diamonds {
  constructor(scene) {
    this.relatedScene = scene;
    this.diamonds = this.relatedScene.physics.add.group();
    this.relatedScene.physics.add.collider(this.relatedScene.ball.get(), this.diamonds, this.ballImpact, null, this);
  }
  
  create(x, y, sprite, relatedPower) {
    let diamond = this.diamonds.create(x, y, sprite)
    diamond.relatedPower = relatedPower;
    diamond.setScale(0.6);
    diamond.anims.play(sprite + 'animation');
    diamond.body.setAllowRotation();
    diamond.body.setAngularVelocity(100);
    diamond.body.setVelocity(Phaser.Math.Between(-100, 100), Phaser.Math.Between(-100, 100));
    diamond.setBounce(1);
    diamond.setCollideWorldBounds(true);
  }

  ballImpact(ball, diamond) {
    diamond.destroy();
    diamond.relatedPower.givePower();
    let currentVelocity = ball.body.velocity;
    this.relatedScene.removeGlueFromBall();
    if(currentVelocity.y > 0) {
      ball.body.setVelocityY(300);
    } else {
      ball.body.setVelocityY(-300);
    }
  }
}

Los diamantes estarán asociados a un poder, que se asignará al crear el diamante. Cada diamante tendrá también un sprite y animación parametrizadas.

El impacto con la bola hará que el diamante desaparezca y dará el poder al jugador. Además, para evitar que la bola se vea frenada como consecuencia del impacto, hacemos que, después del impacto la bola tenga siempre una velocidad vertical de 300. Para implementar este comportamiento quizás haya un condicional que te puede despistar:

if(currentVelocity.y > 0) {
      ball.body.setVelocityY(300);
} else {
      ball.body.setVelocityY(-300);
}

Eso sirve para saber si la bola estaba moviéndose hacia arriba o hacia abajo después del impacto. Si era hacia arriba entonces asignamos una velocidad negativa y si era hacia abajo la velocidad es positiva.

Las colisiones con los ladrillos se configurarán en la propia configuración de la fase del juego, ya que la fase es quien sabe qué ladrillos están en cada momento.

Este componente todavía lo tenemos que modificar algo, para conseguir aplicar la parte que nos falta, que es crear los poderes especiales que conseguiremos cuando capturemos los diamantes. Pero esto ya lo veremos en el siguiente artículo.

Hasta aquí hemos aprendido a crear y configurar animaciones en base a sprites, lo que es un gran avance. Lo cierto es que no es nada complejo gracias al motor de juevos Phaser.

Miguel Angel Alvarez

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

Manual