Si vas a aprender a desarrollar videojuegos, o ya te dedicas a ello, habrás oido hablar del «game loop».
En este post te comento desde el game loop clásico hasta el enfoque moderno:
Loop clásico
El Game Loop es el corazón básico de todo juego, el bucle principal que hace que todo funcione.
Piensa en cualquier tipo de videojuego, ¿qué ocurre mientras estás jugando?
- Pasan cosas en la pantalla. Se mueven los enemigos 👾 , suena la música, o incluso nuestro player hace algo.
- Si interactúas, por ejemplo moviendo el joystick 🕹 , gamepad 🎮, o pulsando alguna tecla ⌨️ , el juego reacciona a dicho estímulo, y volvemos a esperar otro estímulo.
¿Cómo funciona un game loop?
Si traducimos esto a pseudo-código, nos quedaría un bucle tal que así:
while (true) {
processInput();
update();
render();
}
processInput: Procesa la entrada del usuario desde la última iteración.
update: Actualiza el estado del juego un paso más. Ejecutando la inteligencia artificial o la física en caso de haberlas.
render: Muestra los resultados del nuevo estado, en pantalla.
Si ejecutamos este bucle infinito, las instrucciones se ejecutarán unas tras otras, tan rápidas como la máquina nos lo permita, y así de forma indefinida, por lo que nos encontramos con 2 problemas principales:
- La sobrecarga, el procesador estará funcionando al 100%.
- La velocidad, dependiendo de la potencia de la máquina, las iteraciones del bucle se ejecutarán en mayor o menor tiempo, dando lugar a una ejecución más o menos lenta del juego.
Por lo que aunque ya tenemos nuestro game loop básico, tendremos que mejorarlo para evitar dichos problemas.
Loop con frame limiting
¿Y si ponemos una espera tras cada ciclo? Nos quedaría algo como:
while (true) {
processInput();
update();
render();
sleep( time );
}
Con sleep, «dormiríamos» el proceso un determinado tiempo, para una vez transcurrido, volver a iterar en nuestro bucle.
Recuerda, que comentamos que dependiendo de la potencia de nuestra máquina, tardará más o menos en procesar las instrucciones, y esto podría hacer que nuestro juego vaya a diferente velocidad, por lo que tendremos que tener en cuenta dicha velocidad.
Para ello, se suele establecer una velocidad deseada, por ejemplo 60 FPS (frames por segundo), que significa que cada frame se ejecuta cada 1/60 = 16 milisegundos. Así el tiempo que tenemos que dormir será de 16 milisegundos, menos el transcurrido en ejecutar processInput() + update() + render().
Quedando el código:
while (true) {
var start = getCurrentTime();
processInput();
update();
render( start + (1/FPS) - getCurrentTime() );
}
Con esto tendríamos un Game Loop más «estable».
Implementación paso a paso
Veamos ahora un ejemplo muy básico desarrollado en javascript (que me perdonen los expertos en javascript, que seguro hay mejores formas de hacerlo a nivel de código).
Partimos de un HTML muy básico, con un div que representará nuestra «paleta», que moveremos con las teclas k y l a izquierda y derecha respectivamente.
<html>
<head>
<title>Primer juego</title>
<script src="game.js"></script>
<style>
.racket {
background-color: red;
width: 5%;
height: 15px;
position: absolute;
bottom: 10px;
left: 100px;
}
</style>
</head>
<body style="background-color: #000;">
<div id="racket" class="racket"></div>
</body>
</html>
Ya tenemos nuestra raqueta colocada, Play Now !!
const KEY_NONE = false;
const KEY_LEFT = 'a';
const KEY_RIGHT = 'd';
let keyPressed = KEY_NONE;
let direction = '';
let position = 100;
window.onload = function() {
setTimeout(gameloop, 100);
};
function gameloop()
{
play();
// Llamamos a gameloop() la siguiente vez. Esto emula el while(true)
setTimeout(gameloop, 100);
}
// Loop principal del juego
function play() {
processInput();
update();
render();
}
function processInput() {
if ( keyPressed == KEY_LEFT ) {
direction = '+';
} else if ( keyPressed == KEY_RIGHT ) {
direction = '-';
}
}
function update() {
if ( direction == '+' ) {
position = position + 10;
} else if ( direction == '-' ) {
position = position - 10;
}
// Aquí actualizaríamos el estado del juego, y otros elementos como enemigos
}
function render() {
let racket = document.getElementById('racket')
racket.style.left = position + "px";
}
// -------
document.addEventListener('keydown', function(e){
if ((e.key === KEY_LEFT) || (e.key === KEY_RIGHT)) {
keyPressed = e.key;
}
});
document.addEventListener('keyup', function(e){
if ((e.key === KEY_LEFT) || (e.key === KEY_RIGHT)) {
keyPressed = KEY_NONE;
}
});
function sleep(ms) {
var unixtime_ms = new Date().getTime();
while(new Date().getTime() < unixtime_ms + ms) {}
}
Aunque no es mi fuerte javascript, he querido implementarlo en este lenguaje, para que veamos una forma diferente de implementar el while(true), y el sleep(), pero que veamos en esencia de que se trata de lo mismo, es decir, un bucle «infinito», y una espera en cada iteración.
Y ya tenemos nuestro cutre-juego interactuando con nosotros.
Frame callback moderno
Hasta ahora hemos visto el enfoque clásico donde el juego controla completamente el loop principal.
Este modelo sigue siendo válido y continúa utilizándose en muchos engines y librerías actuales. Sin embargo, en plataformas modernas como navegadores o sistemas móviles existe otro enfoque muy común: el frame callback.
En este modelo el sistema operativo o el navegador notifican cuándo es el momento adecuado para generar el siguiente frame.
¿Por qué apareció este modelo?
Con el tiempo aparecieron nuevos problemas:
- Consumo energético
- Gestión térmica
- Multiples aplicaciones compartiendo GPU
- Monitores con refresco variable
- Sistemas multitarea complejos
- Navegadores y compositores gráficos
En este contexto, dejar que cada aplicación ejecutase un loop infinito sin coordinación empezó a ser poco eficiente.
Por eso los sistemas modernos comenzaron a ofrecer «APIs» sincronizadas con el refresco real de pantalla.
El caso más conocido: requestAnimationFrame
En JavaScript moderno el patrón habitual es:
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
Aquí ya no existe un while(running) manual.
En su lugar:
El navegador decide cuándo debe renderizarse el próximo frame
- Invoca nuestro callback
- El juego actualiza y renderiza
- Se solicita el siguiente frame
¿Qué ventajas tiene?
- Sincronización automática con el monitor
- El navegador coordina el renderizado con el refresco real de pantalla.
- Reduce frames innecesarios
- Menos consumo de CPU y batería.
Con requestAnimationFrame, el navegador normalmente limitará el ritmo a 60 FPS o 120 FPS o el refresco disponible.
El navegador puede pausar pestañas ocultas, reducir FPS en segundo plano, coordinar GPU y compositor u optimizar el scheduling.
Todo esto ocurre automáticamente.
El game loop sigue existiendo, es importante entender que el concepto de game loop no desaparece.
Seguimos teniendo:
input
update
render
La diferencia es únicamente quién controla el ritmo de ejecución, antes era nuestro juego quien decidía cuando generar frames, y ahora es el navegador o sistema.
Frame callback en otras plataformas:
- iOS con CADisplayLink
- Android con Choreographer
- Motores modernos sincronizados con vsync
- APIs gráficas integradas con compositores modernos
Y en plataformas modernas, especialmente web y móviles, este segundo enfoque suele ser el predominante.
Aplicando un game loop en un juego real (Godot + vibe coding)
Si en esta nueva era en la que la IA ha venido a darnos super poderes, lo tuyo es el vibe coding, que sepas que puedes conectar motores como Godot con ella.
En este vídeo puedes ver cómo se aplica este concepto al crear un juego real con Godot. Aunque no se explica directamente el término “game loop”, verás claramente cómo se repite el ciclo de actualización y renderizado.
