ERCMart

Juegos web

Cómo hacer un juego para navegador con Three.js, paso a paso

· 11 min de lectura · Laboratorio ERCMart · Quién escribe

Ilustración anime de una desarrolladora adulta de noche con un juego 3D en la laptop y un joystick táctil en el teléfono

Para hacer un juego para navegador necesitas tres cosas: una biblioteca que dibuje 3D o 2D (por ejemplo Three.js), tu lógica de juego en módulos de JavaScript y un hosting estático que sirva esos archivos. No hace falta instalar un motor ni compilar nada: el navegador ya entiende WebGL y módulos ES. Lo difícil no es arrancar, es que el juego se sienta bien en un celular, pese poco y se actualice sin romperse.

Un juego para navegador es un juego hecho con HTML, JavaScript y WebGL que corre dentro de una pestaña y se abre con un enlace, sin instalar nada.

En ERCMart, la cadena que se quedó con el mundo entero, el supermercado siempre está abierto. Nuestro juego también: se abre con un enlace. Así lo montamos. Si todavía no tienes clara la idea del juego, empieza por la guía de cómo hacer un videojuego desde cero; aquí nos concentramos en la parte web.

Tu primer juego en Three.js en 40 líneas

Antes de la teoría, algo que se mueve. Guarda esto como index.html, sírvelo con un servidor local y abre la página. Mueves el cubo azul con las flechas o con WASD; cuando tocas el rojo, salta a otro lugar y sumas un punto.

<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <title>Mi primer juego</title>
  <style>body { margin: 0; overflow: hidden; } #hud { position: fixed; top: 8px; left: 8px; color: #fff; font: 18px sans-serif; }</style>
  <script type="importmap">
    { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js" } }
  </script>
</head>
<body>
  <div id="hud">Puntos: 0</div>
  <script type="module">
    import * as THREE from 'three';
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
    camera.position.set(0, 12, 8); camera.lookAt(0, 0, 0);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(innerWidth, innerHeight); document.body.appendChild(renderer.domElement);
    const box = (color) => new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color }));
    const player = box(0x33ccff);
    const target = box(0xff3366);
    target.position.set(3, 0, -2);
    scene.add(player, target);
    const keys = {};
    addEventListener('keydown', (e) => { keys[e.code] = true; });
    addEventListener('keyup', (e) => { keys[e.code] = false; });
    const clock = new THREE.Clock();
    let points = 0;
    function loop() {
      requestAnimationFrame(loop);
      const step = 6 * clock.getDelta(); // 6 unidades por segundo, sin importar los fps
      if (keys.ArrowLeft || keys.KeyA) player.position.x -= step;
      if (keys.ArrowRight || keys.KeyD) player.position.x += step;
      if (keys.ArrowUp || keys.KeyW) player.position.z -= step;
      if (keys.ArrowDown || keys.KeyS) player.position.z += step;
      if (player.position.distanceTo(target.position) < 1) {
        target.position.set(Math.random() * 10 - 5, 0, Math.random() * 8 - 4);
        document.getElementById('hud').textContent = 'Puntos: ' + ++points;
      }
      renderer.render(scene, camera);
    }
    loop();
  </script>
</body>
</html>

Ahí están las piezas de cualquier juego web: una escena con cámara y renderer, la entrada guardada en un objeto keys, un bucle con requestAnimationFrame que avanza el mundo y dibuja, y una colisión por distancia entre dos centros. El movimiento se multiplica por el tiempo transcurrido (clock.getDelta(), en segundos) para que el cubo vaya igual de rápido en una pantalla de 60 Hz que en una de 120 Hz. El marcador es HTML normal encima del canvas.

El import map apunta a la versión 0.170.0 de Three.js en jsDelivr, publicada en octubre de 2024; al 3 de noviembre de 2026 npm ya lista la 0.187.0, pero fijar una versión exacta evita que una actualización te cambie el comportamiento sin avisar.

Qué necesitas para hacer un juego para navegador

Todo juego web tiene estas capas:

Capa Qué hace Ejemplo en un juego web
Render Dibuja la escena en un <canvas> Three.js sobre WebGL
Bucle de juego Avanza el tiempo, mueve enemigos, resuelve choques requestAnimationFrame y tu código
Entrada Teclado, ratón, dedo, gamepad Pointer Events y eventos de teclado
Contenido Niveles, criaturas, reglas, música Módulos JS, texturas, glTF, audio
Interfaz Menús, HUD, pausa HTML y CSS encima del canvas
Distribución Que llegue a quien juega Hosting estático y un enlace

La ventaja del navegador es que las capas de interfaz y distribución vienen casi gratis: el HUD puede ser HTML normal, con texto accesible y botones de verdad, y publicar es subir archivos. Si estás decidiendo entre la web y un motor de escritorio, en qué motor gráfico elegir comparamos criterios.

Three.js con módulos ES y sin paso de build

Three.js se puede usar con npm y un empaquetador, pero no es obligatorio. Su manual de instalación describe la otra vía, sin herramientas de compilación: cargar la biblioteca desde un CDN con un import map que le dice al navegador dónde vive, como en el ejemplo de arriba. La misma técnica funciona apuntando a una copia local, que es lo que hacemos nosotros. Un import map es un bloque JSON en el HTML que traduce nombres como "three" a una ruta real; MDN lo marca como disponible en todos los navegadores principales desde marzo de 2023.

Con una copia local, el import map queda así:

<script type="importmap">
  {"imports": {"three": "./assets/three.module.js"}}
</script>
<script type="module" src="main.js"></script>

Ventajas y costos de no compilar

Un detalle que confunde al principio: los módulos no cargan si abres el HTML con doble clic. Necesitas un servidor local, aunque sea el más simple.

Organiza el código en módulos pequeños

El juego crece rápido. Separa desde el principio: un módulo para la energía, otro para la progresión, otro para cada familia de enemigos, otro para la entrada táctil. Cuando un módulo exporta funciones puras (reciben números, devuelven números) puedes probarlo sin abrir el juego. Ese orden es también lo que permite reutilizar un motor en varias variantes, como contamos más abajo.

Controles táctiles que no se pelean entre sí

Un juego de navegador va a abrirse en un teléfono, lo planees o no. El ejemplo de arriba solo escucha el teclado; en un celular no se movería. Los controles táctiles son donde más se nota si alguien lo probó con los dedos.

Usa Pointer Events

Pointer Events unifican ratón, pluma y dedo en un solo modelo de eventos, disponible en todos los navegadores principales desde julio de 2020. Cada dedo trae un pointerId propio, y setPointerCapture() hace que ese dedo siga mandando eventos al mismo elemento aunque se salga de él. Eso es justo lo que necesita un joystick virtual: si el pulgar se desliza fuera del círculo, el personaje no debe frenarse en seco.

Un dedo, una acción

El error típico es que el dedo que apunta "robe" el joystick o al revés. La solución es asignar cada pointerId a una sola acción desde que toca la pantalla hasta que se levanta:

Detalles que importan

Cuánto debe pesar un juego de navegador

En el navegador, el peso es tiempo de espera.

Dónde publicar un juego web: hosting estático

Si tu juego no guarda partidas en un servidor ni tiene multijugador, es un sitio estático: cualquier hosting que sirva archivos lo publica, y el progreso puede vivir en el propio navegador. Un ranking o cuentas suman un servicio aparte, pero el juego sigue siendo estático.

Cómo actualizar un juego web sin romperlo: releases inmutables

Subes una versión nueva archivo por archivo y, a media subida, alguien abre el juego: recibe el index.html nuevo con un game.js viejo, o al revés. El resultado es una pantalla en blanco o un error raro que no puedes reproducir.

La solución es tratar cada versión como un paquete que no cambia:

  1. Empaqueta la versión completa y calcula una huella (hash) de su contenido.
  2. Súbela a una carpeta nueva cuyo nombre es esa huella. Nada de lo que ya está publicado se toca.
  3. Verifica cada archivo de la carpeta nueva contra el paquete: que exista y que sea idéntico.
  4. Solo entonces cambia las páginas de entrada para que apunten a la carpeta nueva. Ese cambio es pequeño y casi instantáneo.
  5. La versión anterior se queda donde estaba. Si algo sale mal, regresar es volver a apuntar a ella.

Quien abrió el juego antes del cambio termina su partida con la versión vieja, completa. Quien llega después recibe la nueva, completa. Nadie recibe una mezcla. De paso, la caché deja de ser un problema: una carpeta con contenido distinto tiene un nombre distinto.

En ERCMart lo hicimos así

ERCMart Survival es un juego de supervivencia por oleadas hecho con esta receta:

Datos de ERCMart

Medidos en el código del juego el 3 de noviembre de 2026:

¿Por qué la web y no un motor de escritorio? Porque el juego vive dentro de un universo narrativo que se cuenta en páginas, y un enlace lleva directo a jugar. Desarrollamos ese argumento en por qué hacer un juego web.

Checklist antes de publicar tu juego web

Siguiente paso

La mejor manera de entender estas decisiones es tocarlas: abre ERCMart Survival en tu teléfono y fíjate en cómo se reparte el trabajo entre tus dos dedos. Si quieres seguir con la parte visual, lee cómo convertir una imagen en modelo 3D con IA.

Preguntas frecuentes

¿Necesito un motor como Unity para hacer un juego de navegador?

No. Un navegador moderno ya dibuja 3D con WebGL y entiende módulos de JavaScript. Con una biblioteca como Three.js escribes la escena, la cámara y la lógica en archivos .js normales y los sirves como cualquier página. Un motor completo aporta editor y herramientas; para un juego web pequeño o mediano, no es obligatorio.

¿Puedo usar Three.js sin npm ni Vite?

Sí. El manual de Three.js describe cómo usarlo sin herramientas de compilación: un import map en el HTML le dice al navegador de dónde cargar la biblioteca (en su ejemplo, un CDN; también sirve una copia local), y tus módulos la importan por nombre. Necesitas servir los archivos con un servidor local, porque abrirlos con doble clic no carga módulos.

¿Cómo hago controles táctiles para un juego web?

Usa Pointer Events, que unifican ratón, pluma y dedo en un mismo modelo de eventos. Asigna cada dedo a una sola acción por su pointerId (uno mueve, otro apunta), usa setPointerCapture para que no se pierda y pon touch-action: none en el área de juego para que no haga scroll. Al cambiar de pestaña, suelta todo.

¿Cómo actualizo un juego web sin que se rompa para quien está jugando?

Publica cada versión completa en una carpeta nueva, sin tocar la anterior. Verifica que todos los archivos llegaron idénticos y solo entonces cambia la página de entrada para que apunte a la carpeta nueva. Así nadie recibe una mezcla de archivos viejos y nuevos, la caché deja de estorbar y volver atrás es apuntar otra vez a la versión previa.

¿Cuánto debe pesar un juego de navegador?

No hay una cifra oficial; depende de tu público y su conexión. La regla útil es medir: separa el código del motor del contenido, comprime texturas y modelos, y carga cada nivel cuando se necesita. Si tu juego reutiliza un motor común, cada variante nueva pesa poco comparada con el total.