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

balloon-cover

Player Movement with Music checkmark bullet

A sample project that features character's movement, animations, and sounds, with a camera that follows the character smoothly.

View sample project

Behind 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 et FinaleCollision 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() })

      
Your cool escaped html goes here.