Como crear un juego en realidad aumentada WebAR – Parte Dos

Tu Primer Videojuego en Realidad Aumentada

(Parte 2): ¡Dando Vida al Juego!

ingcarlosreina con un ipad en la mano derecha mostrando el marcador de la WebAR que hace parte del tutorial de como aprender a hacer un video juego en realidad aumentada
cómo hacer un video juego en realidad aumentada WebAR

Autor: IngCarlosReina – Blog Realidad Aumentada Empezando Desde Cero

¡Hola de nuevo aumentados, apasionados de la Realidad Aumentada!

En la Parte 1 de esta serie, pusimos la primera piedra de nuestro proyecto: entendimos la estructura básica de una aplicación WebAR con MindAR y Three.js, y dejamos todo listo para la acción. Si te la perdiste, te recomiendo que le eches un vistazo antes de continuar: Como crear un video juego en realidad aumentada – Parte Uno

Hoy, nos arremangamos y nos sumergimos en lo más emocionante: transformar nuestra demo técnica en un videojuego funcional y divertido. Añadiremos objetivos, desafíos, interactividad y ese pulido técnico necesario para que la experiencia sea fluida en cualquier dispositivo.

¿Quieres hacer tus propios marcadores e imágenes para realidad aumentada?

Utiliza la herramienta para markers de Realidad Aumentada Empezando Desde Cero: Compilador de Marcadores AR | Crea Realidad Aumentada Gratis

¿Estás listo para darle vida a nuestro «Caza Monedas AR«?

Los Pilares de Nuestro Juego
Un videojuego no es solo un modelo 3D que se mueve; es una experiencia con reglas y metas. Estos son los elementos que implementaremos para crearla:

  • El Objetivo: ¡Atrapa las Monedas!
    Nuestro juego ahora tiene un propósito claro: el jugador debe conducir el coche para recolectar 5 monedas que aparecen distribuidas aleatoriamente sobre el marcador. Esto introduce una meta y una condición de victoria.
  • El Desafío: Lucha Contra el Reloj
    Para añadir un toque de urgencia y rejugabilidad, hemos incorporado un cronómetro. Se activa en cuanto el jugador realiza la primera acción y se detiene al conseguir la última moneda. ¡El objetivo ahora es batir tu propio récord!
  • La Estrategia: El Turbo de Velocidad
    El botón ‘A’ ya no es solo para un efecto visual. Ahora activa un turbo que duplica la velocidad del coche durante 2 segundos. Pero ¡cuidado! Tiene un período de enfriamiento de 5 segundos, lo que obliga al jugador a usarlo estratégicamente para alcanzar esa moneda lejana en el momento justo.
  • El Feedback: Sonido y Dinamismo
    Cada moneda recolectada emite un sonido satisfactorio, proporcionando una respuesta inmediata al jugador. Además, cada moneda gira sobre su propio eje a una velocidad y dirección únicas, haciendo que el mundo se sienta más vivo y dinámico.

El Pulido Técnico: Asegurando la Jugabilidad
Crear un juego para la web implica enfrentarse a los desafíos de los navegadores móviles. En esta versión, hemos solucionado dos problemas muy comunes:

  • Bloqueo del «Pull-to-Refresh»: En los móviles, especialmente en Safari, arrastrar el dedo por la pantalla puede hacer que el navegador intente refrescar la página, arruinando la experiencia. Hemos implementado una solución con CSS y JavaScript para bloquear este comportamiento y mantener la inmersión total.
  • Activación del Audio: Los navegadores también bloquean el sonido hasta que el usuario interactúa con la pantalla. Solucionamos esto con una técnica profesional: reproducir un audio silencioso invisible en el primer toque, «desbloqueando» el permiso para que el resto de los sonidos del juego puedan funcionar sin problemas.

Al final de este tutorial el resultado será este:

 

Empecemos con el código HTML, CSS y JAVASCRIPT 

El HTML es el siguiente para nuestra aplicacion de realidad aumentada:

<!DOCTYPE html>
<html>
<head>
<!-- Define la codificación de caracteres para soportar tildes y ñ -->
<meta charset="UTF-8">

<!-- Asegura que la página se vea bien en dispositivos móviles y no sea escalable por el usuario -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<!-- Título de la página que aparecerá en la pestaña del navegador -->
<title>AR Coin Hunter Game - Parte 2</title>

<!-- Import Map: Define alias para las URL de las librerías de Three.js y MindAR -->
<!-- Esto nos permite usar 'three' en lugar de la URL completa en nuestro JavaScript -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
"mindar-image-three": "https://cdn.jsdelivr.net/npm/mind-ar@1.2.5/dist/mindar-image-three.prod.js"
}
}
</script>

<!-- Enlace a nuestra hoja de estilos externa que definirá la apariencia de la UI -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Etiqueta de audio oculta con un sonido silencioso (codificado en Base64) -->
<!-- Su única función es ser reproducida en el primer toque para "desbloquear" el permiso de audio en móviles -->
<audio id="unlock-audio" src="data:audio/mpeg;base64"></audio>

<!-- Contenedor principal donde MindAR renderizará la escena de Three.js -->
<div id="ar-container"></div>

<!-- Contenedor para toda la interfaz de usuario (joystick, botones, marcadores), superpuesto a la escena AR -->
<div id="ui-container">
<!-- Contenedor del joystick de movimiento -->
<div id="joystick-container">
<div id="joystick-base">
<div id="joystick-handle"></div>
</div>
</div>

<!-- Contenedor para los botones de acción A y B -->
<div id="buttons-container">
<div id="button-b" class="action-button">B</div>
<div id="button-a" class="action-button">A</div>
</div>

<!-- Panel para mostrar la puntuación de monedas -->
<div id="score-panel">
Monedas: <span id="score-value">0/5</span>
</div>

<!-- Panel para mostrar el cronómetro -->
<div id="timer-panel">
Tiempo: <span id="timer-value">00:00</span>
</div>

<!-- Mensaje de victoria, inicialmente oculto gracias a la clase 'hidden' -->
<div id="win-message" class="hidden">
¡GANASTE!
</div>
</div>

<!-- Enlace a nuestro archivo de JavaScript externo, de tipo 'module' para poder usar 'import' -->
<script src="main.js" type="module"></script>
</body>
</html>

Ahora que ya hemos logrado terminar nuestro HTML pasemos a la sección del estilo CSS. Esta es la encargada de darle reglas y crea el como vamos a ver nuestro video juego WebAR.

/* Estilos generales para el body de la página */
body {
margin: 0; /* Elimina el margen por defecto del navegador */
font-family: Arial, sans-serif; /* Establece una fuente legible */
/* Evita el rebote y el gesto de "jalar para refrescar" en navegadores móviles, especialmente en iOS */
overscroll-behavior-y: contain;
}

/* Contenedor principal de la escena AR que ocupará toda la pantalla */
#ar-container {
width: 100vw; /* 100% del ancho de la ventana */
height: 100vh; /* 100% del alto de la ventana */
position: relative; /* Necesario para posicionar elementos hijos de forma absoluta */
overflow: hidden; /* Oculta cualquier contenido que se desborde */
}

/* Contenedor de la UI, superpuesto a la escena AR */
#ui-container {
position: absolute; /* Se posiciona sobre el contenedor AR */
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10; /* Asegura que esté por encima de la escena AR */
pointer-events: none; /* Permite que los clics "atraviesen" este contenedor, excepto en los elementos hijos que lo reactiven */
}

/* Contenedor del joystick, posicionado en la esquina inferior izquierda */
#joystick-container {
position: absolute;
bottom: 30px;
left: 30px;
width: 120px;
height: 120px;
pointer-events: auto; /* Reactiva los eventos de puntero para que el joystick sea interactivo */
}

/* La base circular del joystick */
#joystick-base {
position: relative;
width: 100%;
height: 100%;
background: rgba(128, 128, 128, 0.4); /* Fondo gris semitransparente */
border-radius: 50%; /* Lo hace perfectamente circular */
border: 2px solid rgba(255, 255, 255, 0.5); /* Borde blanco semitransparente */
}

/* El mango del joystick que se mueve */
#joystick-handle {
position: absolute;
width: 50px;
height: 50px;
background: rgba(255, 255, 255, 0.7); /* Fondo blanco más opaco */
border-radius: 50%; /* También circular */
left: 50%;
top: 50%;
transform: translate(-50%, -50%); /* Centra el mango perfectamente en la base */
cursor: grab; /* Cambia el cursor para indicar que es un objeto arrastrable */
}

/* Contenedor de los botones de acción, en la esquina inferior derecha */
#buttons-container {
position: absolute;
bottom: 40px;
right: 30px;
display: flex; /* Usa flexbox para alinear los botones */
gap: 20px; /* Espacio entre los botones */
align-items: center; /* Centra los botones verticalmente */
pointer-events: auto; /* Permite la interacción con los botones */
}

/* Estilo común para todos los botones de acción */
.action-button {
width: 60px;
height: 60px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.7);
display: flex; /* Permite centrar el texto dentro del botón */
justify-content: center; /* Centra el texto horizontalmente */
align-items: center; /* Centra el texto verticalmente */
font-size: 24px;
font-weight: bold;
color: white;
user-select: none; /* Evita que el texto del botón pueda ser seleccionado */
cursor: pointer; /* Muestra una mano para indicar que es clickeable */
transition: background-color 0.2s; /* Añade una transición suave al cambiar de color */
}

/* Color de fondo específico para el botón A */
#button-a {
background: rgba(220, 50, 50, 0.6);
}

/* Color de fondo específico para el botón B */
#button-b {
background: rgba(50, 150, 220, 0.6);
}

/* Estilo para el botón A cuando está en período de enfriamiento (cooldown) */
#button-a.cooldown {
background: rgba(100, 100, 100, 0.6); /* Color gris para indicar que está inactivo */
cursor: not-allowed; /* Cambia el cursor para mostrar que no se puede usar */
}

/* Panel de puntuación, en la esquina superior izquierda */
#score-panel {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 10px 15px;
border-radius: 10px;
font-size: 18px;
font-weight: bold;
}

/* Panel del cronómetro, en la esquina superior derecha */
#timer-panel {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 10px 15px;
border-radius: 10px;
font-size: 18px;
font-weight: bold;
font-family: monospace; /* Fuente monoespaciada para que los números no "bailen" */
}

/* Mensaje de victoria, centrado en la pantalla */
#win-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* Truco para centrar perfectamente el elemento */
background: linear-gradient(45deg, #ffc107, #ff9800); /* Fondo con gradiente llamativo */
color: white;
padding: 30px 50px;
border-radius: 20px;
font-size: 48px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5); /* Sombra para que el texto resalte */
border: 4px solid white;
}

/* Clase de utilidad para ocultar elementos visualmente */
.hidden {
display: none;
}

Y para finalizar nuestro código está el JavaScript. Este es el más importante de nuestra aplicación AR pues es se encarga de la lógica y básicamente es la que permite que todo funcione y que funcione bien. 

// Importamos los módulos necesarios de las librerías Three.js y MindAR
// THREE: El núcleo de la librería para renderizado 3D.
// MindARThree: El conector entre MindAR (la Realidad Aumentada) y Three.js.
// GLTFLoader: Una extensión para cargar modelos 3D en formato .gltf o .glb.
import * as THREE from 'three';
import { MindARThree } from 'mindar-image-three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

// Añadimos un listener que ejecutará todo el código cuando el contenido HTML de la página esté completamente cargado.
document.addEventListener('DOMContentLoaded', () => {

// --- DECLARACIÓN DE VARIABLES GLOBALES ---

// Variables de la escena y AR
let mindarThree = null; // Almacenará la instancia principal de MindAR.
let clock = new THREE.Clock(); // Un reloj para medir el tiempo delta (diferencia entre fotogramas), crucial para animaciones fluidas.

// Variables de los modelos 3D y partículas
let carModel = null; // Almacenará el objeto 3D del coche.
let mixer = null; // El AnimationMixer de Three.js, para controlar las animaciones del modelo.
let carAnimationAction = null; // La acción de animación específica del coche.
let smokeParticleSystem = null, smokeParticleGeometry = null; // Objetos para el sistema de partículas de humo continuo.
let burstParticleSystem = null, burstParticlesData = []; // Objetos para la ráfaga de humo del turbo.
const smokeParticleCount = 50, burstParticleCount = 100; // Cantidad de partículas para cada efecto.

// Variables del Juego
let coins = []; // Un array para guardar todos los objetos de las monedas en la escena.
const TOTAL_COINS = 5; // Constante que define cuántas monedas hay que recolectar.
let score = 0; // Puntuación actual del jugador.
let coinModel = null; // Almacenará el modelo 3D base de la moneda para poder clonarlo.

// Variables de Audio
let collectSound = null; // Almacenará el sonido que se reproduce al recolectar una moneda.
let listener = null; // El "oído" de la escena, necesario para el audio posicional.

// Variables de la UI y Controles
let moveVector = { x: 0, y: 0 }; // Un objeto para almacenar la dirección del joystick (-1 a 1).
let isDragging = false; // Bandera para saber si el joystick está siendo arrastrado.
let joystickTouchId = null; // Para identificar el dedo específico que controla el joystick en pantallas táctiles.

// Variables para el Cronómetro y Estado del Juego
let gameStarted = false; // Bandera para saber si el juego ha comenzado.
let gameWon = false; // Bandera para saber si el jugador ha ganado.
let startTime = 0; // Guardará el momento exacto en que empieza el juego para calcular el tiempo transcurrido.

// Variables para el Turbo
const normalSpeed = 0.3; // Velocidad de movimiento base del coche.
const turboSpeed = 1.5; // Velocidad cuando el turbo está activado.
let currentSpeed = normalSpeed; // La velocidad que se usa en cada fotograma, empieza siendo la normal.
let isTurboOnCooldown = false; // Bandera para saber si el turbo está en período de enfriamiento.

// --- FUNCIÓN PRINCIPAL DE CONFIGURACIÓN DE AR ---
// Esta función es 'async' porque necesitamos esperar a que se carguen modelos y se inicie MindAR.
const setupAR = async () => {
// Inicializa MindAR, vinculándolo al contenedor HTML y especificando el archivo de marcadores.
mindarThree = new MindARThree({ container: document.querySelector("#ar-container"), imageTargetSrc: "./assets/targets.mind" });
// Extraemos el renderizador, la escena y la cámara de la instancia de MindAR.
const { renderer, scene, camera } = mindarThree;

// --- ILUMINACIÓN ---
// Añadimos una luz ambiental que ilumina todos los objetos de la escena de manera uniforme.
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
// Añadimos una luz direccional, como el sol, para crear sombras y dar más realismo.
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 7.5); // La posicionamos arriba y a un lado.
scene.add(directionalLight);

// --- ANCLA DE AR ---
// Creamos un ancla en el primer marcador (índice 0) del archivo targets.mind.
// Todo lo que añadamos al `anchor.group` aparecerá sobre este marcador.
const anchor = mindarThree.addAnchor(0);

// --- INICIALIZACIÓN DE LOADERS ---
// Creamos una instancia del cargador de modelos GLTF.
const gltfLoader = new GLTFLoader();
// Creamos una instancia del cargador de texturas.
const textureLoader = new THREE.TextureLoader();

// --- CONFIGURACIÓN DE AUDIO ---
// Creamos un 'AudioListener' y lo añadimos a la cámara. La cámara actúa como los oídos del jugador.
listener = new THREE.AudioListener();
camera.add(listener);
// Creamos un cargador de audio.
const audioLoader = new THREE.AudioLoader();
// Creamos un objeto de Audio global, que usará nuestro listener.
collectSound = new THREE.Audio(listener);
// Cargamos el archivo de sonido y, cuando esté listo, lo asignamos a nuestro objeto de audio.
audioLoader.load('./assets/collect.mp3', (buffer) => {
collectSound.setBuffer(buffer); // Asigna el audio cargado.
collectSound.setVolume(0.8); // Ajusta el volumen.
});

// --- CARGA DEL MODELO DEL COCHE ---
gltfLoader.load("./assets/low-poly_cartoon_style_car_03.glb", (gltf) => {
carModel = gltf.scene; // El modelo 3D está en la propiedad 'scene' del archivo gltf.
carModel.rotation.x = Math.PI / 2; // Rota el coche para que quede plano sobre el marcador.
carModel.scale.set(0.3, 0.3, 0.3); // Hacemos el coche un 30% de su tamaño original.
carModel.position.set(0, 0, 0); // Lo centramos en el ancla.
anchor.group.add(carModel); // Añadimos el coche al ancla para que aparezca en la AR.

// Si el modelo tiene animaciones, las configuramos.
if (gltf.animations && gltf.animations.length) {
mixer = new THREE.AnimationMixer(carModel); // Creamos un mixer para este modelo.
const clip = gltf.animations[0]; // Tomamos la primera animación.
carAnimationAction = mixer.clipAction(clip); // Creamos una acción de animación.
carAnimationAction.setLoop(THREE.LoopOnce); // Hacemos que la animación se reproduzca solo una vez.
carAnimationAction.clampWhenFinished = true; // Mantiene el estado final de la animación.
}

// Una vez cargado el coche, creamos los efectos de humo asociados a él.
createContinuousSmoke();
createBurstSmoke();
});

// --- CARGA DEL MODELO DE LA MONEDA ---
gltfLoader.load("./assets/coin.glb", (gltf) => {
coinModel = gltf.scene; // Guardamos el modelo base de la moneda.
coinModel.rotation.x = Math.PI / 2; // La ponemos "de pie" sobre el marcador.
coinModel.scale.set(0.1, 0.1, 0.1); // La hacemos un 10% de su tamaño.
spawnCoins(); // Llamamos a la función que creará las 5 monedas en la escena.
});

// --- CARGA DE TEXTURA DE HUMO ---
// Usamos 'await' para asegurarnos de que la textura esté lista antes de crear las partículas.
const smokeTexture = await textureLoader.loadAsync('./assets/Humo.png');

// --- FUNCIONES DE PARTÍCULAS ---
// (Estas funciones crean la geometría y material para los efectos de humo)
const createContinuousSmoke = () => { /* ...código de partículas... */ smokeParticleGeometry = new THREE.BufferGeometry(); const positions = new Float32Array(smokeParticleCount * 3); for (let i = 0; i < smokeParticleCount * 3; i++) positions[i] = (Math.random() - 0.5) * 0.1; smokeParticleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const particleMaterial = new THREE.PointsMaterial({ map: smokeTexture, size: 400.0, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true }); smokeParticleSystem = new THREE.Points(smokeParticleGeometry, particleMaterial); smokeParticleSystem.position.z = -1; carModel.add(smokeParticleSystem); };
const createBurstSmoke = () => { /* ...código de partículas... */ const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(burstParticleCount * 3); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const material = new THREE.PointsMaterial({ map: smokeTexture, size: 600.0, transparent: true, opacity: 0, depthWrite: false, sizeAttenuation: true }); burstParticleSystem = new THREE.Points(geometry, material); burstParticleSystem.position.z = -1; burstParticleSystem.visible = false; carModel.add(burstParticleSystem); };

// --- FUNCIÓN PARA CREAR LAS MONEDAS ---
const spawnCoins = () => {
if (!coinModel) return; // Si el modelo de moneda aún no ha cargado, no hacemos nada.
// Creamos un bucle para generar el número total de monedas definido en TOTAL_COINS.
for (let i = 0; i < TOTAL_COINS; i++) {
const newCoin = coinModel.clone(); // Clonamos el modelo base para crear una nueva moneda.
// Le damos una posición aleatoria en el plano X e Y.
newCoin.position.set( (Math.random() - 0.5) * 1.8, (Math.random() - 0.5) * 1.8, 0 );
// Le asignamos una propiedad personalizada para su velocidad de rotación única.
newCoin.userData.rotationSpeed = (Math.random() - 0.5) * 4;
coins.push(newCoin); // Añadimos la nueva moneda a nuestro array.
anchor.group.add(newCoin); // La añadimos a la escena AR.
}
};

// Iniciamos el motor de MindAR.
await mindarThree.start();

// --- BUCLE PRINCIPAL DE ANIMACIÓN Y JUEGO ---
// Esta función se ejecutará en un bucle continuo (aproximadamente 60 veces por segundo).
renderer.setAnimationLoop(() => {
const delta = clock.getDelta(); // Obtenemos el tiempo transcurrido desde el último fotograma.
if (mixer) mixer.update(delta); // Actualizamos el mixer de animación del coche.

// Movemos el coche según el vector del joystick y la velocidad actual.
if (carModel) {
carModel.position.x += moveVector.x * currentSpeed * delta;
carModel.position.y -= moveVector.y * currentSpeed * delta; // Usamos -= en Y porque el joystick está invertido.
// Rotamos el coche para que mire en la dirección del movimiento.
if (moveVector.x !== 0 || moveVector.y !== 0) { carModel.rotation.y = Math.atan2(moveVector.x, moveVector.y); }
}

// Animamos las monedas.
if (carModel) {
coins.forEach(coin => {
if (coin.visible) { coin.rotation.y += coin.userData.rotationSpeed * delta; } // Rotamos cada moneda en su eje Y.
});
if (!gameWon) checkCollisions(); // Si el juego no ha terminado, comprobamos colisiones.
}

// Actualizamos el cronómetro.
if (gameStarted && !gameWon) {
const elapsedTime = Date.now() - startTime; // Calculamos el tiempo transcurrido.
const seconds = Math.floor(elapsedTime / 1000) % 60; // Extraemos los segundos.
const minutes = Math.floor(elapsedTime / (1000 * 60)); // Extraemos los minutos.
// Actualizamos el texto en el HTML, usando padStart para añadir un 0 si es necesario (ej. 01:05).
document.getElementById('timer-value').innerText = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}

// Actualizamos las partículas de humo.
if (smokeParticleSystem) { /* ...código de partículas... */ const positions = smokeParticleGeometry.attributes.position.array; for (let i = 0; i < smokeParticleCount * 3; i += 3) { positions[i + 2] -= 0.005; if (positions[i + 2] < -0.6) { positions[i + 2] = Math.random() * 0.05; positions[i] = (Math.random() - 0.5) * 0.05; } } smokeParticleGeometry.attributes.position.needsUpdate = true; }
if (burstParticleSystem && burstParticleSystem.visible) { /* ...código de partículas... */ const positions = burstParticleSystem.geometry.attributes.position.array; for (let i = 0; i < burstParticleCount; i++) { positions[i * 3] += burstParticlesData[i].velocity.x; positions[i * 3 + 1] += burstParticlesData[i].velocity.y; positions[i * 3 + 2] += burstParticlesData[i].velocity.z; } burstParticleSystem.material.opacity -= delta * 1.5; if (burstParticleSystem.material.opacity <= 0) { burstParticleSystem.visible = false; } burstParticleSystem.geometry.attributes.position.needsUpdate = true; }

// Renderizamos la escena con la cámara actualizada.
renderer.render(scene, camera);
});
};

// Llamamos a la función principal para que todo empiece a funcionar.
setupAR();

// --- FUNCIONES AUXILIARES DEL JUEGO ---

// Inicia el juego y el cronómetro.
const startGame = () => {
if (gameStarted) return; // Si ya empezó, no hacemos nada.

// Solución para el audio en móviles: reproduce un sonido silencioso en el primer toque.
document.getElementById('unlock-audio').play().catch(() => {});
// Como respaldo, intenta reanudar el contexto de audio si estaba suspendido.
if (listener.context.state === 'suspended') {
listener.context.resume();
}

gameStarted = true; // Marcamos el juego como iniciado.
startTime = Date.now(); // Guardamos el tiempo de inicio.
};

// Comprueba si el coche ha chocado con alguna moneda.
const checkCollisions = () => {
if (!carModel) return;
const carPosition = carModel.position;
coins.forEach(coin => {
if (coin.visible) {
const distance = carPosition.distanceTo(coin.position); // Calcula la distancia entre el coche y la moneda.
if (distance < 0.15) { // Si la distancia es menor a un umbral, es una colisión.
coin.visible = false; // Ocultamos la moneda.
score++; // Incrementamos la puntuación.
if (collectSound && !collectSound.isPlaying) { collectSound.play(); } // Reproducimos el sonido.
updateScoreUI(); // Actualizamos la UI.
checkWinCondition(); // Comprobamos si ha ganado.
}
}
});
};

// Actualiza el texto de la puntuación en el HTML.
const updateScoreUI = () => { document.getElementById('score-value').innerText = `${score}/${TOTAL_COINS}`; };

// Comprueba si se han recogido todas las monedas.
const checkWinCondition = () => {
if (score >= TOTAL_COINS) {
gameWon = true; // Marcamos el juego como ganado.
document.getElementById('win-message').classList.remove('hidden'); // Mostramos el mensaje de victoria.
}
};

// --- LÓGICA DE CONTROLES Y EVENTOS ---

// Obtenemos las referencias a los elementos HTML de los controles.
const joystickHandle = document.getElementById('joystick-handle'); const joystickBase = document.getElementById('joystick-base'); const buttonA = document.getElementById('button-a'); const buttonB = document.getElementById('button-b'); let joystickRadius = 0;

// (El resto de esta sección contiene las funciones que manejan las interacciones del usuario con el joystick y los botones,
// tanto para ratón como para pantalla táctil. Cada interacción llama a 'startGame()' para asegurar que el juego comience).
const updateJoystickPosition = (clientX, clientY) => { if (joystickRadius === 0) joystickRadius = joystickBase.offsetWidth / 2; const baseRect = joystickBase.getBoundingClientRect(); const centerX = baseRect.left + joystickRadius; const centerY = baseRect.top + joystickRadius; let deltaX = clientX - centerX; let deltaY = clientY - centerY; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); if (distance > joystickRadius) { deltaX = (deltaX / distance) * joystickRadius; deltaY = (deltaY / distance) * joystickRadius; } joystickHandle.style.transform = `translate(-50%, -50%) translate(${deltaX}px, ${deltaY}px)`; moveVector.x = deltaX / joystickRadius; moveVector.y = deltaY / joystickRadius; };
const resetJoystick = () => { isDragging = false; joystickHandle.style.transform = `translate(-50%, -50%)`; joystickHandle.style.cursor = 'grab'; moveVector = { x: 0, y: 0 }; };
const joystickStart = (e) => { e.preventDefault(); startGame(); isDragging = true; joystickHandle.style.cursor = 'grabbing'; };
const joystickMove = (e) => { if (!isDragging) return; updateJoystickPosition(e.clientX, e.clientY); };
const joystickEnd = () => { if (isDragging) resetJoystick(); };
const joystickTouchStart = (e) => { if (joystickTouchId !== null) return; e.preventDefault(); startGame(); const touch = e.changedTouches[0]; joystickTouchId = touch.identifier; isDragging = true; joystickHandle.style.cursor = 'grabbing'; };
const joystickTouchMove = (e) => { if (joystickTouchId === null) return; e.preventDefault(); for (let i = 0; i < e.changedTouches.length; i++) { const touch = e.changedTouches[i]; if (touch.identifier === joystickTouchId) { updateJoystickPosition(touch.clientX, touch.clientY); break; } } };
const joystickTouchEnd = (e) => { if (joystickTouchId === null) return; for (let i = 0; i < e.changedTouches.length; i++) { const touch = e.changedTouches[i]; if (touch.identifier === joystickTouchId) { resetJoystick(); joystickTouchId = null; break; } } };

// Función para el botón A (Turbo).
const pressButtonA = (e) => {
e.preventDefault();
startGame();
if (isTurboOnCooldown) return; // Si está en enfriamiento, no hace nada.
isTurboOnCooldown = true; // Activa el enfriamiento.
currentSpeed = turboSpeed; // Cambia la velocidad a turbo.
buttonA.classList.add('cooldown'); // Cambia el estilo del botón.
if (carModel && burstParticleSystem && !burstParticleSystem.visible) { /* ...código de partículas... */ const positions = burstParticleSystem.geometry.attributes.position.array; burstParticlesData = []; for (let i = 0; i < burstParticleCount; i++) { positions[i * 3] = 0; positions[i * 3 + 1] = 0; positions[i * 3 + 2] = 0; burstParticlesData.push({ velocity: new THREE.Vector3((Math.random() - 0.5) * 0.01, (Math.random() - 0.5) * 0.01, (Math.random() - 0.5) * 0.05 - 0.05) }); } burstParticleSystem.geometry.attributes.position.needsUpdate = true; burstParticleSystem.material.opacity = 1.0; burstParticleSystem.visible = true; }
setTimeout(() => { currentSpeed = normalSpeed; }, 2000); // Después de 2s, vuelve a la velocidad normal.
setTimeout(() => { isTurboOnCooldown = false; buttonA.classList.remove('cooldown'); }, 5000); // Después de 5s, el turbo está disponible de nuevo.
};

// Función para el botón B (Animación).
const pressButtonB = (e) => {
e.preventDefault();
startGame();
if (carAnimationAction) { carAnimationAction.reset().play(); } // Reproduce la animación del coche.
};

// Bloqueo definitivo del scroll y pull-to-refresh en móviles.
window.addEventListener('touchmove', function (event) {
event.preventDefault(); // Cancela la acción por defecto del navegador para el evento 'touchmove'.
}, { passive: false }); // 'passive: false' es necesario para poder usar preventDefault().

// Asignamos todas las funciones a los eventos correspondientes (mousedown/touchstart, etc.).
joystickHandle.addEventListener('mousedown', joystickStart); document.addEventListener('mousemove', joystickMove); document.addEventListener('mouseup', joystickEnd); buttonA.addEventListener('mousedown', pressButtonA); buttonB.addEventListener('mousedown', pressButtonB);
joystickHandle.addEventListener('touchstart', joystickTouchStart, { passive: false }); document.addEventListener('touchmove', joystickTouchMove, { passive: false }); document.addEventListener('touchend', joystickTouchEnd); document.addEventListener('touchcancel', joystickTouchEnd); buttonA.addEventListener('touchstart', pressButtonA, { passive: false }); buttonB.addEventListener('touchstart', pressButtonB, { passive: false });
});

Al finalizar esta guía paso a paso desde cero y gratis de cómo hacer un video juego usando realidad aumentada y deberías poder visualizar tu video juego de la siguiente manera:

VideoGame WebAR Augmented Reality

¿Tienes errores y no funciona como debería? 

¡Felicidades! Ahora tienes un videojuego de Realidad Aumentada completo y funcional. Hemos pasado de una simple visualización a una experiencia interactiva con objetivos, desafíos y pulido técnico.

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Scroll al inicio
0
Would love your thoughts, please comment.x
()
x