# Cómo hacer un juego para navegador con Three.js, paso a paso

Fuente canónica: https://ercmart.com/blog/como-hacer-un-juego-web.html · Publicado 2026-11-03 · Actualizado 2026-11-03 · Laboratorio ERCMart (Piloko)

> Cómo hacer un juego para navegador con Three.js y módulos ES, sin build: controles táctiles, peso de archivos, hosting estático y releases que no se rompen.

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](/blog/como-hacer-un-videojuego.html); 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.

```html
<!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](/blog/elegir-motor-grafico.html) 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](https://threejs.org/manual/#en/installation) 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](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap) 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í:

```html
<script type="importmap">
  {"imports": {"three": "./assets/three.module.js"}}
</script>
<script type="module" src="main.js"></script>
```

### Ventajas y costos de no compilar

- **Lo que ganas:** cambias una línea y recargas. No hay configuración que se rompa entre versiones de herramientas.
- **Lo que pagas:** sin empaquetador no hay *tree-shaking* ni minificado automático. El manual de Three.js también advierte que todas las dependencias deben venir de la misma versión y del mismo origen; si mezclas fuentes, el código se puede duplicar o dejar de funcionar.
- **Una regla práctica:** el CDN sirve para aprender; para tu juego, copia la biblioteca a tu proyecto en una versión fija. Así una caída ajena no tumba tu juego y sabes exactamente qué versión corre.

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](https://developer.mozilla.org/en-US/docs/Web/API/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:

- El dedo que cae sobre el joystick queda como **dedo de movimiento**.
- Cualquier otro dedo que toque el escenario queda como **dedo de puntería**: apunta y dispara hacia ese punto; si lo arrastras, sigue disparando.
- Al soltar, cada dedo libera solo su acción.

### Detalles que importan

- **Zona muerta:** un joystick sin zona muerta hace que el personaje camine solo con el temblor del pulgar. Un margen pequeño cerca del centro lo resuelve.
- **Movimiento analógico:** la distancia del pulgar al centro debe regular la velocidad, no solo la dirección.
- **Mezclar teclado y dedo:** normaliza el vector final para que caminar en diagonal no sea más rápido.
- **Soltar todo al perder el foco:** si llega una notificación o cambias de pestaña, puede que nunca recibas el `pointerup` de ese dedo. Escucha `pointercancel`, `blur` y `visibilitychange` y reinicia los controles; si no, el personaje sigue caminando solo.
- **`touch-action: none`:** con Pointer Events, `preventDefault()` no basta para que arrastrar el dedo sobre el juego deje de hacer scroll o zoom en la página. Eso lo controla la propiedad CSS [`touch-action`](https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action): ponla en `none` en el área de juego y en el joystick, y deja `manipulation` en los botones del menú. Si el navegador decide que el gesto es un scroll, te manda `pointercancel`: trátalo igual que soltar el dedo.

## Cuánto debe pesar un juego de navegador

En el navegador, el peso es tiempo de espera.

- **Mide antes de optimizar.** Separa cuánto pesa la biblioteca, cuánto tu motor y cuánto cada nivel. Sorprende lo poco que suele pesar la lógica comparada con texturas, audio y modelos.
- **Texturas:** usa formatos comprimidos para web y resoluciones que de verdad se vean en pantalla.
- **Modelos 3D:** [glTF](https://www.khronos.org/gltf/) es el formato pensado para transmitir y cargar escenas de forma eficiente; las mallas que genera una IA pueden pesar unos 25 MB cada una antes de optimizarlas.
- **Carga por nivel:** lo que solo usa un escenario debe cargarse al entrar a ese escenario.
- **Caché:** agrega una marca de versión a cada archivo (`style.css?v=…`) o, mejor, publica cada versión en su propia carpeta, como explicamos abajo.

## 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](https://ercmart.com/survival/) es un juego de supervivencia por oleadas hecho con esta receta:

- **Three.js en módulos ES, sin build.** El juego carga una copia fija de Three.js desde su propia carpeta y la importa como módulo. PRISMA y PRISMA Chatarra, los juegos hermanos, usan un import map para encontrar la misma biblioteca.
- **Un motor, muchas variantes.** Los siete escenarios comparten un motor de código propio; un escenario nuevo es una pieza pequeña encima, no un juego aparte.
- **Controles táctiles en un módulo aparte.** `touch-controls.js` resuelve el joystick con zona muerta y movimiento analógico, un dedo para mover y otro para apuntar y disparar, captura de puntero y reinicio al perder el foco. Las funciones de cálculo son puras, así que se pueden probar sin abrir el juego.
- **HUD en HTML.** Vida, energía, oleada y la barra del jefe son elementos HTML sobre el canvas; la barra del jefe y el joystick llevan etiquetas ARIA.
- **Releases inmutables.** Cada versión se publica en una carpeta nombrada por la huella de su contenido; las páginas de entrada cambian solo después de verificar todos los archivos. Las versiones anteriores siguen disponibles para volver atrás.

### Datos de ERCMart

Medidos en el código del juego el 3 de noviembre de 2026:

- Three.js revisión 170, copia local sin minificar: alrededor de 1.3 MB.
- Motor propio compartido por los siete escenarios: más de 500 KB de código.
- El Coliseo (arena, cinco criaturas y sus reglas): tres archivos que suman 27 KB, alrededor del 5 % del motor; conectarlo tomó 62 líneas nuevas en seis archivos existentes.
- PRISMA: módulos compartidos de unos 157 KB y planetas de unos 35 KB en promedio cada uno.
- `touch-controls.js`: 56 líneas.

¿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](/blog/por-que-hacer-un-juego-web.html).

## Checklist antes de publicar tu juego web

- [ ] La biblioteca está en una versión fija dentro de tu proyecto.
- [ ] Probaste con dos dedos a la vez en un teléfono real.
- [ ] Al cambiar de pestaña, nada se queda presionado.
- [ ] Sabes cuánto pesa la biblioteca, el motor y cada nivel.
- [ ] Cada versión se publica completa en una carpeta nueva.
- [ ] Tienes un camino de regreso a la versión anterior.

## Siguiente paso

La mejor manera de entender estas decisiones es tocarlas: [abre ERCMart Survival](https://ercmart.com/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](/blog/assets-3d-personalizados.html).

## 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.
