Aventura de búsqueda de monedas congeladas

Únete a una nueva aventura con el Capitán Doty y recorre el Mundo Helado. ¡Recoge monedas de oro y escapa de las setas heladas!

frozencoinhuntadventure

Hazlo tuyo con el proyecto de ejemplo

frozencointsplatter

Estudio: Coin Scatter checkmark bullet

Este proyecto te guía sobre cómo esparcir aleatoriamente Monedas en un radio determinado utilizando el componente de Niantic Studio.

Ver ejemplo de proyecto
dotysnowball

Estudio: Doty Snowball Party checkmark bullet

Este proyecto te guiará en la creación de un minijuego con el motor de física de Niantic Studio.

Ver ejemplo de proyecto

Behind the Build: Frozen Coin Hunt Adventure

Written by Alex di Guida

May 16, 2025


Introduction

Esta experiencia es un juego WebAR creado con Niantic Studio Beta, octubre de 2024.

El personaje principal es el famoso Capitán Doty. Enfréntate a tres capítulos diferentes:

The Tiny Mushroom
- Recoge 20 monedas de oro mientras evitas al champiñón diminuto que te persigue.
The Angry Mushroom
- Corecoge 20 monedas de oro mientras evitas al champiñón enfadado que intenta atraparte. Las monedas están todas escondidas, muévete con el copo de nieve mágico flotando cerca para revelar las monedas.
El Rey Champiñón
-En tsu desafío final, inicia una lucha de bolas de nieve con el Rey Champiñón para ganar. Recoge las bolas de nieve delsuelo de y lánzalas al rey para derrotarlo.

Tienes tres corazones que representan tus vidas. Si se pierden los tres, se acaba el juego.

Un temporizador permite al jugador controlar el tiempo que tarda en completar los tres retos, y la puntuación final se muestra una vez ganada la partida.

Hay tres niveles de dificultad, que cambian la velocidad de las setas que te persiguen.

Usa el dedo en la pantalla para mover a Doty y recoger monedas o bolas de nieve.

Project Structure

Escena 3D
  • Entidades Base:  Incluye la cámara AR en Perspectiva y las Luces de Ambiente/Direccionales.
  • Entidades UI: Comprende todos los elementos de la interfaz de usuario que se muestran en la pantalla, proporcionando información y retroalimentación al jugador. Se muestra un tutorial entre cada desafío.

Activos
  • Incluye todos los modelos 3D, archivos de audio e imágenes utilizados en el juego.
Scripts

La carpeta de scripts está dividida en tres carpetas anidadas:
 - Componentes:

Contiene la lógica principal del juego. Incluye las pantallas de interfaz de usuario, el movimiento del jugador, la lógica de generación de monedas en el suelo congelado, los comportamientos de los champiñones y la lógica de seguimiento, así como el gestor del juego, que gestiona los eventos durante la sesión de juego.

- Ayudante:

Incluye varias funciones de utilidad utilizadas a través de los componentes, ayudando a simplificar y organizar la lógica del juego.


 - Clases:

Contiene el material de sombreado utilizado para gestionar el sistema de partículas, que representa la nieve que cae sobre el campo de juego.

Implementation

Un enfoque detallado de los scripts principales del juego, que gestionan toda la lógica central de esta experiencia. 

Cada componente puede tener diferentes escuchas clave para diferentes propósitos:
- game-start: Se activa cuando el juego comienza o se reanuda tras una pausa.
- reset: Maneja el estado de reinicio del componente al pasar al siguiente desafío.
- pause: Detiene animaciones y comprobaciones, como la proximidad entre la seta y el jugador o entre una moneda y el jugador.
- hide: Se utiliza para los elementos de interfaz de usuario, ocultando la interfaz cuando el juego está en pausa o cuando la interfaz de usuario no es necesario.

Los oyentes se envían desde el componente game-manager.ts

Coin

coin.ts

Este es el componente coin que gestiona la creación de una nueva moneda dentro de la escena


         
   ecs.GltfModel.set(world, eid, {url: coinAsset})


   world.events.addListener(eid, ecs.events.GLTF_MODEL_LOADED, (result) => {
     // @ts-ignore
     const {data: {model}} = result


     storeCoinMaterial(model, eid)
     applyRandomPositionToCoin(world, eid)
   })

      

-
- Una vez cargado el modelo, almacena el material de la moneda para utilizarlo posteriormente y gestionar su opacidad.
- Asigna una posición aleatoria a la moneda dentro de un radio determinado en el momento de su creación.

spread-coins.ts

La función spreadCoins es la función llamada en add que se encarga de crear las monedas en la escena:

         
const spreadCoins = (world, component) => {
 const {eid, schema: {totalCoins}} = component


 for (let i = 0; i < totalCoins; i++) {
   const coinEntity = world.createEntity()
   // @ts-ignore
   const floatingSpeed = THREE.MathUtils.randFloat(0.0005, 0.0015)
   // @ts-ignore
   const floatingAmplitude = THREE.MathUtils.randFloat(0.5, 2)
   Coin.set(world, coinEntity, {floatingSpeed, floatingAmplitude})
   world.setParent(coinEntity, eid)  // Set coin created as a children of Coins group
   coins.push(coinEntity)
 }
}

      

Luego tenemos el checkSnowflakeProximity y checkPlayerProximity que se llaman en la función tick cada fotograma en el componente:

La función checkPlayerProximity es responsable de calcular la distancia entre el jugador y una moneda. A medida que el jugador se acerca a la moneda, ésta se recoge y se oculta de la escena, esperando un reinicio antes de comenzar el siguiente desafío

El booleano de recogida evita llamar repetidamente a la función mientras se recoge la moneda.

         
const checkPlayerProximity = (world, component) => {
 coins.forEach((coin) => {
   ......
   const distanceToPlayer = vecCoinPosition.distanceTo(vecPlayerPosition)
   if (!collecting && distanceToPlayer <= collectionDistance) {
     collecting = true
     world.events.dispatch(eid, 'coin-collect')  // Updated UI score
     coins = coins.filter(c => c !== coin)  // Clear collected coin from the coins array
     disableCoin(world, coin)
     collecting = false
   }
 })
}

      

El checkSnowflakeProximity es una función similar. A medida que el jugador se acerca a la moneda, la opacidad del material de la moneda aumenta gradualmente. Si el jugador se acerca lo suficiente a una moneda, ésta se recoge y finalmente se retira de la escena, ya que no será necesaria en la fase final.

Reproductor

player-controller.ts

Este es el componente del reproductor:

         
const handleGameStart = () => {
     HoldDrag.set(world, eid, {factor: 12, lag: 0.25, distanceThreshold: 0.05, cone, help, timer})
   }


   const handleReset = () => {
     HoldDrag.remove(world, eid)


     const position = ecs.Position.cursor(world, eid)
     position.x = 0
     position.y = 0
     position.z = 0


     const quaternion = ecs.Quaternion.cursor(world, eid)
     quaternion.x = 0
     quaternion.y = 0
     quaternion.z = 0
     quaternion.w = 0
   }


   world.events.addListener(eid, 'reset', handleReset)
   world.events.addListener(eid, 'game-start', handleGameStart)

      

El componente HoldDrag se establece al inicio permitiendo mover a Doty alrededor
La función handleReset devuelve a Doty a su posición y rotación inicial al pasar al siguiente reto

hold-drag.ts

Maneja el gesto de mantener y arrastrar el dedo, moviendo a Doty alrededor a través de 3 oyentes principales:

         
world.events.addListener(eid, ecs.input.SCREEN_TOUCH_START, handleStart)
world.events.addListener(eid, ecs.input.SCREEN_TOUCH_MOVE, handleMove)
world.events.addListener(eid, ecs.input.SCREEN_TOUCH_END, handleEnd)

      

Este componente también incluye las animaciones "Caminar" e "Inactivo" para Doty, que se activan cuando Doty se mueve o está quieto, respectivamente.

Proyectil

projectile.ts

Este es el componente proyectil que maneja la creación de una nueva moneda dentro de la escena. Representa una bola de nieve lanzada a la seta rey.

Un proyectil una vez cargado se inicializa con una escala y un timeStart, que permiten manejar su ciclo de vida en la función tick posterior

         
// Wait for the Projectile's model to be fully loaded
world.events.addListener(eid, ecs.events.GLTF_MODEL_LOADED, initializeProjectile)

      

Un escuchador de colisiones se encarga de comprobar si el Rey Champiñón es golpeado con una bola de nieve y en ese caso su escala se reduce si su escala alcanza un determinado disparador, el juego está ganado, el jugador ha derrotado al rey champiñón

         
   // Collision event listener
   world.events.addListener(eid, ecs.physics.COLLISION_START_EVENT, (event) => {
     const {data: {other}} = event
     // Check if there is a collision with the target "Tree"
     if (target && target.toString() === other.toString()) {
       const {x, y, z} = ecs.Scale.get(world, target)
       const toX = x - 0.2
       const toY = y - 0.2
       const toZ = z - 0.2
       ecs.ScaleAnimation.set(
         world,
         target, {
           fromX: x, fromY: y, fromZ: z, toX, toY, toZ, duration: 100, loop: false,
         }
       )
       if (toX <= 1) { console.log('GAME WIN!')
         ecs.GltfModel.set(world, target, {
           animationClip: 'Death',
           loop: false,
           paused: false,
         })
         world.events.dispatch(eid, 'game-win')
       }
     }
   })

      

projectile-spawner.ts

El spawner de proyectiles es una entidad dentro de Doty, una caja alrededor de su nivel de cuerpo desde donde la bola de nieve "proyectil" son engendrados y lanzados al rey.

Cuando el botón de bola de nieve en la UI es presionado, un evento llamado "spawn-projectile" es despachado. Este evento activa el componente generador de proyectiles, que crea un nuevo proyectil en la escena.


         
     world.events.addListener(eid, 'spawn-projectile', () => {
     spawnProjectile()
   })

      

La función spawnProjectile crea y lanza una nueva entidad proyectil en la escena. Establece la posición y orientación iniciales del proyectil en función de la posición y rotación mundiales del generador. La función calcula la trayectoria del proyectil, incluida su dirección hacia delante y una componente vertical para un movimiento parabólico. También configura las propiedades físicas del proyectil, como la masa, el radio y el rebote, antes de fijar su velocidad y dirección para lanzarlo.

Bolas de nieve

snowball.ts

Este es el componente de bola de nieve que maneja la creación de una nueva bola de nieve en el suelo congelado dentro de la escena
Una vez cargada una bola de nieve obtiene una escala y una posición aleatoria establecida dentro de un radio dado.


         
world.events.addListener(eid, ecs.events.GLTF_MODEL_LOADED, () => {
     applyRandomPosition(world, eid)
     const snowballScale = ecs.Scale.cursor(world, eid)
     snowballScale.x = scale
     snowballScale.y = scale
     snowballScale.z = scale
   })

      

spread-snowballs.ts

Este componente es similar al componente spread-coins.ts con 2 funciones principales:
- La función checkPlayerProximity calcula la distancia entre el jugador y una bola de nieve, determinando si la bola de nieve puede ser recogida.
- Una función spread snowball que crea cada bola de nieve alrededor del suelo helado.



 

UI - Screens

Cada componente UI dentro de esta carpeta representa una pantalla o simplemente un elemento UI como iconos o un solo botón mostrado a lo largo de la sesión de juego. Todos ellos tienen una función setupCss que añade su CSS específico dentro de la etiqueta <style> del head <>.

game-over-screen.ts

Muestra el logo del juego
Pausa todo el audio del juego
Finalmente reproduce el audio del juego

landing-screen.ts

Esta es la pantalla que se muestra al principio al ejecutar el juego
Es donde el usuario puede seleccionar el nivel de dificultad que desea experimentar. Se crea un elemento desplegable con un oyente que cambiará el nivel de dificultad al seleccionar una opción:

         
// DIFFICULTY DROPDOWN
......
const dropdown = document.createElement('select')
......
const options = ['[ Game Difficulty ]', 'easy', 'normal', 'hard']
options.forEach((option) => {
  const optionElement = document.createElement('option')
  optionElement.value = option
  optionElement.textContent = option
  ......
  dropdown.appendChild(optionElement)
})


// Add an event listener to get the selected value when it changes
dropdown.addEventListener('change', (event) => {
 // @ts-ignore
 const {target: {value}} = event
 // Update mode
 updateMode(value)
 // Handle option title selection assigned normal as default selection in that case
 if (value === options[0]) defaultSelectedValue.selected = true
})

      

Por último, esta pantalla también se encarga de pulsar el botón "START" para activar el primer tutorial del juego:

         
// ENTER BUTTON
const button = document.createElement('button')
button.id = 'enter-btn'
button.innerText = 'ENTER'
button.addEventListener('click', () => {
  ......
  world.events.dispatch(tutorial, 'show-game-tutorial', {speed, mode})
})

      

tutorial-screen.ts

- Esta pantalla se utiliza cuando el juego se lanza por primera vez y entre cada capítulo para mostrar las nuevas instrucciones y mecánicas de juego
- Al pulsar el botón "OK", despacha eventos para iniciar los diferentes componentes encargados de ejecutar la sesión de juego:

         
// Handle on press start game
   button.addEventListener('click', () => {
     const dataAttr = dataAttribute.cursor(eid)
     world.events.dispatch(hearts, 'game-start', {})
     world.events.dispatch(timer, 'game-start', {mode: dataAttr.mode})
 ......

      

Está la creación de los elementos de interfaz de usuario, como el control deslizante y 2 botones de flecha de navegación.

win-screen.ts

- Muestra el logo del juego win
- Pausa todo el audio del juego
- Finalmente reproduce el audio del juego win


 

IU - Elementos

El resto de elementos son principalmente botones o iconos individuales que se muestran durante una sesión de juego. Todos ellos tienen 2 eventos diferentes que permiten ocultar o mostrar su contenido al usuario:

         
 world.events.addListener(eid, 'hide', () => handleHide(world, eid)) // hide
  world.events.addListener(eid, 'game-start', () => handleShow(world, eid)) // show

      

hearts.ts

Crea los 3 iconos de corazones UI en la esquina inferior izquierda de la pantalla que representan las 3 vidas del doty

help.ts

Es un botón que muestra el "?"icono que el usuario puede pulsar durante una sesión de juego para mostrar el tutorial si es necesario
Mientras se muestra el tutorial, el juego está en pausa, por lo que el usuario puede volver a leer las instrucciones con seguridad

score.ts

Este componente está diseñado específicamente para la IU de puntuación y ayuda a ocultar o mostrar la puntuación

snowball-button.ts

Se trata de un botón que permite al usuario activar un evento para lanzar una bola de nieve durante la parte final del juego:



         
world.events.dispatch(spawner, 'spawn-projectile') 

      

timer.ts

Este componente se encarga de crear la interfaz de usuario del temporizador que se ejecuta durante una sesión de juego activa
Este temporizador se puede pausar y reanudar

Otros

Los componentes restantes aquí son el gestor del juego, la seta, las partículas de nieve y el copo de nieve.

game-manager.ts

El gestor del juego es el componente central del código que conecta los eventos durante la sesión de juego, asegurando que la lógica del juego se ejecuta sin problemas y permitiendo al jugador progresar a través de los diferentes capítulos del juego.


         
   /**
    * STATES
    */
   ecs.defineState('default').initial().onEnter(() => {
     // When entering the 'default' state, add listeners for the required events
     world.events.addListener(world.events.globalId,
       'coin-collect', coinCollect)
     world.events.addListener(world.events.globalId,
       'heart-lost', heartLost)
     world.events.addListener(world.events.globalId,
       'game-win', gameWinState)
     world.events.addListener(world.events.globalId,
       'game-over', gameOverState)
     world.events.addListener(world.events.globalId,
       'game-pause', gamePauseState)
   }).onExit(() => {
     // When exiting the 'default' state, remove the listeners
     world.events.removeListener(world.events.globalId,
       'coin-collect', coinCollect)
     world.events.removeListener(world.events.globalId,
       'heart-lost', heartLost)
     world.events.removeListener(world.events.globalId,
       'game-win', gameWinState)
     world.events.removeListener(world.events.globalId,
       'game-over', gameOverState)
     world.events.removeListener(world.events.globalId,
       'game-pause', gamePauseState)
   })

      

mushroom.ts

El componente seta es la entidad enemiga del juego. Como ya se ha mencionado, existen 3 tipos diferentes de seta: la seta pequeña, la seta enfadada y la seta rey

Las principales piezas lógicas de este componente son:
- función growMushroom: que se encarga de hacer crecer la seta cuando el jugador se mueve de su punto de origen y el juego está comenzando

- función followPlayer: encargada de establecer la siguiente posición y rotación de la seta para seguir los movimientos del jugador.

         
export const followPlayer = (world, component, ecsMushroomPosition) => {
 const {eid, data: {speed}} = component
 // Smoothly move mushroom towards player using lerp
 const lerpFactor = speed
 ecsMushroomPosition.x = lerp(ecsMushroomPosition.x, vecPlayerPosition.x, lerpFactor)
 ecsMushroomPosition.y = lerp(ecsMushroomPosition.y, vecPlayerPosition.y, lerpFactor)
 ecsMushroomPosition.z = lerp(ecsMushroomPosition.z, vecPlayerPosition.z, lerpFactor)
 // Update mushroom's position
 vecMushroomPosition = vec3.xyz(ecsMushroomPosition.x, ecsMushroomPosition.y, ecsMushroomPosition.z)
 // Make the mushroom face the player, but only rotate on the Y axis
 const direction = vecPlayerPosition.minus(vecMushroomPosition)  // Define the direction from mushroom to player
 // Calculate the angle based on the X and Z axes of the player position
 const angle = Math.atan2(direction.x, direction.z)
 // Set the Y rotation of the mushroom to face the player
 const rotationQuat = quat.yRadians(angle)
 // Directly set the new quaternion in ECS
 const currentQuaternion = getQuaternion(world, eid)
 currentQuaternion.x = rotationQuat.x
 currentQuaternion.y = rotationQuat.y
 currentQuaternion.z = rotationQuat.z
 currentQuaternion.w = rotationQuat.w
}

      

checkPlayerProximity: comprueba en cada fotograma si la seta está lo suficientemente cerca del jugador, en ese caso se retira un corazón y se envía el evento heart-lost

         
const dispatchLosingHeart = (world, mushroomId, trigger = 500) => {
 if (hearts.length <= 0) return
 const currentTime = Date.now()  // Get the current time in milliseconds
 if (currentTime - lastExecutionTime >= trigger) {  // Check if 500 second has passed
   lastExecutionTime = currentTime  // Update the last execution time
   world.events.dispatch(mushroomId, 'heart-lost', {mushroomId})
 }
}


export const checkPlayerProximity = (world, component, distanceToPlayer) => {
 const {eid, schema: {distanceCatch}} = component
 // Check if the distance is less than distanceCatch
 if (distanceToPlayer <= distanceCatch) {
   dispatchLosingHeart(world, eid)
 }
}

      

La función dispatchLosingHeart tiene una lógica de estrangulamiento, esto asegura que la función no se ejecute más a menudo que el intervalo de disparo. Para evitar perder los 3 corazones en 1 frame por ejemplo

snow-particles.ts

Es el componente encargado de crear las partículas de nieve que caen en cada escenario
Utiliza el material SnowParticles que se encuentra en scripts/classes/SnowParticles


snowflake.ts

Es la entidad 3D que flota alrededor de Doty en el capítulo 2
Este componente se encarga de mostrar u ocultar el componente, ya que no es necesario durante toda la sesión de juego
Hay un 


Ayudantes

Estos ayudantes son un conjunto de archivos que proporcionan un desglose de la lógica llamada en diferentes lugares del código aquí el principal que quiero ir a través de: 


datos.ts

En este archivo encontrarás un montón de configuraciones y ajustes utilizados a lo largo del código como los valores de velocidad, los datos de los hongos (clip a reproducir, su tamaño, urls). Todos los datos de los tutoriales como los títulos, instrucciones de texto.

store.ts

En este archivo se encuentran las funciones storeTimer y getAllTimers. Se encargan de almacenar el valor del tiempo y el modo de dificultad que utilizó el jugador al llegar al final del juego


Your cool escaped html goes here.