En el post anterior vimos los primeros pasos al utilizar el elemento <canvas>
y como dibujar formas básicas en él. Esto está bien como primera aproximación, pero generalmente, queremos crear animaciones o minijuegos, por lo que necesitamos preparar lo que se llama el bucle de juego o el bucle de animación.
El bucle de animación
Este bucle es una función que se ejecuta continuamente en un intervalo de tiempo. Este intervalo de tiempo se llama frecuencia de actualización o FPS (Frames per second). Por ejemplo, si queremos que la animación se mueva a una velocidad de 60fps
, entonces se debe ejecutar cada 1/60
.
El código creará un círculo relleno de color rosa que rebotará verticalmente en pantalla. Ten en cuenta que para generar una animación, se suele tener una estructura general donde realizamos varias tareas. Esta sería una buena primera aproximación:
- 1️⃣ Fuera del bucle, inicialización (tareas iniciales que sólo se ejecutan una vez)
- 2️⃣ Dentro del bucle, actualización (tareas de lógica, cálculo de datos)
- 3️⃣ Dentro del bucle, renderización (tareas de dibujado y pintado)
- 4️⃣ Dentro del bucle, reiniciar bucle (decidir si continuar bucle o parar)
Una primera buena práctica es diferenciar el código que pinta del código que actualiza los datos de la animación o juego. Veamos una primera implementación definiendo estos 4 grupos:
En este fragmento de código, establecemos las coordenadas x
e y
donde estará el círculo. Las variables dx
y dy
definen la velocidad con la que avanza en cada eje y radius
es el radio del círculo. Además, establecemos la velocidad de actualización del bucle en FPS
:
let x = 100, y = 100, dx = 2, dy = 2, radius = 25;
const FPS = 1000 / 60;
En este fragmento de código simplemente actualizamos la coordenada y
(eje vertical) aumentándolo en la velocidad dy
. También comprobamos si se ha llegado al límite inferior o superior, en cuyo caso, invertimos el signo de la velocidad:
y += dy
if (y + radius > canvas.height || y - radius < 0) dy *= -1;
Por último, en esta parte realizamos las tareas de pintado en el canvas. Borramos el lienzo con clearRect()
en cada fotograma para evitar que se acumulen, creamos un trayecto y dibujamos el círculo relleno:
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = "deeppink";
ctx.fill();
ctx.closePath();
Sin embargo, aunque podemos ver la estructura general, el código es muy caótico si lo colocamos secuencialmente, por lo que convendría organizarlo un poco, así que vamos a ello:
- 1️⃣ Fuera de
gameLoop()
, inicialización (coordenadas, FPS, radio del círculo, etc...) - 2️⃣ Dentro de
update()
, actualización (mover el círculo y comprobar si ha llegado a un límite) - 3️⃣ Dentro de
draw()
, renderización (borrar lienzo y dibujar posición del círculo) - 4️⃣ Dentro de
gameLoop()
, reiniciar bucle (simplemente, volver a ejecutar el bucle)
Utilizando las secciones que mencionamos anteriormente, vamos a organizarlo en funciones:
<canvas></canvas>
<script type="module">
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 200;
canvas.height = 200;
// Inicialización
let y = 100, dy = 2;
const x = 100, dx = 2, radius = 25;
const FPS = 1000 / 60;
// Actualización de lógica
const update = () => {
y += dy
if (y + radius > canvas.height || y - radius < 0) dy *= -1;
}
// Renderizado y pintado
const draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = "deeppink";
ctx.fill();
ctx.closePath();
}
// Bucle del juego (update, draw, reinicio)
function gameLoop() {
update();
draw();
setTimeout(gameLoop, FPS);
}
gameLoop();
</script>
Observa que en este ejemplo el bucle gameLoop
es un bucle infinito que nunca para (hasta que se cierre el navegador). Si queremos que en algún momento pare la animación, tendríamos que establecer una condición para que en un caso concreto, no se ejecute más el gameLoop
.
Mejorando con requestAnimationFrame()
En el ejemplo anterior, utilizamos la función setTimeout()
que programa la ejecución de una función tras un tiempo concreto. Aunque funciona correctamente, el uso de setTimeout()
puede acarrearnos ciertos problemas en casos especificos, como que no se sincronice correctamente con el monitor (ejecuta la animación más rápido o más lento y pierde fotogramas o se desincroniza), no suspende la animación si la pestaña del navegador no está activa (uso innecesario de recursos), etc.
Característica | setInterval() / setTimeout() | requestAnimationFrame() |
---|---|---|
Sincronización con el monitor | ❌ No sincronizado | ✅ Sincronizado |
Eficiencia en segundo plano | ❌ Sigue ejecutándose | ✅ Se pausa automáticamente |
Precisión temporal | ❌ Puede variar | ✅ Alta precisión |
Uso del tiempo | ❌ No optimizado | ✅ Optimizado por el navegador |
Flexibilidad de FPS | Manual | Automático |
Para evitar estos problemas vamos a utilizar requestAnimationFrame()
, que lo gestiona todo mejor:
<canvas></canvas>
<script type="module">
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 200;
canvas.height = 200;
// Inicialización
let y = 100, dy = 2;
const x = 100, dx = 2, radius = 25;
// Actualización de lógica
const update = () => {
y += dy
if (y + radius > canvas.height || y - radius < 0) dy *= -1;
}
// Renderizado y pintado
const draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = "deeppink";
ctx.fill();
ctx.closePath();
}
// Bucle del juego (update, draw, reinicio)
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
gameLoop();
</script>
Comprobarás que ahora, no tenemos que definir los FPS
, y la animación va automáticamente más fluida. Si queremos cambiar la velocidad del elemento, tendríamos que cambiar la lógica de nuestra animación, en nuestro caso el valor de dy
.