ウォール・ボップ

Wall Bopは、テキサス州オースティンのケル・ブラウンによる壁画を、インタラクティブな音楽記憶ゲームに変換するオーディオビジュアルWebAR体験です。 ローレン・シュローダーが開発したこのゲームは、プレイヤーが壁画の異なる部分をタップすることでアニメーションとサウンドを起動し、音楽のシーケンスを正しく繰り返すことを挑戦する内容となっています。 ニアンティック・スタジオとニアンティック・VPSを使用し、ローレンはゲームを壁画の正確な位置にマッピングし、現実とデジタルの世界を融合させた新たな方法で表現しました。

Wall Bop

Behind the Build

Written by Lauren Schroeder

February 27, 2025


Introduction

この体験は、Niantic Studioを使用して作成されたWebARゲームです。2024年11月

Project Structure

3D Scene
  • ゲーム: ゲームの残りのコンポーネントと、ゲームロジックを処理するメインの game.js スクリプトが含まれます。
  • VPS 位置: 体験を開始するためにスキャンされる VPS の位置です。 位置依存のゲームオブジェクトは、このロケーションの子コンポーネントすべてです。
  • ベースエンティティ: 視点ARカメラとアンビエント/方向性ライトを含みます。
  • UIエンティティ: 画面に表示されるすべてのユーザーインターフェース要素を構成し、プレイヤーに情報とフィードバックを提供します。 各チャレンジの間でチュートリアルが表示されます。
アセット
  • ゲーム内で使用されるすべての3Dモデルとオーディオファイルが含まれています。 「Models」フォルダーには、ブロブとアニメーションが含まれています。一方、「sound」フォルダーにはサウンドクリップが含まれており、ゲームが勝利状態に入った際に再生される最終曲も格納されています。
スクリプト
  • game.js: このスクリプトはゲームロジックを処理します。パズルを初期化し、正しい解答を再生し、進行状況を追跡します。 勝利とリスタートのロジックをトリガーします
  • blob.js: このスクリプトは各壁の形状で実行されます。 ブロブがタッチされた後の処理を処理し、アニメーションを更新し、特定のサウンドを再生します。

Implementation

ブロブ操作 

ここでブロブを押すことで、アニメーションを再生したり音を出したりできます。

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

      

まず、ブロブ上のクリックイベントを検出するためのリスナーを設定します。

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

      })

      

クリックされると、『submitBlob』というイベントが送信され、ゲームが入力を受け付けるようにします。

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

      

オーディオコンポーネントが再生されるように設定され、そのブロブに割り当てられた登録済みのサウンドファイルが再生されます。

         
      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)

      

 

ゲーム

game.js: ゲームデータの初期設定

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

      

ブロブの正しい順序はシーケンス配列に設定されています。これにより、ゲームはテストシーケンスを正しい順序で再生できます。 このシーケンスは、ゲームのユーザー入力部分における正確性を確認するためにも使用されます。

他のゲームオブジェクトのリンク

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

      

このスキーマを使用すると、ゲーム要素をシーケンスIDにリンクさせることができます。 また、ゲームに勝利した際に再生されるstartButtonとアニメーションオブジェクトも読み込みます。

ゲーム状態の初期化

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

      

オンボーディング状態が最初にトリガーされ、これによりゲーム内のUIのイベントリスナーが初期化されます。

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

      

ゲームが開始され、ブロブが押された際にトリガーされるイベントにリスナーが追加されます。

ゲーム状態の評価と入力結果の処理

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

      
  • 関数 handleBlobPress はゲームの状態を管理します。 現在のゲーム進行状況を追跡することで、勝利条件と敗北条件を判定します。 
  • guessIndex は、ユーザーがパズル内のどの位置にいるかを判断し、シーケンスIDの正確性を確認するために使用されます。
  • ユーザーがシーケンスの最後まで到達すると、勝利条件がトリガーされます。
  • 勝利条件がトリガーされると、ゲームコンポーネントのオーディオサンプルが再生され、Win Entityのアニメーションが開始されます

ユーザーに正しいシーケンスを再生する

         
  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)


      
  • ゲームがユーザーが聴くための正しい順序で各サウンドサンプルを再生するように、時間間隔が設定されます
  • 。 特定のブロブが参照され、トリガーされます。
  • これにより、ブロブのサウンドとアニメーションが必要なタイミングで再生されます。

初期UI

  • 現在のUIコンポーネントの制限を回避するために、JavaScriptを使用してカスタムUI画面を作成できます。 この画面はゲーム開始時に表示され、ゲームの遊び方に関する基本的な説明が表示されます。
         
   // 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.