Del Píxel al Mundo Real: La Nueva Era de las Mascotas Virtuales
Aprende como hacer una mascota virtual para tu video juego o Tamagotchi

¿Recuerdas la fiebre de los Tamagotchis? Aquellas pequeñas mascotas de bolsillo que cuidábamos con unos cuantos botones se convirtieron en un fenómeno global. Nos enseñaron sobre la responsabilidad digital y crearon un vínculo emocional con un puñado de píxeles. Ahora, imagina llevar esa idea a un nivel completamente nuevo: una mascota que no vive en una pantalla, sino en tu propia habitación, y que no responde a botones, sino al sonido de tu voz.
Gracias a la magia de la Realidad Aumentada (AR) y el reconocimiento de voz, ese futuro ya está aquí. El código que tenemos entre manos es la prueba viviente, un portal que trae a la vida a un compañero 3D con una doble personalidad: puede ser un fiel amigo o un poderoso guerrero de combate.
La Magia Detrás del Telón: ¿Cómo Funciona?
Este proyecto es una increíble sinfonía de tecnologías web que trabajan en conjunto para crear una experiencia inmersiva:
- Realidad Aumentada (MindAR): Utiliza la cámara de tu teléfono para detectar un marcador y anclar el modelo 3D en el mundo real, haciendo que parezca que está realmente en tu mesa o en el suelo de tu sala.
Para este tutorial vamos a utilizar una imagen creada por Google Gemini:

El modelo 3D que vamos a utilizar para este tutorial lo conseguí de manera gratuita en: Cartoon Cat – Download Free 3D model by Baydinman (@baydinman) [4a738ea] – Muchas gracias por compartir este modelo con todas sus animaciones.
- Animación de Modelos 3D (A-Frame y Three.js): Se encarga de renderizar el personaje 3D y gestionar su complejo esqueleto de animaciones. Cada uno de sus 25 movimientos, desde un simple saludo hasta un ataque final, es controlado por este motor.
- Comandos de Voz (Web Speech API): Este es el corazón de la interacción. La API de reconocimiento de voz del navegador se mantiene en un estado de escucha continua, analizando todo lo que dices para encontrar palabras clave que activen una de las animaciones del modelo.
El resultado es una comunicación fluida y natural. Ya no necesitas un mando o una pantalla táctil; tu voz es el único control que necesitas.
¿Cuál es el potencial para este tipo de tutorial?
Potencial 1: Un Videojuego de Acción en tu Mesa
El código actual está configurado para la batalla. Con un modelo 3D que tiene 25 animaciones de combate, las posibilidades son enormes. Simplemente con decir «ataca con la espada» o «disparo rápido», tu compañero AR obedece al instante, acompañado de un efecto de sonido que añade epicentro a la acción.
Podemos imaginar un videojuego donde luchamos contra hordas de enemigos virtuales que aparecen en nuestro entorno, dirigiendo a nuestro campeón con órdenes tácticas. «¡Esquiva!», «¡Ataque fuerte!», «¡Modo pistola!». La estrategia y la inmersión alcanzarían un nivel nunca antes visto en los juegos móviles.
Potencial 2: El Tamagotchi del Siglo XXI
Pero, ¿qué pasa si cambiamos el modelo 3D de un guerrero a un cachorro adorable o un gato juguetón? La misma tecnología que impulsa un juego de acción puede dar vida a la mascota virtual más avanzada jamás creada.
Los comandos de voz se transformarían. En lugar de órdenes de combate, le diríamos:
- «Hola, ¿cómo estás?» y nos respondería moviendo la cola.
- «Siéntate» y obedecería al instante.
- «¿Quieres jugar?» y traería una pelota virtual.
Esta es la evolución natural del Tamagotchi. Una mascota virtual con la que podemos hablar, interactuar y crear un vínculo emocional mucho más profundo, viéndola moverse e interactuar con nuestro propio mundo.
Dividamos toda la app de realidad aumentada en tres partes: HTML, CSS y JavaScript.
Comencemos con la parte del HTML:
<!DOCTYPE html> <html> <head> <!-- Define el conjunto de caracteres como UTF-8 para soportar tildes y caracteres especiales --> <meta charset="UTF-8"> <!-- Título que aparecerá en la pestaña del navegador --> <title>Mascota virtual en realidad aumentada - Videojuego o Tamagotchi</title> <!-- Configura el viewport para que la página se vea bien en dispositivos móviles --> <meta name="viewport" content="width=device-width, initial-scale=1" /> <!-- Importa la librería principal de A-Frame para la Realidad Virtual y Aumentada --> <script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script> <!-- Importa la librería de MindAR para el reconocimiento de imágenes en A-Frame --> <script src="https://cdn.jsdelivr.net/npm/mind-ar@1.2.5/dist/mindar-image-aframe.prod.js"></script> <!-- Enlaza la hoja de estilos CSS externa que define la apariencia de la interfaz --> <link rel="stylesheet" href="style.css"> </head> <body> <!-- ========= INTERFAZ DE USUARIO ========= --> <!-- Contenedor principal para todos los elementos de la interfaz (botón y texto de estado) --> <div id="ui-container"> <!-- Botón que el usuario presionará para activar o desactivar el micrófono --> <button id="voice-button">Activar Micrófono</button> <!-- Párrafo que mostrará mensajes de estado al usuario (ej: "Escuchando...", "Comando reconocido") --> <p id="status-text">Activa el micrófono para hablar con el robot</p> </div> <!-- ========= ESCENA DE REALIDAD AUMENTADA ========= --> <!-- Define la escena de A-Frame donde ocurrirá toda la magia de la RA --> <a-scene <!-- Configura MindAR para que use el archivo 'targets.mind' como el marcador a reconocer --> mindar-image="imageTargetSrc: ./targets.mind;" <!-- Asegura una correcta gestión de colores para que el modelo 3D se vea bien --> color-space="sRGB" renderer="colorManagement: true, physicallyCorrectLights" <!-- Deshabilita la interfaz de usuario predeterminada de modo VR y de permisos, ya que tenemos la nuestra --> vr-mode-ui="enabled: false" device-orientation-permission-ui="enabled: false"> <!-- 'a-assets' es un contenedor para precargar todos los recursos (modelos 3D, sonidos, etc.) --> <a-assets> <!-- Precarga el modelo 3D del robot desde el archivo 'cat.glb' y le asigna el ID 'robot-model' --> <a-asset-item id="robot-model" src="./cat.glb"></a-asset-item> <!-- Precarga el archivo de sonido y le asigna el ID 'animation-sound' --> <audio id="animation-sound" src="./sound.mp3" preload="auto"></audio> </a-assets> <!-- Define la cámara a través de la cual el usuario verá el mundo --> <a-camera position="0 0 0" look-controls="enabled: false"></a-camera> <!-- Entidad que actúa como ancla. Su contenido solo será visible cuando MindAR detecte el marcador 'targetIndex: 0' --> <a-entity mindar-image-target="targetIndex: 0"> <!-- Muestra el modelo 3D precargado en la escena --> <a-gltf-model id="robot" rotation="0 0 0" position="0 -0.5 0" scale="0.5 0.5 0.5" src="#robot-model" <!-- Adjunta nuestro componente personalizado 'voice-control', que contiene toda la lógica de JavaScript --> voice-control> </a-gltf-model> </a-entity> </a-scene> <!-- Importa el archivo JavaScript externo que contiene toda la lógica de la aplicación --> <script src="script.js"></script> </body> </html>
Pasemos a la parte del CSS:
Aunque esta WebAR no tiene muchos estilos pues está enfocada más hacia el modelo 3D y los comandos voz.
/* ========= ESTILOS GENERALES ========= */
/* Define los estilos base para todo el cuerpo del documento */
body {
margin: 0; /* Elimina el margen predeterminado del navegador para que la escena ocupe toda la pantalla */
font-family: 'Helvetica', 'Arial', sans-serif; /* Establece una fuente limpia y legible para todo el texto */
}
/* ========= CONTENEDOR DE LA INTERFAZ DE USUARIO ========= */
/* Estilos para el panel principal que contiene el botón y el texto de estado */
#ui-container {
position: fixed; /* Fija el panel en la pantalla, para que no se mueva al hacer scroll */
bottom: 0; /* Lo posiciona en la parte inferior de la pantalla */
left: 50%; /* Lo posiciona horizontalmente en el centro */
transform: translateX(-50%); /* Ajusta la posición horizontal para centrarlo perfectamente */
padding: 15px; /* Añade espacio interno para que los elementos no toquen los bordes */
z-index: 10; /* Asegura que la interfaz esté siempre por encima de la escena de Realidad Aumentada */
text-align: center; /* Centra el texto y el botón dentro del panel */
background: rgba(0, 0, 0, 0.6); /* Fondo negro semitransparente para que sea legible sin ocultar completamente la vista de la cámara */
border-radius: 20px 20px 0 0; /* Redondea las esquinas superiores para un look más moderno */
color: white; /* Establece el color del texto en blanco para un buen contraste con el fondo oscuro */
width: 90%; /* El panel ocupará el 90% del ancho de la pantalla */
max-width: 400px; /* Limita el ancho máximo en pantallas grandes para que no se vea desproporcionado */
box-sizing: border-box; /* Asegura que el padding no afecte el ancho total del elemento */
}
/* ========= BOTÓN DE CONTROL DE VOZ ========= */
/* Estilos para el botón principal de activar/desactivar micrófono */
#voice-button {
padding: 12px 24px; /* Define el espacio interno del botón, haciéndolo más grande y fácil de presionar */
font-size: 16px; /* Tamaño del texto dentro del botón */
font-weight: bold; /* Texto en negrita para mayor legibilidad */
border: none; /* Elimina el borde predeterminado del botón */
background-color: #007aff; /* Color de fondo azul, típico de botones de acción */
color: white; /* Color del texto en blanco */
border-radius: 25px; /* Redondea completamente las esquinas para un look de "píldora" */
cursor: pointer; /* Cambia el cursor a una mano para indicar que es un elemento clickeable */
transition: background-color 0.3s, transform 0.1s; /* Anima suavemente los cambios de color de fondo y tamaño */
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); /* Añade una sombra sutil para darle profundidad */
}
/* Estilo para el botón cuando el cursor del ratón está sobre él */
#voice-button:hover {
background-color: #0056b3; /* Oscurece el color de fondo para dar feedback visual */
}
/* Estilo para el botón mientras se está presionando */
#voice-button:active {
transform: scale(0.95); /* Encoge ligeramente el botón para simular una pulsación física */
}
/* Estilo especial que se aplica al botón cuando el micrófono está activo */
#voice-button.listening {
background-color: #ff3b30; /* Cambia el color a rojo para indicar claramente que está "grabando" o escuchando */
}
/* ========= TEXTO DE ESTADO ========= */
/* Estilos para el párrafo que muestra mensajes al usuario */
#status-text {
margin-top: 10px; /* Añade un espacio superior para separarlo del botón */
font-size: 14px; /* Tamaño de fuente ligeramente más pequeño que el del botón */
min-height: 20px; /* Define una altura mínima para evitar que el layout "salte" cuando el texto cambia */
}Para finalizar pasemos al cerebro de nuestra aplicacion gratis de realidad aumentada. El Javascript:
// Se registra un nuevo componente de A-Frame llamado 'voice-control'.
// Este componente contendrá toda la lógica para manejar las animaciones y el reconocimiento de voz.
AFRAME.registerComponent('voice-control', {
// La función 'init' se ejecuta una sola vez cuando el componente se carga por primera vez.
init: function () {
// Mensaje de depuración para confirmar que el componente ha comenzado.
console.log("PUNTO 1: Componente 'voice-control' INICIADO.");
// Almacena referencias a elementos importantes para un acceso más fácil.
this.model = this.el; // 'this.el' es la entidad de A-Frame a la que se adjunta este componente (el modelo 3D).
this.mixer = null; // Se inicializa el mezclador de animaciones de Three.js como nulo. Se creará cuando el modelo cargue.
this.actions = {}; // Un objeto para almacenar todas las acciones de animación (clips) del modelo.
this.activeAction = null; // Almacenará la animación que se está reproduciendo actualmente.
this.baseState = 'Arm_Idle'; // Define la animación de reposo a la que el modelo volverá después de una acción.
this.isListening = false; // Un booleano para rastrear si el micrófono está activo o no.
// Busca el elemento de audio en el HTML y lo guarda para reproducirlo más tarde.
this.animationSound = document.querySelector('#animation-sound');
// 'bind(this)' asegura que cuando estas funciones se llamen como event listeners, 'this' se refiera al componente y no al elemento que disparó el evento.
this.toggleListening = this.toggleListening.bind(this);
this.onSpeechResult = this.onSpeechResult.bind(this);
this.onMixerFinished = this.onMixerFinished.bind(this);
// Llama a las funciones que configuran el reconocimiento de voz y las animaciones.
this.setupSpeechRecognition();
// Añade un listener que espera a que el modelo 3D ('gltf-model') se haya cargado completamente.
this.model.addEventListener('model-loaded', () => {
console.log("PUNTO 2: Evento 'model-loaded' disparado.");
// Una vez cargado el modelo, se procede a configurar el sistema de animaciones.
this.setupAnimationMixer();
});
},
// Configura el mezclador de animaciones de Three.js.
setupAnimationMixer: function() {
// Obtiene el objeto 3D (mesh) del componente 'gltf-model' de A-Frame.
const gltf = this.model.getObject3D('mesh');
// Si no se encuentra el mesh o no tiene animaciones, muestra un error y detiene la ejecución.
if (!gltf || !gltf.animations || gltf.animations.length === 0) {
console.error("ERROR CRÍTICO: No se encontró el mesh del modelo o no tiene animaciones.");
return;
}
// Crea una nueva instancia del mezclador de animaciones de Three.js, pasándole el modelo 3D.
this.mixer = new THREE.AnimationMixer(gltf);
// Añade un listener para el evento 'finished', que se dispara cuando una animación de un solo disparo termina.
this.mixer.addEventListener('finished', this.onMixerFinished);
// Obtiene todos los clips de animación del modelo.
const clips = gltf.animations;
// Mensajes de depuración para mostrar que la configuración fue exitosa y listar las animaciones encontradas.
console.log("PUNTO 3: El sistema de animación está listo.");
console.log('====== ANIMACIONES ENCONTRADAS ======');
console.log(clips.map(clip => clip.name)); // Muestra un array con los nombres de todas las animaciones.
console.log('====================================');
// Lista con los nombres de todas las animaciones que esperamos encontrar en el modelo.
const animationNames = [
"Idle_Harmonic", "Idle_Dreamer", "Idle_omical", "Idle_Invasive",
"Arm_Attack_End", "Arm_Attack_Hight", "Arm_Attack_Light", "Arm_Attack_Medium",
"Arm_Death", "Arm_Hit", "Arm_Idle",
"Pistol_Attack_End", "Pistol_Attack_Hight", "Pistol_Attack_Light", "Pistol_Attack_Medium",
"Pistol_Death", "Pistol_Hit", "Pistol_Idle",
"Sword_Attack_End", "Sword_Attack_Hight", "Sword_Attack_Light", "Sword_Attack_Medium",
"Sword_Death", "Sword_Hit", "Sword_Idle"
];
// Lista de animaciones que deben reproducirse solo una vez (y no en bucle).
const oneShotAnimations = [
"Arm_Attack_End", "Arm_Attack_Hight", "Arm_Attack_Light", "Arm_Attack_Medium", "Arm_Death", "Arm_Hit",
"Pistol_Attack_End", "Pistol_Attack_Hight", "Pistol_Attack_Light", "Pistol_Attack_Medium", "Pistol_Death", "Pistol_Hit",
"Sword_Attack_End", "Sword_Attack_Hight", "Sword_Attack_Light", "Sword_Attack_Medium", "Sword_Death", "Sword_Hit"
];
// Recorre la lista de nombres de animaciones.
animationNames.forEach(name => {
// Busca el clip de animación correspondiente en el modelo por su nombre.
const clip = THREE.AnimationClip.findByName(clips, name);
if (clip) {
// Si se encuentra, crea una "acción" de animación para ese clip.
const action = this.mixer.clipAction(clip);
// Almacena la acción en nuestro objeto 'actions' para un acceso fácil.
this.actions[name] = action;
// Si la animación está en la lista de 'oneShotAnimations', la configura para que no se repita en bucle.
if (oneShotAnimations.includes(name)) {
action.setLoop(THREE.LoopOnce); // Se reproduce una vez.
action.clampWhenFinished = true; // Mantiene el último frame al terminar.
}
} else {
// Si una animación esperada no se encuentra en el modelo, muestra una advertencia.
console.warn(`Animación "${name}" no encontrada.`);
}
});
// Una vez configuradas todas las animaciones, inicia la animación de reposo por defecto.
if (this.actions.Arm_Idle) {
this.statusText.textContent = "¡Modelo listo! Activa el micrófono.";
this.playAnimation('Arm_Idle');
} else {
console.error("Animación base 'Arm_Idle' no encontrada.");
}
},
// La función 'tick' se ejecuta en cada frame de la escena de A-Frame. Es el bucle de actualización.
tick: function (time, timeDelta) {
// Si el mezclador de animaciones existe, lo actualiza.
if (this.mixer) {
// 'timeDelta' es el tiempo en milisegundos desde el último frame. Se divide por 1000 para convertirlo a segundos.
this.mixer.update(timeDelta / 1000);
}
},
// Configura la API de Reconocimiento de Voz del navegador.
setupSpeechRecognition: function() {
// Busca la implementación de la API en el objeto 'window' (con prefijos para compatibilidad).
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
// Si el navegador no soporta la API, muestra un mensaje de error.
if (!SpeechRecognition) {
this.statusText.textContent = "Reconocimiento de voz no soportado.";
return;
}
// Crea una nueva instancia del servicio de reconocimiento.
this.recognition = new SpeechRecognition();
// Configura los parámetros del reconocimiento.
this.recognition.lang = 'es-LA'; // Idioma: Español de Latinoamérica.
this.recognition.interimResults = false; // No queremos resultados provisionales, solo el final.
this.recognition.continuous = true; // Modo de escucha continua.
// Obtiene referencias a los elementos de la UI.
this.voiceButton = document.getElementById('voice-button');
this.statusText = document.getElementById('status-text');
// Asigna los listeners de eventos.
this.voiceButton.addEventListener('click', this.toggleListening); // Al hacer clic en el botón.
this.recognition.addEventListener('result', this.onSpeechResult); // Cuando se obtiene un resultado de voz.
// El evento 'onend' se dispara cuando el servicio de voz se detiene (por silencio, etc.).
this.recognition.onend = () => {
// Si se supone que debería estar escuchando, lo reinicia para mantener la escucha continua.
if (this.isListening) {
console.log("El servicio de voz se detuvo, reiniciando...");
this.recognition.start();
}
};
// Maneja cualquier error que ocurra durante el reconocimiento.
this.recognition.onerror = (event) => {
console.error("Error en el reconocimiento de voz:", event);
this.statusText.textContent = `Error: ${event.error}`;
this.isListening = false; // Desactiva el estado de escucha.
this.updateButtonUI(); // Actualiza la UI para reflejar el estado desactivado.
};
},
// Activa o desactiva la escucha del micrófono.
toggleListening: function () {
// Invierte el estado de 'isListening'.
this.isListening = !this.isListening;
if (this.isListening) {
// Si ahora está escuchando, inicia el reconocimiento.
this.recognition.start();
this.statusText.textContent = "Escuchando continuamente...";
} else {
// Si se desactiva, detiene el reconocimiento.
this.recognition.stop();
this.statusText.textContent = "Micrófono desactivado.";
}
// Actualiza la apariencia del botón para reflejar el nuevo estado.
this.updateButtonUI();
},
// Actualiza el texto y el color del botón según el estado de escucha.
updateButtonUI: function() {
if (this.isListening) {
this.voiceButton.textContent = 'Desactivar Micrófono';
this.voiceButton.classList.add('listening'); // Añade la clase 'listening' para ponerlo rojo.
} else {
this.voiceButton.textContent = 'Activar Micrófono';
this.voiceButton.classList.remove('listening'); // Quita la clase 'listening'.
}
},
// Se ejecuta cuando el servicio de voz reconoce un comando.
onSpeechResult: function (event) {
// Obtiene el último resultado de voz reconocido.
const lastResult = event.results[event.results.length - 1];
// Extrae el texto, lo limpia (minúsculas, sin espacios extra, sin puntuación) y lo normaliza para manejar tildes.
const command = lastResult[0].transcript.trim().toLowerCase().replace(/[¿?¡!]/g, '').normalize('NFC');
this.statusText.textContent = `Dijiste: "${command}"`;
let animationToPlay = null; // Variable para almacenar el nombre de la animación a reproducir.
// Larga cadena de 'if/else if' para encontrar una coincidencia entre el comando de voz y una animación.
// Se usan sinónimos y variaciones para una interacción más natural.
if (command.includes("modo armónico") || command.includes("relájate")) animationToPlay = "Idle_Harmonic";
else if (command.includes("modo soñador") || command.includes("ponte a pensar")) animationToPlay = "Idle_Dreamer";
else if (command.includes("modo cómico") || command.includes("haz algo gracioso")) animationToPlay = "Idle_omical";
else if (command.includes("modo invasivo") || command.includes("acecha")) animationToPlay = "Idle_Invasive";
else if (command.includes("golpe final") || command.includes("termina el ataque")) animationToPlay = "Arm_Attack_End";
else if (command.includes("ataque alto") || command.includes("ataque fuerte")) animationToPlay = "Arm_Attack_Hight";
else if (command.includes("ataque ligero") || command.includes("ataque rápido")) animationToPlay = "Arm_Attack_Light";
else if (command.includes("ataque medio") || command.includes("ataca")) animationToPlay = "Arm_Attack_Medium";
else if (command.includes("muere") || command.includes("derrotado")) animationToPlay = "Arm_Death";
else if (command.includes("te hirieron") || command.includes("recibe un golpe")) animationToPlay = "Arm_Hit";
else if (command.includes("manos libres") || command.includes("postura normal") || command.includes("hola")) animationToPlay = "Arm_Idle";
else if (command.includes("último disparo") || command.includes("disparo final")) animationToPlay = "Pistol_Attack_End";
else if (command.includes("disparo alto") || command.includes("apunta arriba")) animationToPlay = "Pistol_Attack_Hight";
else if (command.includes("disparo ligero") || command.includes("disparo rápido")) animationToPlay = "Pistol_Attack_Light";
else if (command.includes("dispara") || command.includes("fuego")) animationToPlay = "Pistol_Attack_Medium";
else if (command.includes("muerte por pistola")) animationToPlay = "Pistol_Death";
else if (command.includes("recibiste un disparo")) animationToPlay = "Pistol_Hit";
else if (command.includes("saca la pistola") || command.includes("modo pistola")) animationToPlay = "Pistol_Idle";
else if (command.includes("estocada final")) animationToPlay = "Sword_Attack_End";
else if (command.includes("espadazo alto") || command.includes("corte alto")) animationToPlay = "Sword_Attack_Hight";
else if (command.includes("espadazo ligero") || command.includes("corte rápido")) animationToPlay = "Sword_Attack_Light";
else if (command.includes("ataca con la espada") || command.includes("corta")) animationToPlay = "Sword_Attack_Medium";
else if (command.includes("muerte por espada")) animationToPlay = "Sword_Death";
else if (command.includes("te cortaron") || command.includes("recibe un corte")) animationToPlay = "Sword_Hit";
else if (command.includes("saca la espada") || command.includes("en guardia") || command.includes("modo espada")) animationToPlay = "Sword_Idle";
console.log(`Comando: "${command}" -> Animación: "${animationToPlay}"`);
// Si se encontró una animación válida para el comando...
if (animationToPlay && this.actions[animationToPlay]) {
// Comprueba si una animación es de un solo disparo para decidir si reproduce el sonido.
const oneShotAnimations = [
"Arm_Attack_End", "Arm_Attack_Hight", "Arm_Attack_Light", "Arm_Attack_Medium", "Arm_Death", "Arm_Hit",
"Pistol_Attack_End", "Pistol_Attack_Hight", "Pistol_Attack_Light", "Pistol_Attack_Medium", "Pistol_Death", "Pistol_Hit",
"Sword_Attack_End", "Sword_Attack_Hight", "Sword_Attack_Light", "Sword_Attack_Medium", "Sword_Death", "Sword_Hit"
];
// El sonido solo se reproduce para las animaciones de acción (one-shot).
if (this.animationSound && oneShotAnimations.includes(animationToPlay)) {
this.animationSound.currentTime = 0; // Reinicia el sonido al principio.
this.animationSound.play(); // Reproduce el sonido.
}
// Llama a la función para ejecutar la animación.
this.playAnimation(animationToPlay);
}
},
// Reproduce una animación específica, gestionando la transición desde la anterior.
playAnimation: function (name) {
// Si la animación no existe o ya se está reproduciendo, no hace nada.
if (!this.actions[name] || this.activeAction === this.actions[name]) return;
// Lista de animaciones de un solo disparo.
const oneShotAnimations = [
"Arm_Attack_End", "Arm_Attack_Hight", "Arm_Attack_Light", "Arm_Attack_Medium", "Arm_Death", "Arm_Hit",
"Pistol_Attack_End", "Pistol_Attack_Hight", "Pistol_Attack_Light", "Pistol_Attack_Medium", "Pistol_Death", "Pistol_Hit",
"Sword_Attack_End", "Sword_Attack_Hight", "Sword_Attack_Light", "Sword_Attack_Medium", "Sword_Death", "Sword_Hit"
];
// Si la nueva animación NO es de un solo disparo, se convierte en el nuevo estado base.
if (!oneShotAnimations.includes(name)) {
this.baseState = name;
}
// Guarda la acción anterior y establece la nueva como la activa.
const prevAction = this.activeAction;
this.activeAction = this.actions[name];
// Si había una animación anterior, la desvanece suavemente.
if (prevAction) {
prevAction.fadeOut(0.3);
}
// Reinicia la nueva animación, la hace visible y la reproduce con un fundido de entrada.
this.activeAction.reset().setEffectiveWeight(1).fadeIn(0.3).play();
},
// Se ejecuta cuando una animación de un solo disparo ha terminado.
onMixerFinished: function (e) {
// Obtiene el nombre de la animación que acaba de terminar.
const finishedActionName = e.action.getClip().name;
const oneShotAnimations = [
"Arm_Attack_End", "Arm_Attack_Hight", "Arm_Attack_Light", "Arm_Attack_Medium", "Arm_Death", "Arm_Hit",
"Pistol_Attack_End", "Pistol_Attack_Hight", "Pistol_Attack_Light", "Pistol_Attack_Medium", "Pistol_Death", "Pistol_Hit",
"Sword_Attack_End", "Sword_Attack_Hight", "Sword_Attack_Light", "Sword_Attack_Medium", "Sword_Death", "Sword_Hit"
];
// Si la animación que terminó es una de las de un solo disparo...
if (oneShotAnimations.includes(finishedActionName)) {
// ...reproduce la animación del estado base para que el modelo vuelva a su postura de reposo.
this.playAnimation(this.baseState);
}
}
});La Próxima Frontera: IA Conversacional y Gestos
Lo que hemos construido es solo el comienzo. El verdadero potencial de esta tecnología se desata cuando empezamos a combinarla con otras herramientas de vanguardia:
- Inteligencia Artificial Conversacional: Imagina conectar la transcripción de la voz a un modelo de lenguaje grande (LLM) como la API de Gemini o ChatGPT. En lugar de limitarse a 25 comandos predefinidos, nuestra mascota virtual podría mantener una conversación real. Podríamos preguntarle «¿cómo estuvo tu día?» y recibir una respuesta generada por IA, o pedirle que «invente una historia de ciencia ficción» y que la narre mientras gesticula.
Aquí en realidad aumentada Empezando Desde Cero ya hemos creado aplicaciones con inteligencia artificial:
Utiliza el micrófono de Android en Realidad Aumentada
Inteligencia artificial en una app de realidad aumentada
Realidad Aumentada en Javascript con inteligencia artificial
- Reconocimiento de Gestos y Manos: ¿Y si además de la voz, nuestro compañero pudiera vernos? Utilizando bibliotecas como MediaPipe de Google, podríamos integrar el reconocimiento de gestos a través de la misma cámara. Levantar el pulgar podría hacer que el robot baile, o mostrar la palma de la mano podría hacer que se detenga. Esta interacción «multimodal» (voz + gestos) es el siguiente gran salto hacia una inmersión total.
Mira el ejemplo que hicimos en el blog: Crea app de realidad aumentada que reconoce tus manos
- Interacción con el Entorno (Plane Detection): Las futuras versiones de WebAR permitirán que el personaje no solo flote sobre un marcador, sino que reconozca superficies como el suelo o las mesas. Podríamos decirle «salta sobre la silla» y ver cómo lo hace, creando una ilusión aún más poderosa de que realmente comparte nuestro espacio.
El Futuro es Conversacional
Este proyecto es más que un simple experimento tecnológico; es una ventana al futuro de la interacción digital. Ya sea comandando un ejército en un videojuego o cuidando de una mascota virtual, la tecnología nos está llevando hacia una era donde nuestras palabras tienen el poder de dar forma a los mundos digitales que nos rodean. La próxima vez que veas a tu personaje favorito en un videojuego, pregúntate: ¿qué pasaría si pudieras hablarle? La respuesta está más cerca de lo que crees.
¿Quieres llevar tu proyecto al siguiente nivel? 🚀 El código fuente completo de este tutorial, incluyendo todos los comandos de animación del modelo 3D que ves en el video, está disponible en exclusiva para nuestra comunidad. ¡Únete ahora al grupo de WhatsApp de ‘Realidad Aumentada Empezando Desde Cero’ y accede a todo el material con este enlace! Ingresa aquí.