Wand-Bop

Wall Bop ist ein audiovisuelles WebAR-Erlebnis, das ein Wandbild von Kel Brown in Austin, Texas, in ein interaktives musikalisches Gedächtnisspiel verwandelt. Das von Lauren Schroeder entwickelte Spiel lädt die Spieler dazu ein, auf verschiedene Bereiche des Wandgemäldes zu tippen, wodurch Animationen und Geräusche ausgelöst werden und sie aufgefordert werden, eine musikalische Sequenz korrekt zu wiederholen. Mit Niantic Studio und Niantic VPS hat Lauren das Spiel präzise auf den Standort des Wandgemäldes übertragen und so die reale und die digitale Welt auf faszinierende Weise miteinander verschmolzen.

Wall Bop

Behind the Build

Written by Lauren Schroeder

February 27, 2025


Introduction

Diese Erfahrung ist ein WebAR-Spiel, das mit Niantic Studio im November 2024 erstellt wurde.

Project Structure

3D-Szenen-
  • -Spiel: Hier befinden sich die restlichen Spielkomponenten sowie das Hauptskript "game.js", das die Spielelogik verarbeitet.
  • VPS-Speicherort: Dies ist der VPS-Speicherort, der zum Starten der Erfahrung gescannt wird. Die positionsabhängigen Spielobjekte sind alle untergeordnete Komponenten dieses Standorts.
  • Basisentitäten: Umfasst die perspektivische AR-Kamera und Umgebungs-/Richtungslichter.
  • UI-Entitäten: Umfasst alle auf dem Bildschirm angezeigten Elemente der Benutzeroberfläche, die dem Spieler Informationen und Feedback liefern. Zwischen jeder Herausforderung wird ein Tutorial angezeigt.
Assets
  • Enthält alle im Spiel verwendeten 3D-Modelle und Audiodateien. Der Ordner "Models" enthält Blobs und Animationen, während der Ordner "Sound" Soundclips sowie den endgültigen Song enthält, der abgespielt wird, wenn das Spiel gewonnen wurde.
    Skripte
    für "
"
  • game.js: Dieses Skript ist für die Spielelogik zuständig – es initialisiert das Puzzle, spielt die richtige Lösung ab und verfolgt den Fortschritt. Es löst die Gewinn- und Neustartlogik aus
  • blob.js: Dieses Skript wird für jede Wandform verwendet. Es verarbeitet, was passiert, nachdem der Blob berührt wurde, indem es die Animation aktualisiert und einen bestimmten Sound abspielt.

Implementation

Blob-Interaktions 

Hier können die Blobs gedrückt werden, um sie zu animieren und Geräusche zu erzeugen.

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

      

Zunächst richten wir einen Listener ein, um Klickereignisse auf dem Blob zu erkennen.

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

      })

      

Bei einem Klick wird ein Ereignis namens "submitBlob" ausgelöst, damit das Spiel die Eingabe registriert.

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

      

Die Audiokomponente wird dann so eingestellt, dass die für diesen Blob registrierte Audiodatei abgespielt wird.

         
      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)

      

 

Spiel

game.js: Einrichten der Spieldaten

         
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...']

      

Die richtige Reihenfolge der Blobs wird im Sequenzarray festgelegt, damit das Spiel die Testsequenz in der richtigen Reihenfolge abspielen kann. Die Sequenz wird auch verwendet, um die Genauigkeit während der Benutzereingabe im Spiel zu überprüfen.

Andere Spielobjekte verknüpfen

         
  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,
  },

      

Mit dem Schema können Sie Spielelemente mit den Sequenz-IDs verknüpfen. Es ruft auch den startButton und das animierte Objekt auf, das abgespielt werden soll, wenn Sie das Spiel gewinnen.

Initialisierung des Spielzustands

         
 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')

      

Der Onboarding-Status wird zuerst ausgelöst, damit der Ereignis-Listener für die Benutzeroberfläche im Spiel initialisiert werden kann.

         
    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)
          })

      

Das Spiel wird dann gestartet und ein Listener wird zu den Ereignissen hinzugefügt, die ausgelöst werden, wenn ein Blob gedrückt wird.

Bewertung des Spielstatus und der Eingabenergebnisse

         
          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
              })
            })
          }
        }

      
  • Die Funktion handleBlobPress verwaltet den Spielstatus. Es überprüft die Gewinn- und Verlustbedingungen, indem es den aktuellen Spielfortschritt verfolgt. 
  • guessIndex wird verwendet, um zu sehen, wie weit der Benutzer im Puzzle ist, und um die Richtigkeit der Sequenz-ID zu überprüfen
  • .
  • Sobald der Benutzer das Ende der Sequenz erreicht hat, wird die Gewinnbedingung ausgelöst
  • .
  • Für die Gewinnbedingung wird das Audio-Sample der Spielkomponente ausgelöst und die Animation der Gewinn-Entität gestartet.

Die richtige Reihenfolge für den Benutzer abspielen

         
  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)


      
  • Es wird ein Zeitintervall festgelegt, damit das Spiel die einzelnen Sound-Samples in der richtigen Reihenfolge abspielt, damit der Benutzer sie hören kann
  • .
  • Der spezifische Blob wird referenziert und ausgelöst. Dadurch werden der Ton und die Animation des Blobs bei Bedarf abgespielt.

Anfängliche Benutzeroberfläche

  • Um Einschränkungen der aktuellen UI-Komponente zu umgehen, können Sie mit JavaScript einen benutzerdefinierten UI-Bildschirm erstellen. Dieser Bildschirm erscheint zu Beginn des Spiels und enthält grundlegende Anweisungen zum Spielablauf.
         
   // 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.