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.

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)