Animaciones y tweens
Este es el paso 9 de los 11 del tutorial para crear un juego Breakout con JavaScript puro. Veremos cómo implementar animaciones y tweens en nuestro juego para que se vea más vistoso y vivo. Esto dará como resultado una experiencia mejor y más entretenida.
Animaciones
Una animación de sprites consiste en tomar una hoja de sprites y mostrar los sprites uno tras otro. Como ejemplo, haremos que la pelota se tambalee cuando choque con algo.
Antes que nada, descarga la hoja de sprites y guárdala en tu directorio /img.
Reemplaza la URL de la imagen de la pelota por la URL de la hoja de sprites:
const ball = new Ball("img/wobble.png", ctx);
En la clase Ball, añade un size explícito para que GameObject.preload() no use las dimensiones de toda la hoja de sprites como tamaño de la pelota. Añade también propiedades para la secuencia de fotogramas y el tiempo transcurrido desde que empezó la animación:
class Ball extends GameObject {
size = { w: 20, h: 20 };
wobbleFrames = [0, 1, 0, 2, 0, 1, 0, 2, 0];
wobbleTime = null;
// ... propiedades y métodos existentes ...
}
La secuencia de fotogramas hace referencia a los tres sprites por sus posiciones: 0, 1 y 2. Un wobbleTime de null significa que no se está reproduciendo ninguna animación, así que mostramos el fotograma 0.
Reproducir la animación
Añade los siguientes métodos a Ball, conservando sus métodos move() y onCollide() existentes:
class Ball extends GameObject {
// ...
playWobble() {
this.wobbleTime = 0;
}
updateAnimation(dt) {
if (this.wobbleTime === null) {
return;
}
this.wobbleTime += dt;
if (this.wobbleTime >= this.wobbleFrames.length * (1 / 24)) {
this.wobbleTime = null;
}
}
draw() {
const frame =
this.wobbleTime === null
? 0
: this.wobbleFrames[Math.floor(this.wobbleTime / (1 / 24))];
const { left, top } = this.hitbox;
this.ctx.drawImage(
this.asset,
frame * this.size.w,
0,
this.size.w,
this.size.h,
left,
top,
this.size.w,
this.size.h,
);
}
}
El método playWobble() inicia la animación, o la reinicia si ya se está reproduciendo. El método updateAnimation() la hace avanzar usando el tiempo transcurrido en segundos. Reproducimos la animación a 24 fotogramas por segundo, así que cada fotograma dura 1 / 24 segundos.
El método draw() sobrescrito usa la forma de nueve argumentos de ctx.drawImage(). Los cuatro primeros números después de la imagen seleccionan un rectángulo de la hoja de sprites; los cuatro últimos indican la posición y el tamaño de ese rectángulo en el canvas.
Aplicar la animación cuando la pelota golpea la paleta
Añade un método onCollide() a Paddle para iniciar la animación de la pelota cuando el sistema de colisiones informe de un golpe:
class Paddle extends GameObject {
// ...
onCollide() {
ball.playWobble();
}
}
La animación se reproduce cada vez que la pelota golpea la paleta. Si crees que el juego se verá mejor, también puedes llamar a ball.playWobble() dentro del método onCollide() del ladrillo.
Tweens
Mientras que las animaciones de sprites muestran fotogramas uno tras otro, los tweens animan de forma suave las propiedades de un objeto del mundo del juego, como el ancho o la opacidad.
Vamos a añadir un tween a nuestro juego para que los ladrillos desaparezcan de forma suave cuando la pelota los golpee. Seguimos necesitando quitar cada ladrillo de bricks y de colliders justo después de golpearlo, para que no se pueda volver a golpear ni a puntuar. Para seguir dibujándolo durante el tween, añade un array aparte junto a bricks:
const disappearingBricks = [];
Reemplaza la clase Brick por lo siguiente:
class Brick extends GameObject {
shrinkTime = 0;
constructor(url, ctx, x, y, w, h) {
super(url, ctx);
this.pos = { x, y };
this.size = { w, h };
}
draw() {
const scale = 1 - Math.min(this.shrinkTime / 0.2, 1);
const width = this.size.w * scale;
const height = this.size.h * scale;
this.ctx.drawImage(
this.asset,
this.pos.x - width / 2,
this.pos.y - height / 2,
width,
height,
);
}
onCollide() {
bricks.splice(bricks.indexOf(this), 1);
colliders.splice(colliders.indexOf(this), 1);
disappearingBricks.push(this);
score += 10;
}
}
La propiedad shrinkTime registra cuánto tiempo lleva encogiéndose el ladrillo, en segundos. Queremos que el tween dure 0,2 segundos, así que al dividirla entre 0,2 obtenemos el progreso de 0 a 1. Si restamos ese progreso a 1, obtenemos una escala que disminuye de forma lineal desde el tamaño completo hasta cero. Las coordenadas de dibujo mantienen la imagen que se encoge centrada en la posición original del ladrillo.
Ahora, después de quitar el ladrillo de la jugabilidad, se sigue registrando en disappearingBricks para poder animarlo.
Actualizar y dibujar los efectos
Reemplaza update() por lo siguiente para hacer avanzar ambos efectos en cada fotograma:
function update(timestamp) {
const dt = lastTimestamp === null ? 0 : (timestamp - lastTimestamp) / 1000;
lastTimestamp = timestamp;
ball.updateAnimation(dt);
for (let i = disappearingBricks.length - 1; i >= 0; i--) {
const brick = disappearingBricks[i];
brick.shrinkTime += dt;
if (brick.shrinkTime >= 0.2) {
disappearingBricks.splice(i, 1);
}
}
moveBall(dt);
// código de dibujo...
for (const brick of disappearingBricks) {
brick.draw();
}
drawStatus();
if (bricks.length === 0 && disappearingBricks.length === 0) {
alert("¡Ganaste el juego, felicidades!");
location.reload();
return;
}
requestAnimationFrame(update);
}
Hacemos avanzar los efectos existentes antes de mover la pelota, para que las animaciones que inician las colisiones de este fotograma empiecen en el tiempo cero. Recorremos disappearingBricks hacia atrás al quitar los tweens terminados, para que quitar elementos no desplace los índices de los elementos que aún no hemos visitado. El dibujo se hace después de estas actualizaciones e incluye tanto los ladrillos intactos como los que están desapareciendo.
La condición de victoria ahora espera a que ambos arrays estén vacíos, para que el último ladrillo termine de encogerse antes de que aparezca el mensaje de victoria. La condición bricks.length > 0 que ya existe en moveBall() detiene la pelota una vez que se han golpeado todos los ladrillos, mientras que el bucle de dibujo sigue funcionando para terminar los tweens.
Compara tu código
Esto es lo que deberías tener hasta ahora, funcionando en vivo. Para ver su código fuente, haz clic en el botón "Play".
Próximos pasos
Las animaciones y los tweens se ven muy bien, pero podemos añadir aún más cosas a nuestro juego: en la siguiente sección veremos cómo gestionar la entrada de botones.