Lumænaut_

Así funciona el bucle del videojuego

Input, actualizar, renderizar — el latido del videojuego

Arte a color ASCII: corredores corriendo en una pista futurista, sugiriendo un circuito repetido; el movimiento representa el latido del bucle del juego, donde el input, el actualizar y el renderizar se suceden continuamente.

Cada videojuego que has jugado — desde el Pong hasta el último éxito de mundo abierto — lo mueve un solo latido: el bucle del juego. El bloque de código que no se detiene. Mientras el bucle corre, el juego está vivo. Cuando se detiene, el juego termina. Entender cómo funciona es el primer paso para hacer tus propios videojuegos.

El bucle más simple

En el núcleo, un bucle de juego hace tres cosas, una y otra vez: revisa el input, actualiza el estado del juego (posiciones, puntajes, física) y renderiza el siguiente cuadro en pantalla. En pseudocódigo se ve así:


while (gameIsRunning) {  // Mientras el juego esté corriendo
    processInput();  // Procesa el input
    update();  // Actualiza el estado del juego
    render();  // Renderiza el siguiente cuadro en pantalla
}
          

Eso es todo. Sin magia. El bucle corre tan rápido como la máquina lo permita (o como tú lo limites). Cada vuelta es un “tic” del reloj del juego. Mueves al jugador, actualizas a los enemigos, resuelves colisiones y pintas el resultado. Repites hasta que el jugador cierra.

Por qué los cuadros por segundo no son fijos

En las máquinas arcade y consolas viejas la lógica corría a un ritmo fijo, por ejemplo 60 veces por segundo. En computadoras y celulares de hoy el ritmo varía: una escena pesada puede ir a 50 FPS y un menú simple a 120 FPS. Si atas la lógica del juego a “una actualización por cuadro”, el juego irá más rápido en hardware potente y más lento en uno débil. Un personaje que se mueve 10 píxeles por cuadro cruzaría la pantalla en la mitad del tiempo a 120 FPS que a 60 FPS. Eso se siente roto e injusto.

La solución es usar el tiempo delta (suele escribirse dt): el tiempo en segundos (o milisegundos) desde el cuadro anterior. Multiplicas el movimiento y otros cambios por dt. Así, “10 píxeles por segundo” se vuelve position += 10 * dt. El personaje recorre la misma distancia por segundo sin importar cuántos cuadros renderice la máquina. El juego queda independiente del framerate.

Actualizar vs renderizar

Conviene tener claro la diferencia entre “actualizar” y “renderizar”. Actualizar es donde cambias el mundo del juego: física, IA, timers, input. Renderizar es donde llevas ese estado a la pantalla. Puedes actualizar varias veces en un cuadro (por ejemplo física a paso fijo) y renderizar una sola vez, o saltarte un render si el cuadro se atrasó. Muchos motores hacen “actualizar a 60 Hz, renderizar cuando se pueda” para que la física sea estable y la imagen varíe.

El bucle en el navegador

En el navegador no escribes while (true). El navegador te da requestAnimationFrame (a veces abreviado rAF). Le pasas una función; el navegador la llama antes del siguiente refresco, casi siempre al ritmo de la pantalla (60 o 120 Hz). Tu “bucle” es: pedir el siguiente cuadro y en esa función hacer tu actualización y tu render. Una llamada por cuadro, con un tiempo que puedes medir para obtener dt.

Mira el bucle en acción

Demostración: una pelota se mueve y rebota; el bucle corre con requestAnimationFrame.
Cuadro: 0 FPS: — Δt: — ms

Es el mismo bucle: en cada cuadro actualizamos la posición de la pelota con el tiempo delta y luego renderizamos. Pausa y usa “Avanzar 1 cuadro” para hacer una actualización y un render a la vez.

En la demo de arriba la posición de la pelota se actualiza con position += velocity * dt. Su velocidad está en “unidades por segundo”, no “unidades por cuadro”. Los números muestran el cuadro actual, los FPS y el tiempo delta. Al pausar se detiene el bucle; “Avanzar 1 cuadro” hace una actualización y un render para que veas el bucle dar un solo tic.

Paso fijo vs paso variable

Algunos juegos usan un paso fijo para la física (por ejemplo siempre 1/60 s por paso). Pueden ejecutar varios pasos de física en un cuadro si el cuadro fue largo, para que la simulación no se desincronice del tiempo real. Otros usan paso variable: una actualización por cuadro, con dt igual al tiempo real transcurrido. Cada enfoque tiene sus pros y contras: el fijo es estable y reproducible; el variable es más simple y es suficiente para muchos juegos. Los motores profesionales suelen mezclar (física fija, render variable).

Un solo bucle para todos

No importa qué tan grande sea el juego, la idea es la misma: un bucle, input → actualizar → renderizar, con el tiempo (tiempo delta) impulsando los cambios. Cuando lo ves así, puedes entrar a sistemas de entrada, pipelines de renderizado y motores de física sabiendo que todos se conectan a este latido. Dominar el bucle es tener la base sobre la que se construye todo lo demás.

Un bucle. Input, actualizar, renderizar. Repetir.

Aplico las mismas ideas en Second Runner, una extensión de Chrome de estilo arcade que escribí en JavaScript. Cada minijuego corre con requestAnimationFrame: leemos la entrada (posición del ratón), avanzamos la simulación (pelota y paletas) usando el tiempo transcurrido para que el movimiento sea estable aunque cambien los FPS, y luego dibujamos en el canvas — el mismo ritmo entrada → actualizar → renderizar que describe este post. Puedes instalarla desde Chrome Web Store — Second Runner.

Referencias & lecturas adicionales

¿Aún no tienes suficiente de Lumænaut_? Lee esto...