Búsqueda de globos

Perdidos en el bosque: La Búsqueda del Globo es un atractivo juego de puzles en 3D que invita a los jugadores a navegar por un caprichoso bosque junto al curioso Chico Púrpura. Con controles intuitivos, elementos dinámicos y efectos visuales encantadores, el juego es un ejemplo perfecto de cómo Niantic Studio permite a los creadores crear juegos web 3D únicos y atractivos. Tuvimos la oportunidad de conocer su trayectoria creativa y cómo dieron vida a Lost in the Woods.

Hazlo tuyo con el proyecto de ejemplo

balloon-cover

Movimiento del jugador con la música checkmark bullet

Un proyecto de ejemplo que incluye el movimiento, las animaciones y los sonidos del personaje, con una cámara que lo sigue suavemente.

Ver ejemplo de proyecto

Behind the Build: Balloon Search

Written by eenieweenie interactive

December 26, 2024


Introduction

Esta experiencia es un juego de navegador en 3D creado con Niantic Studio (Beta) en octubre de 2024.

El personaje principal, Purple Guy, comienza en la entrada del bosque y debe navegar por el terreno para recoger globos y llegar al final.

A lo largo del camino:

  • Hay 8 globos repartidos por el mapa, seguidos por un ticker en la esquina superior derecha de la pantalla.
  • Evita las masas de agua; al pisar el agua, el juego se reinicia y pierdes todos los globos recogidos.
  • Utiliza las teclas de flecha para moverte arriba, abajo, izquierda y derecha, y pulsa la barra espaciadora para alternar entre la pantalla de bienvenida y las instrucciones.

Project Structure

Escena 3D
  • Entidades base: Incluye la cámara ortográfica y las luces ambientales/direccionales para el entorno del juego.
  • Entidades de interfaz de usuario: Contiene todos los elementos de la interfaz de usuario, incluyendo un tutorial al principio para explicar el objetivo principal del juego.

Activos
  • Audio: Música de fondo, efectos de sonido para la recogida de globos, game-over y final.
  • Modelos: Globos, tipo morado y objetos del entorno.
Guiones
  • /colisionadores/:
    • finale-collider.js: Detecta colisiones con el área final.
    • game-over-collider.js: Detecta las colisiones que conducen a un estado de game-over.
    • marcador-colisionador.js: Detecta colisiones con globos para actualizar la puntuación.
  • globo.js: Registra el componente Globo como una entidad reutilizable
    a lo largo del juego.
  • character-controller.js: Gestiona el movimiento del jugador, las animaciones y los efectos de sonido de
    en función de la entrada y el estado del juego.
  • cámara-seguidor.js: Mantiene la cámara en posición relativa al personaje.
  • game-controller.js: Sirve como eje central para gestionar los estados del juego, las transiciones y las actualizaciones de la interfaz de usuario.

Implementation

La lógica central del juego está gestionada por una serie de componentes interconectados. A continuación se muestran fragmentos de código de las principales funciones.

Gestor del juego

El game-controller.js actúa como gestor del juego, conectando diferentes eventos y gestionando las transiciones de estado.

 
Ejemplo: Gestión de estados
         
  // Define other states
  ecs.defineState('welcome')
    .initial()
    .onEvent('ShowInstructionsScreen', 'instructions')
    .onEnter(() => {
      console.log('Entering welcome state.')
      dataAttribute.set(eid, {currentScreen: 'welcome', score: 0})  // Reset score
      resetUI(world, schema, 'welcomeContainer')  // Show welcome screen
    })
  ecs.defineState('instructions')
    .onEvent('ShowMovementScreen', 'movement')
    .onEnter(() => {
      console.log('Entering instructions state.')
      dataAttribute.set(eid, {currentScreen: 'instructions'})
      resetUI(world, schema, 'instructionsContainer')  // Show instructions screen
    })
  ecs.defineState('movement')
    .onEvent('StartGame', 'inGame')
    .onEnter(() => {
      console.log('Entering movement state.')
      dataAttribute.set(eid, {currentScreen: 'movement'})
      resetUI(world, schema, 'moveInstructionsContainer')  // Show movement screen
    })
  ecs.defineState('inGame')
    .onEvent('gameOver', 'fall')
    .onEvent('finale', 'final')
    .onEnter(() => {
      console.log('Entering inGame state.')
      dataAttribute.set(eid, {currentScreen: 'inGame'})
      resetUI(world, schema, null)  // Show score UI

      if (schema.character) {
      world.events.dispatch(schema.character, 'start_moving')  // Dispatch start moving event
      }

      world.events.addListener(world.events.globalId, 'balloonCollected', balloonCollect)
      world.events.addListener(world.events.globalId, 'FinaleCollision', handleFinaleCollision)
    })
    .onExit(() => {
      world.events.dispatch(eid, 'exitPoints')  // Hide points UI
      world.events.removeListener(world.events.globalId, 'balloonCollected', balloonCollect)
      world.events.removeListener(world.events.globalId, 'FinaleCollision', handleFinaleCollision)
    })

      

Características principales

  • Transiciones: Cambios entre estados del juego como bienvenida, instrucciones, inGame y caída.
  • Manejo de Eventos: Maneja eventos como balloonCollected y FinaleCollision para actualizar puntuaciones o activar la lógica de game-over.
 
Ejemplo: Manejo de Eventos
         
  // Balloon collection handler
  const balloonCollect = () => {
  	const data = dataAttribute.acquire(eid)
  	data.score += schema.pointsPerBalloon

    if (schema.pointValue) {
    ecs.Ui.set(world, schema.pointValue, {
    text: data.score.toString(),
    })
    }

    ecs.Audio.set(world, eid, {
    url: balloonhit,
    loop: false,
    })

    console.log(`Balloon collected! Score: ${data.score}`)
    dataAttribute.commit(eid)
  }

      

Elementos dinámicos del juego

  • Colisionador Final: Detecta cuando el jugador llega a la zona final y pasa a la pantalla de puntuación final.
  • Colisionador depuntuación: Realiza un seguimiento de la recogida de globos y actualiza la puntuación.
  • Cámara de seguimiento: Actualiza dinámicamente la posición de la cámara para seguir al jugador.

 

Ejemplo: Manejo de Colisiones
         
  const handleCollision = (event) => {
    if (schemaAttribute.get(eid).character === event.data.other) {
      console.log('Finale collision detected!')
      world.events.dispatch(schemaAttribute.get(eid).gameController, 'FinaleCollision')
    }
  }

  ecs.defineState('default')
    .initial()
    .onEnter(() => {
    	world.events.addListener(eid, ecs.physics.COLLISION_START_EVENT, handleCollision)
    })
    .onExit(() => {
    	world.events.removeListener(eid, ecs.physics.COLLISION_START_EVENT, handleCollision)
    })

      
 
Ejemplo: Actualización de la puntuación
         
  const handleCollision = (event) => {
    if (schemaAttribute.get(eid).character === event.data.other) {
    	console.log(`Collision detected with character entity: ${event.data.other}`)
    	// Notify the GameController
    	world.events.dispatch(schemaAttribute.get(eid).gameController, 'balloonCollected')
    	console.log('balloonCollected event dispatched to GameController')
  	}
  }

      
 
Ejemplo: Seguimiento de cámara
         
const {Position} = ecs
const {vec3} = ecs.math

const offset = vec3.zero()
const playerPosition = vec3.zero()

const cameraFollower = ecs.registerComponent({
  name: 'cameraFollower',
  schema: {
    player: ecs.eid,  // Reference to the player entity
    camera: ecs.eid,  // Reference to the camera entity
    offsetX: ecs.f32,  // X offset for the camera position
    offsetY: ecs.f32,  // Y offset for the camera position
    offsetZ: ecs.f32,  // Z offset for the camera position
  },
  schemaDefaults: {
    offsetX: 0,  // Default X offset
    offsetY: 5,  // Default Y offset
    offsetZ: 8,  // Default Z offset
  },
  tick: (world, component) => {
    const {player, camera, offsetX, offsetY, offsetZ} = component.schema
    offset.setXyz(offsetX, offsetY, offsetZ)

    // Set the camera position to the current player position plus an offset.
    Position.set(world, camera, playerPosition.setFrom(Position.get(world, player)).setPlus(offset))
  },
})

      

INTERFAZ DE USUARIO

Pantallas dinámicas

Cada pantalla de la IU se muestra u oculta dinámicamente en función del estado del juego.

  • Pantalla de bienvenida(welcomeContainer):
    • Propósito: Muestra el botón "Empezar" y reinicia la puntuación.
  • Pantallade instrucciones (instructionsContainer):
    • Propósito: Proporciona instrucciones de juego.
  • Pantalla de instrucciones de movimiento(moveInstructionsContainer):
    • Propósito: Enseña los controles de movimiento.
  • Pantalla de fin de partida(gameOverContainer):
    • Finalidad: Muestra el mensaje de fin de partida.
  • Pantallas de puntuación final y perfecta(finalScoreContainer, perfectScoreContainer):
    • Propósito: Destaca la puntuación final del jugador.
Elementos interactivos
  • Título y valor del punto:
    • Se actualiza dinámicamente durante el juego para mostrar la puntuación del jugador.
 
Ejemplo: Restablecer IU
         
const resetUI = (world, schema, activeContainer = null) => {
  [
    'welcomeContainer',
    'instructionsContainer',
    'moveInstructionsContainer',
    'gameOverContainer',
    'finalScoreContainer',
    'perfectScoreContainer',
    ...Array.from({length: 9}, (_, i) => `scoreContainer${i}`),
  ].forEach((container) => {
    if (schema[container]) {
      ecs.Ui.set(world, schema[container], {
        opacity: container === activeContainer ? 1 : 0,  // Show active container, hide others
      })
      console.log(
        container === activeContainer
          ? `Showing ${container}`
          : `Hiding ${container}`
      )
    } else {
      console.warn(`Container ${container} is not defined in the schema.`)
    }
  })

      
 
Ejemplo: Actualización dinámica de resultados
         
ecs.Ui.set(world, schema.pointValue, { text: data.score.toString() })

      
 
Ejemplo: Resaltar contenedor de puntuación
         
ecs.Ui.set(world, schema.pointValue, { text: data.score.toString() })

      
Your cool escaped html goes here.