Ballonsuche

Lost in the Woods: Balloon Search ist ein fesselndes 3D-Puzzlespiel, in dem die Spieler gemeinsam mit dem neugierigen Purple Guy durch einen märchenhaften Wald navigieren müssen. Mit intuitiven Steuerelementen, dynamischen Elementen und einer bezaubernden Grafik ist das Spiel ein perfektes Beispiel dafür, wie Niantic Studio Entwickler dabei unterstützt, einzigartige und fesselnde 3D-Webspiele zu entwickeln. Wir hatten die Gelegenheit, mehr über ihren kreativen Werdegang zu erfahren und darüber, wie sie Lost in the Woods zum Leben erweckt haben.

Gestalten Sie es mit dem Beispielprojekt ganz nach Ihren Wünschen.

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

Diese Erfahrung ist ein 3D-Browsergame, das im Oktober 2024 mit Niantic Studio (Beta) erstellt wurde.

Der Hauptcharakter, Purple Guy, beginnt am Eingang des Waldes und muss sich durch das Gelände navigieren, um Ballons zu sammeln und das Finale zu erreichen.

Unterwegs:

  • Auf der Karte sind 8 Ballons verstreut, die durch einen Ticker in der oberen rechten Ecke des Bildschirms verfolgt werden.
  • Vermeiden Sie Gewässer, denn wenn Sie ins Wasser treten, wird das Spiel zurückgesetzt und Sie verlieren alle gesammelten Ballons.
  • Verwenden Sie die Pfeiltasten, um sich nach oben, unten, links und rechts zu bewegen, und drücken Sie die Leertaste, um den Begrüßungsbildschirm und die Anweisungen anzuzeigen.

Project Structure

3D-Szene
  • Basisentitäten: Enthält die orthogonale Kamera und Umgebungs-/Richtungslichter für die Spielumgebung.
  • UI-Entitäten: Enthält alle Elemente der Benutzeroberfläche, einschließlich eines Tutorials zu Beginn, in dem das Hauptziel des Spiels erklärt wird.

Assets
  • Audio: Hintergrundmusik, Soundeffekte für das Sammeln von Ballons, Game Over und Finale.
  • Modelle: Ballons, Purple Guy und Umgebungsobjekte.
Skripte
  • /colliders/:
    • finale-collider.js: Erkennt Kollisionen mit dem Finalbereich.
    • game-over-collider.js: Erkennt Kollisionen, die zu einem Spielende führen.
    • score-collider.js: Erkennt Kollisionen mit Ballons, um den Punktestand zu aktualisieren.
  • balloon.js: Registriert die Balloon-Komponente als wiederverwendbare Entität "
    " für das gesamte Spiel.
  • character-controller.js: Verwaltet die Bewegungen und Animationen der Spieler sowie die Soundeffekte von "
    " basierend auf den Eingaben und dem Spielstatus.
  • camera-follower.js: Hält die Kamera relativ zum Charakter positioniert.
  • game-controller.js: Dient als zentrale Schaltstelle für die Verwaltung von Spielzuständen, Übergängen und UI-Aktualisierungen.

Implementation

Die Kernlogik des Spiels wird durch eine Reihe miteinander verbundener Komponenten verwaltet. Die wichtigsten Funktionen werden anhand der folgenden Codeausschnitte demonstriert.

Spielmanager

Die Datei "game-controller.js" fungiert als Spielmanager, verbindet verschiedene Ereignisse und verwaltet Zustandsübergänge.

 
Beispiel: Zustandsverwaltung
         
  // 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)
    })

      

Wichtigste Funktionen

  • Übergänge: Wechselt zwischen Spielzuständen wie " Willkommen", "Anweisungen", "Im Spiel" und "Sturz".
  • Ereignisbehandlung: Verwaltet Ereignisse wie " balloonCollected" und " FinaleCollision ", um Punktestände zu aktualisieren oder die Spielende-Logik auszulösen.
 
Beispiel: Ereignisbehandlung
         
  // 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)
  }

      

Dynamische Spielelemente

  • Finale Collider: Erkennt, wenn der Spieler den Finalbereich erreicht, und wechselt zum Endbildschirm mit der Punktzahl.
  • Score Collider: Verfolgt das Sammeln von Ballons und aktualisiert die Punktzahl.
  • Follow Camera: Aktualisiert dynamisch die Position der Kamera, um dem Spieler zu folgen.

 

Beispiel: Kollisionsbehandlung
         
  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)
    })

      
 
Beispiel: Punktestand-Aktualisierung
         
  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')
  	}
  }

      
 
Beispiel: Kameraführung
         
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))
  },
})

      

UI

Dynamische Bildschirme

Jeder UI-Bildschirm wird je nach Spielstatus dynamisch ein- oder ausgeblendet.

  • Begrüßungsbildschirm (welcomeContainer):
    • Zweck: Zeigt die Schaltfläche "Start" an und setzt den Punktestand zurück.
  • Anweisungsbildschirm (instructionsContainer):
    • Zweck: Enthält Anweisungen zum Spielablauf.
  • Bildschirm mit Bewegungsanweisungen (moveInstructionsContainer):
    • Zweck: Erklärt die Bewegungssteuerung.
  • Bildschirm "Spiel vorbei " (gameOverContainer):
    • Zweck: Zeigt die Game-Over-Meldung an.
  • Bildschirme für Endpunkt und perfekte Punktzahl (finalScoreContainer, perfectScoreContainer):
    • Zweck: Hebt die Endpunktzahl des Spielers hervor.
Interaktive Elemente
  • Punktetitel und Wert:
    • Wird während des Spiels dynamisch aktualisiert, um die Punktzahl des Spielers anzuzeigen.
 
Beispiel: Benutzeroberfläche zurücksetzen
         
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.`)
    }
  })

      
 
Beispiel: Dynamische Punktestandsaktualisierung
         
ecs.Ui.set(world, schema.pointValue, { text: data.score.toString() })

      
 
Beispiel: Score-Container hervorheben
         
ecs.Ui.set(world, schema.pointValue, { text: data.score.toString() })

      
Your cool escaped html goes here.