Bop de pared

Wall Bop es una experiencia audiovisual WebAR que transforma un mural de Kel Brown en Austin (Texas) en un juego de memoria musical interactivo. Desarrollado por Lauren Schroeder, el juego invita a los jugadores a tocar distintas secciones del mural, lo que desencadena animaciones y sonidos, y les reta a repetir correctamente una secuencia musical. Con Niantic Studio y Niantic VPS, Lauren trasladó el juego con precisión a la ubicación del mural, mezclando los mundos real y digital de una forma nueva y atractiva.

Wall Bop

Behind the Build

Written by Lauren Schroeder

February 27, 2025


Introduction

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

Project Structure

Escena 3D
  • Juego: Este contiene el resto de los componentes del juego, así como el script principal game.js que maneja la lógica del juego.
  • Ubicación VPS: Esta es la ubicación VPS que será escaneada para iniciar la experiencia. Los objetos de juego dependientes de la posición son todos componentes hijos de esta localización.
  • Entidades Base: Incluye la cámara de Perspectiva AR y las Luces de Ambiente/Direccionales.
  • Entidades UI: Comprende todos los elementos de 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 y archivos de audio utilizados a lo largo del juego. La carpeta Models contiene blobs y animaciones, mientras que la carpeta sound contiene clips de sonido, así como la canción final que se reproduce cuando el juego entra en el estado de victoria.
Scripts
  • game.js: Este script se encarga de la lógica del juego: inicializa el puzzle, reproduce la solución correcta y registra el progreso. Activa la lógica de ganar y reiniciar
  • blob.js: Este script se utiliza en cada forma de muro. Se encarga de lo que ocurre después de tocar la mancha, actualizando la animación y reproduciendo un sonido específico.

Implementation

Interacción con los blobs 

Aquí es donde se pueden pulsar los blobs para que se animen y emitan sonidos.

         
   world.events.addListener(eid, ecs.input.SCREEN_TOUCH_START, click)

      

En primer lugar, configuramos un receptor para detectar eventos de clic en la nota.

         
    const click = () => {
      world.events.dispatch(world.events.globalId, 'submitBlob', {
        blob: schemaAttribute.get(eid).blob,

      })

      

Al hacer clic, se envía un evento llamado "submitBlob" para que el juego registre la entrada.

         
   ecs.Audio.mutate(world, eid, (cursor) => {
        // Ensure the component's audio sample is playing
        cursor.paused = false
      })

      

A continuación, el componente Audio se configura para que reproduzca el archivo de sonido registrado específico de esa nota.

         
      ecs.ScaleAnimation.set(world, eid, {
        autoFrom: true,
        toX: originalScale.x * scaleAmount,
        toY: originalScale.y * scaleAmount,
        toZ: originalScale.z * scaleAmount,
        loop: false,
        duration: 200,
        easeOut: true,
        easingFunction: 'Elastic',
      })

      
         
      // Set a callback to scale back to original size
      setTimeout(() => {
        ecs.ScaleAnimation.set(world, eid, {
          autoFrom: true,
          toX: originalScale.x,
          toY: originalScale.y,
          toZ: originalScale.z,
          loop: false,
          duration: 200,
          easeOut: true,
          easingFunction: 'Elastic',
        })
      }, 200)

      

 

Juego

game.js: configuración de los datos del juego

         
const sequence = ['w1', 'w2', 'w3', 'b1', 'b2', 'b3', 'b2', 'b3', 'r1', 'r2', 'r3']
const messages = ['FIRST ONE!', 'KEEP GOING..', 'IS THAT ALL YOU GOT?', 'DOING GREAT',
  'YOU GOT THIS', 'OVER HALFWAY', 'WHAT NOW?', 'DOING GREAT!', 'ALMOST THERE', 'LAST ONE...']

      

El orden correcto de los blobs se establece en la matriz de secuencias, para que el juego pueda reproducir la secuencia de prueba en el orden correcto. La secuencia también se utiliza para comprobar la precisión durante la parte del juego en la que el usuario introduce datos.

Vinculación de otros objetos del juego

         
  schema: {
    w1: ecs.eid,
    w2: ecs.eid,
    w3: ecs.eid,
    b1: ecs.eid,
    b2: ecs.eid,
    b3: ecs.eid,
    r1: ecs.eid,
    r2: ecs.eid,
    r3: ecs.eid,
    startButton: ecs.eid,
    winEntity: ecs.eid,
  },

      

El esquema permite vincular los elementos del juego a los identificadores de secuencia. También saca el startButton y el objeto animado que debería sonar cuando ganes la partida.

Inicialización del estado del juego

         
 ecs.defineState('onboarding')
      .onEnter(() => {
        const onxrloaded = () => {
          world.events.addListener(startButton, ecs.input.SCREEN_TOUCH_START, handleStart)
        }
        window.XR8 ? onxrloaded() : window.addEventListener('xrloaded', onxrloaded)
      })
      .onExit(() => {
        world.events.removeListener(startButton, ecs.input.SCREEN_TOUCH_START, handleStart)
      })
      .initial()
      .onTrigger(startGame, 'gameStarted')

      

El estado onboarding se activa en primer lugar, de modo que el receptor de eventos para la interfaz de usuario en el juego puede ser inicializado.

         
    ecs.defineState('gameStarted')
      .onEnter(() => {
        ecs.Ui.mutate(world, startButton, (cursor) => {
          cursor.text = 'LISTEN CLOSELY'
        })
  if (!restarted) {
          world.events.addListener(world.events.globalId, 'submitBlob', (e) => {
            handleBlobPress(e)
          })

      

A continuación, se inicia el juego y se añade un receptor a los eventos que se activan cuando se pulsa una nota.

Evaluar el estado del juego y los resultados de las entradas

         
          function handleBlobPress(e) {
          if (sequence.length > guessIndex + 1) {
            if (e.data.blob == sequence[guessIndex]) {
              ecs.Ui.mutate(world, startButton, (cursor) => {
                cursor.text = messages[guessIndex]
              })
              guessIndex += 1
            } else {
              resetGame(world)
              ecs.Ui.mutate(world, startButton, (cursor) => {
                cursor.text = 'PRESS TO PLAY AGAIN'
              })
              world.events.addListener(startButton, ecs.input.SCREEN_TOUCH_START, handleStart)
            }
          } else {
            ecs.Ui.mutate(world, startButton, (cursor) => {
              cursor.text = 'YOU WIN!'
              resetGame(world)
              ecs.GltfModel.set(world, winEntity, {
                paused: false,
              })
              ecs.Audio.mutate(world, eid, (Audiocursor) => {
                Audiocursor.paused = false
              })
            })
          }
        }

      
  • La función handleBlobPress maneja el estado del juego. Comprueba las condiciones de victoria y derrota realizando un seguimiento del progreso actual del juego. 
  • guessIndex se utiliza para ver hasta qué punto del puzzle se encuentra el usuario y comprobar la precisión del ID de secuencia
  • Una vez que el usuario llega al final de la secuencia, se activa la condición de victoria
  • Para la condición de victoria, se activa la muestra de audio de los componentes del juego y se inicia la animación de la entidad ganadora
  • .

Reproducir la secuencia correcta al usuario

         
  const intervalId = setInterval(() => {
          if (playIndex < sequence.length) {
            try {
              ecs.Audio.mutate(world, schemaAttribute.get(eid)[sequence[playIndex]], (cursor) => {
                ecs.ScaleAnimation.set(world, schemaAttribute.get(eid)[sequence[playIndex]], {
                  autoFrom: true,
                  toX: 1.12,
                  toY: 1.12,
                  toZ: 1.12,
                  loop: false,
                  duration: 1000,
                  easeOut: true,
                  easingFunction: 'Elastic',
                })
                setTimeout(() => {
                  ecs.ScaleAnimation.set(world, schemaAttribute.get(eid)[sequence[playIndex]], {
                    autoFrom: true,
                    toX: 1,
                    toY: 1,
                    toZ: 1,
                    loop: false,
                    duration: 200,
                    easeOut: true,
                    easingFunction: 'Elastic',
                  })
                }, 1000)
                cursor.paused = false
              })
              playIndex++
            } catch (error) {
              console.error('Failed to play', error)
            }
          } else {
            clearInterval(intervalId)
            ecs.Ui.mutate(world, startButton, (cursor) => {
              cursor.text = 'NOW YOU TRY'
            })
          }
        }, 1000)


      
  • Se establece un intervalo de tiempo para que el juego reproduzca cada una de las muestras de sonido en el orden correcto para que el usuario las escuche
  • Se hace referencia a la nota específica y se activa. Esto hace que el sonido y la animación de la mancha se reproduzcan cuando sea necesario.

Interfaz de usuario inicial

  • Para evitar las limitaciones del actual Componente UI, puede utilizar Javascript para crear una pantalla UI personalizada. Esta pantalla aparece cuando se inicia el juego y permite obtener instrucciones básicas sobre cómo jugar.
         
   // Start button
  const startButton = document.createElement('button')
  startButton.textContent = 'LET\'S GO'
  startButton.style.marginTop = '20px'
  startButton.style.padding = '10px 20px'
  startButton.style.fontSize = '16px'
  startButton.style.cursor = 'pointer'
  startButton.style.backgroundColor = 'white'
  startButton.style.color = 'navy'
  startButton.style.border = 'none'


  startButton.style.borderRadius = '5px'


  // Button click event
  startButton.addEventListener('click', (event) => {
    event.stopPropagation()
    document.body.removeChild(instructionsBox)
  })
  instructionsBox.appendChild(startButton)
  document.body.appendChild(instructionsBox)


      
Your cool escaped html goes here.