Recherche de ballons
Lost in the Woods: Balloon Search est un jeu de puzzle en 3D captivant qui invite les joueurs à explorer une forêt fantastique en compagnie du curieux Purple Guy. Avec ses commandes intuitives, ses éléments dynamiques et ses graphismes charmants, ce jeu illustre parfaitement la manière dont Niantic Studio permet aux créateurs de développer des jeux Web en 3D uniques et captivants. Nous avons eu l'occasion de découvrir leur parcours créatif et comment ils ont donné vie à Lost in the Woods.
Personnalisez-le grâce au projet exemple

Player Movement with Music
A sample project that features character's movement, animations, and sounds, with a camera that follows the character smoothly.
View sample projectBehind the Build: Balloon Search

Written by eenieweenie interactive
December 26, 2024
Introduction
Cette expérience est un jeu en 3D pour navigateur créé à l'aide de Niantic Studio (Beta) en octobre 2024.
Le personnage principal, Purple Guy, commence à l'entrée de la forêt et doit parcourir le terrain pour collecter des ballons et atteindre la finale.
En chemin :
- Il y a 8 ballons dispersés sur la carte, suivis par un compteur dans le coin supérieur droit de l'écran.
- Évitez les plans d'eau ; si vous mettez les pieds dans l'eau, le jeu redémarre et vous perdez tous les ballons collectés.
- Utilisez les touches fléchées pour vous déplacer vers le haut, le bas, la gauche et la droite, et appuyez sur la barre d'espace pour afficher ou masquer l'écran d'accueil et les instructions.
Project Structure
Scène 3D
- Entités de base: comprend la caméra orthographique et les lumières ambiantes/directionnelles pour l'environnement du jeu.
- Entités UI: contient tous les éléments de l'interface utilisateur, y compris un didacticiel au début pour expliquer l'objectif principal du jeu.
Ressources
- Audio: musique de fond, effets sonores pour la collecte des ballons, la fin du jeu et le générique.
- Modèles: ballons, Purple Guy et objets environnementaux.
Scripts
/colliders/
:finale-collider.js
: Détecte les collisions avec la zone finale.game-over-collider.js
: Détecte les collisions menant à une fin de partie.score-collider.js
: Détecte les collisions avec les ballons afin de mettre à jour le score.
balloon.js
: Enregistre le composant Balloon en tant qu'entité réutilisable
tout au long du jeu.character-controller.js
: Gère les mouvements, les animations et les effets sonores du joueur en fonction des entrées et de l'état du jeu.camera-follower.js
: maintient la caméra positionnée par rapport au personnage.game-controller.js
: Sert de centre névralgique pour la gestion des états du jeu, des transitions et des mises à jour de l'interface utilisateur.
Implementation
La logique centrale du jeu est gérée par une série de composants interconnectés. Les principales fonctionnalités sont illustrées à l'aide des extraits de code ci-dessous.
Gestionnaire de jeu
Le fichier game-controller.js
agit comme gestionnaire du jeu, connectant différents événements et gérant les transitions d'état.
Exemple : gestion des états
// Define other states
ecs.defineState('welcome')
.initial()
.onEvent('ShowInstructionsScreen', 'instructions')
.onEnter(() => {
console.log('Entering welcome state.')
dataAttribute.set(eid, {currentScreen: 'welcome', score: 0}) // Reset score
resetUI(world, schema, 'welcomeContainer') // Show welcome screen
})
ecs.defineState('instructions')
.onEvent('ShowMovementScreen', 'movement')
.onEnter(() => {
console.log('Entering instructions state.')
dataAttribute.set(eid, {currentScreen: 'instructions'})
resetUI(world, schema, 'instructionsContainer') // Show instructions screen
})
ecs.defineState('movement')
.onEvent('StartGame', 'inGame')
.onEnter(() => {
console.log('Entering movement state.')
dataAttribute.set(eid, {currentScreen: 'movement'})
resetUI(world, schema, 'moveInstructionsContainer') // Show movement screen
})
ecs.defineState('inGame')
.onEvent('gameOver', 'fall')
.onEvent('finale', 'final')
.onEnter(() => {
console.log('Entering inGame state.')
dataAttribute.set(eid, {currentScreen: 'inGame'})
resetUI(world, schema, null) // Show score UI
if (schema.character) {
world.events.dispatch(schema.character, 'start_moving') // Dispatch start moving event
}
world.events.addListener(world.events.globalId, 'balloonCollected', balloonCollect)
world.events.addListener(world.events.globalId, 'FinaleCollision', handleFinaleCollision)
})
.onExit(() => {
world.events.dispatch(eid, 'exitPoints') // Hide points UI
world.events.removeListener(world.events.globalId, 'balloonCollected', balloonCollect)
world.events.removeListener(world.events.globalId, 'FinaleCollision', handleFinaleCollision)
})
Principales fonctionnalités
- Transitions: changements entre les états du jeu, tels que «
accueil »
, «instructions »
, «en cours
» et «chute
». - Gestion des événements: gère les événements tels que
balloonCollected
etFinaleCollision
pour mettre à jour les scores ou déclencher la logique de fin de partie.
Exemple : gestion des événements
// Balloon collection handler
const balloonCollect = () => {
const data = dataAttribute.acquire(eid)
data.score += schema.pointsPerBalloon
if (schema.pointValue) {
ecs.Ui.set(world, schema.pointValue, {
text: data.score.toString(),
})
}
ecs.Audio.set(world, eid, {
url: balloonhit,
loop: false,
})
console.log(`Balloon collected! Score: ${data.score}`)
dataAttribute.commit(eid)
}
Éléments de jeu dynamiques
- Collision finale: détecte lorsque le joueur atteint la zone finale et passe à l'écran du score final.
- Score Collider: suit la collecte des ballons et met à jour le score.
- Follow Camera: met à jour dynamiquement la position de la caméra pour suivre le joueur.
Exemple : gestion des collisions
const handleCollision = (event) => {
if (schemaAttribute.get(eid).character === event.data.other) {
console.log('Finale collision detected!')
world.events.dispatch(schemaAttribute.get(eid).gameController, 'FinaleCollision')
}
}
ecs.defineState('default')
.initial()
.onEnter(() => {
world.events.addListener(eid, ecs.physics.COLLISION_START_EVENT, handleCollision)
})
.onExit(() => {
world.events.removeListener(eid, ecs.physics.COLLISION_START_EVENT, handleCollision)
})
Exemple : mise à jour du score
const handleCollision = (event) => {
if (schemaAttribute.get(eid).character === event.data.other) {
console.log(`Collision detected with character entity: ${event.data.other}`)
// Notify the GameController
world.events.dispatch(schemaAttribute.get(eid).gameController, 'balloonCollected')
console.log('balloonCollected event dispatched to GameController')
}
}
Exemple : Suivi de caméra
const {Position} = ecs
const {vec3} = ecs.math
const offset = vec3.zero()
const playerPosition = vec3.zero()
const cameraFollower = ecs.registerComponent({
name: 'cameraFollower',
schema: {
player: ecs.eid, // Reference to the player entity
camera: ecs.eid, // Reference to the camera entity
offsetX: ecs.f32, // X offset for the camera position
offsetY: ecs.f32, // Y offset for the camera position
offsetZ: ecs.f32, // Z offset for the camera position
},
schemaDefaults: {
offsetX: 0, // Default X offset
offsetY: 5, // Default Y offset
offsetZ: 8, // Default Z offset
},
tick: (world, component) => {
const {player, camera, offsetX, offsetY, offsetZ} = component.schema
offset.setXyz(offsetX, offsetY, offsetZ)
// Set the camera position to the current player position plus an offset.
Position.set(world, camera, playerPosition.setFrom(Position.get(world, player)).setPlus(offset))
},
})
Interface utilisateur
Écrans dynamiques
Chaque écran de l'interface utilisateur est affiché ou masqué de manière dynamique en fonction de l'état du jeu.
- Écran d'accueil (
welcomeContainer
) :- Objectif : affiche le bouton « Démarrer » et réinitialise le score.
- Écran d'instructions (
instructionsContainer
) :- Objectif : fournit des instructions sur le gameplay.
- Écran d'instructions de déplacement (
moveInstructionsContainer
) :- Objectif : enseigne les commandes de déplacement.
- Écran Game Over (
gameOverContainer
) :- Objectif : affiche le message « Game Over ».
- Écrans de score final et parfait (
finalScoreContainer
,perfectScoreContainer
) :- Objectif : met en évidence le score final du joueur.
Éléments interactifs
- Titre et valeur du point:
- Se met à jour dynamiquement pendant le jeu pour afficher le score du joueur.
Exemple : Réinitialiser l'interface utilisateur
const resetUI = (world, schema, activeContainer = null) => {
[
'welcomeContainer',
'instructionsContainer',
'moveInstructionsContainer',
'gameOverContainer',
'finalScoreContainer',
'perfectScoreContainer',
...Array.from({length: 9}, (_, i) => `scoreContainer${i}`),
].forEach((container) => {
if (schema[container]) {
ecs.Ui.set(world, schema[container], {
opacity: container === activeContainer ? 1 : 0, // Show active container, hide others
})
console.log(
container === activeContainer
? `Showing ${container}`
: `Hiding ${container}`
)
} else {
console.warn(`Container ${container} is not defined in the schema.`)
}
})
Exemple : Mise à jour dynamique du score
ecs.Ui.set(world, schema.pointValue, { text: data.score.toString() })
Exemple : mettre en évidence le conteneur de scores
ecs.Ui.set(world, schema.pointValue, { text: data.score.toString() })