Wall Bop
Wall Bop est une expérience audiovisuelle WebAR qui transforme une fresque murale réalisée par Kel Brown à Austin, au Texas, en un jeu de mémoire musical interactif. Développé par Lauren Schroeder, le jeu invite les joueurs à toucher différentes parties de la fresque, ce qui déclenche des animations et des sons, et les met au défi de reproduire correctement une séquence musicale. À l'aide de Niantic Studio et Niantic VPS, Lauren a cartographié le jeu avec précision à l'emplacement de la fresque, fusionnant ainsi les mondes réel et numérique d'une manière inédite et captivante.

Behind the Build

Written by Lauren Schroeder
February 27, 2025
Introduction
Cette expérience est un jeu WebAR créé à l'aide de Niantic Studio, novembre 2024.
Project Structure
-
s
- : contient le reste des composants du jeu, ainsi que le script game.js principal qui gère la logique du jeu. Emplacement du VPS d'
- : il s'agit de l'emplacement du VPS qui sera analysé pour démarrer l'expérience. Les objets de jeu dépendants de la position sont tous des composants enfants de cet emplacement.
- Entités de base: comprend la caméra AR Perspective et les lumières ambiantes/directionnelles.
- Entités UI: comprend tous les éléments de l'interface utilisateur affichés à l'écran, fournissant des informations et des commentaires au joueur. Un tutoriel est affiché entre chaque défi.
de scènes 3D
Ressources
- Comprend tous les modèles 3D et fichiers audio utilisés dans le jeu. Le dossier Models contient des blobs et des animations, tandis que le dossier sound contient des clips audio, ainsi que la chanson finale jouée lorsque le jeu passe à l'état « victoire ».
Scripts
d'
- game.js: ce script gère la logique du jeu : il initialise le puzzle, joue la solution correcte et suit la progression. Il déclenche la logique de gain et de redémarrage
- blob.js: Ce script est utilisé sur chaque forme de mur. Il gère ce qui se passe après que le blob a été touché, en mettant à jour l'animation et en jouant un son spécifique.
Implementation
de l'interaction avec les blobs
C'est ici que les blobs peuvent être pressés afin d'être animés et de produire des sons.
world.events.addListener(eid, ecs.input.SCREEN_TOUCH_START, click)
Tout d'abord, nous configurons un écouteur pour détecter les événements de clic sur le blob.
const click = () => {
world.events.dispatch(world.events.globalId, 'submitBlob', {
blob: schemaAttribute.get(eid).blob,
})
Lorsque l'utilisateur clique, un événement appelé « submitBlob » est envoyé afin que le jeu enregistre la saisie.
ecs.Audio.mutate(world, eid, (cursor) => {
// Ensure the component's audio sample is playing
cursor.paused = false
})
Le composant Audio est ensuite configuré pour lire le fichier audio enregistré spécifique à ce blob.
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)
Jeu
game.js : configuration des données du jeu
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...']
L'ordre correct des blobs est défini dans le tableau de séquence, afin que le jeu puisse lire la séquence de test dans le bon ordre. La séquence est également utilisée pour vérifier l'exactitude des données saisies par l'utilisateur pendant le jeu.
Lier d'autres objets de jeu
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,
},
Le schéma vous permet de relier les éléments du jeu aux identifiants de séquence. Il récupère également le bouton startButton et l'objet animé qui doit être lu lorsque vous gagnez la partie.
Initialisation de l'état du jeu
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')
L'état d'intégration est déclenché en premier, afin que l'écouteur d'événements pour l'interface utilisateur dans le jeu puisse être initialisé.
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)
})
Le jeu démarre alors, et un écouteur est ajouté aux événements qui se déclenchent lorsqu'un blob est pressé.
Évaluation de l'état du jeu et des résultats des entrées
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 fonction handleBlobPress gère l'état du jeu. Il vérifie les conditions de victoire et de défaite en suivant la progression actuelle du jeu.
- guessIndex est utilisé pour voir où en est l'utilisateur dans le puzzle et vérifier l'exactitude de l'ID de séquence .
- Une fois que l'utilisateur arrive à la fin de la séquence, la condition de victoire est déclenchée .
- Pour la condition de victoire, l'échantillon audio des composants du jeu est déclenché et l'animation de l'entité Win est lancée .
Jouer la séquence correcte à l'utilisateur
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)
- Un intervalle de temps est défini afin que le jeu puisse lire chacun des échantillons sonores dans le bon ordre pour que l'utilisateur puisse les écouter .
- Le blob spécifique est référencé et déclenché. Cela permet de lire le son et l'animation du blob lorsque cela est nécessaire.
Interface utilisateur initiale
- Afin d'éviter les limitations de l'interface utilisateur actuelle, vous pouvez utiliser Javascript pour créer un écran personnalisé. Cet écran apparaît au démarrage du jeu et fournit des instructions de base sur la manière de jouer.
// 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)