Parte: 12 — Juegos web y HTML5 · Fuente: MDN Web Docs — Anatomy of a video game y requestAnimationFrame ⏱️ Duración estimada: 75 min · Nivel: Intermedio
Todo juego web nace de un bucle: actualizar el estado y dibujarlo, muchas veces por segundo. En esta clase construirás ese bucle con requestAnimationFrame (rAF), la API que el navegador ofrece para sincronizar tu dibujo con el refresco de la pantalla. Aprenderás a medir el delta time (tiempo entre cuadros) a partir del timestamp que rAF entrega, para que el movimiento sea igual de rápido en un monitor de 60 Hz que en uno de 144 Hz.
Sobre Canvas 2D dibujarás y moverás objetos, y capturarás input de teclado y puntero. Al final tendrás un mini-juego en JavaScript puro: un cuadrado que se mueve con las flechas y rebota contra los bordes, con la estructura clásica actualizar() / dibujar() que reutilizarás en toda la parte web.
Al finalizar, el alumno podrá:
requestAnimationFrame.actualizar(dt) y dibujar().keydown/keyup) y puntero (pointermove) para controlar un objeto.| # | Tema | Por qué importa |
|---|---|---|
| 1 | requestAnimationFrame |
Sincroniza el dibujo con el refresco y ahorra batería. |
| 2 | Delta time | Independiza la velocidad del framerate del monitor. |
| 3 | Estructura actualizar/dibujar | Separa lógica de render, base de todo motor. |
| 4 | Dibujo en Canvas 2D | clearRect, fillRect y estado del contexto. |
| 5 | Input de teclado | Estado de teclas con keydown/keyup. |
| 6 | Input de puntero | Posición del ratón/dedo con eventos pointer. |
| 7 | Colisión con bordes | Mantiene al objeto dentro del área jugable. |
| 8 | Rebote y velocidad | Introduce vectores de velocidad y reflexión simple. |
requestAnimationFrame(cb): pide al navegador ejecutar cb antes del próximo repintado, pasándole un timestamp. Clave: reemplaza a setInterval para animación fluida.CanvasRenderingContext2D con los métodos de dibujo. Clave: se obtiene con getContext('2d').clearRect: borra una región del canvas. Clave: limpia el cuadro anterior antes de redibujar.actualizar, no en el evento.(vx, vy) en píxeles por segundo. Clave: sumado por dt produce el desplazamiento.Solo necesitas un navegador y un editor de texto; opcionalmente un servidor local como python -m http.server (recomendado para futuras clases con módulos). Abre las herramientas de desarrollo con F12 para ver la consola y depurar. La referencia clave es "Anatomy of a video game" de MDN (https://developer.mozilla.org/es/docs/Games/Anatomy).
Crea una carpeta mini-juego-js/ con un index.html y un juego.js. Trabajaremos sin frameworks: JavaScript y Canvas nativos.
Construirás un cuadrado que se mueve con las flechas y rebota en los bordes.
index.html con un canvas y la referencia al script:<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Cuadrado móvil</title></head>
<body>
<canvas id="lienzo" width="480" height="320" style="background:#101828"></canvas>
<script src="juego.js"></script>
</body>
</html>
juego.js, prepara el contexto y el estado del jugador y del input:const canvas = document.getElementById('lienzo');
const ctx = canvas.getContext('2d');
// Estado del jugador: posición, tamaño y velocidad en px/segundo.
const jugador = { x: 220, y: 140, lado: 40, vx: 120, vy: 90, velTeclado: 200 };
// Registro de teclas presionadas.
const teclas = {};
addEventListener('keydown', (e) => { teclas[e.key] = true; });
addEventListener('keyup', (e) => { teclas[e.key] = false; });
actualizar(dt): aplica el input de flechas y suma la velocidad de rebote, multiplicando siempre por dt:function actualizar(dt) {
// Movimiento por teclado (flechas).
if (teclas['ArrowLeft']) jugador.x -= jugador.velTeclado * dt;
if (teclas['ArrowRight']) jugador.x += jugador.velTeclado * dt;
if (teclas['ArrowUp']) jugador.y -= jugador.velTeclado * dt;
if (teclas['ArrowDown']) jugador.y += jugador.velTeclado * dt;
// Movimiento automático de rebote.
jugador.x += jugador.vx * dt;
jugador.y += jugador.vy * dt;
// Rebote e límites contra los bordes del canvas.
if (jugador.x < 0) { jugador.x = 0; jugador.vx *= -1; }
if (jugador.y < 0) { jugador.y = 0; jugador.vy *= -1; }
if (jugador.x + jugador.lado > canvas.width) { jugador.x = canvas.width - jugador.lado; jugador.vx *= -1; }
if (jugador.y + jugador.lado > canvas.height) { jugador.y = canvas.height - jugador.lado; jugador.vy *= -1; }
}
dibujar(): limpia el cuadro y pinta el cuadrado:function dibujar() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#4ade80';
ctx.fillRect(jugador.x, jugador.y, jugador.lado, jugador.lado);
}
requestAnimationFrame calculando dt en segundos desde el timestamp:let anterior = performance.now();
function bucle(ahora) {
// dt en segundos; el timestamp llega en milisegundos.
const dt = (ahora - anterior) / 1000;
anterior = ahora;
actualizar(dt);
dibujar();
requestAnimationFrame(bucle);
}
requestAnimationFrame(bucle);
python -m http.server 8000) o abre index.html, y prueba: el cuadrado rebota solo y responde a las flechas. Verás que la velocidad es la misma aunque cambie el framerate, porque todo se multiplica por dt.Con esta estructura tienes un motor mínimo reutilizable: cambia actualizar y dibujar para hacer otros juegos.
velTeclado.pointermove para fijar su posición.1/dt) con fillText en una esquina.Crea un mini-juego donde un cuadrado "jugador" (controlado por flechas) debe tocar un cuadrado "objetivo" que rebota por la pantalla; al tocarlo, el objetivo reaparece en una posición aleatoria y un contador sube en pantalla.
Criterio de aceptación: el jugador se mueve con las flechas a velocidad constante independiente del framerate, el objetivo rebota en los cuatro bordes, y al colisionar el contador aumenta y el objetivo se reubica, sin errores en consola.
| Síntoma / mensaje | Causa y cómo arreglar |
|---|---|
| El objeto va muy rápido o muy lento en otro monitor | No multiplicaste por dt. Aplica velocidad * dt a todo movimiento. |
| Se ve un rastro de cuadrados | No limpias el canvas. Llama a clearRect al inicio de dibujar. |
| El primer cuadro da un dt enorme | Inicializaste mal anterior. Usa performance.now() antes de arrancar el bucle. |
| Las flechas hacen scroll de la página | El evento no se controla. Llama a e.preventDefault() en keydown para las flechas. |
| El objeto se sale del canvas | Faltó el límite de un borde. Revisa las cuatro comparaciones con ancho/alto. |
❓ ¿Por qué usar rAF y no setInterval? rAF se sincroniza con el refresco de pantalla, pausa en pestañas ocultas y da un timestamp preciso; setInterval desincroniza y desperdicia recursos.
❓ ¿Qué unidad tiene el delta time? Aquí lo convertimos a segundos, así las velocidades se expresan en píxeles por segundo, más intuitivas.
❓ ¿Debo leer el input dentro del evento o del bucle? Guarda el estado en el evento y consúltalo en actualizar. Así el movimiento es continuo mientras la tecla está presionada.
❓ ¿Cómo evito que un dt muy grande rompa la física? Acota dt a un máximo (por ejemplo 0.05 s) para que un salto de cuadro no teletransporte objetos.
Clase 215 - Exportar Godot a HTML5 y WebAssembly