Westliche Erinnerung

In diesem einzigartigen Spiel für zwei Geräte arbeiten die Spieler mit einem Freund oder einem Fremden zusammen, um durch 3D-gescannte Umgebungen zu navigieren, Rätsel zu lösen und die Geschichte einer eindringlichen digitalen Welt aufzudecken. Mit QR-Code-Mechaniken und Gaußschen Splats fordert Western Memory die Spieler dazu heraus, über den Bildschirm hinauszudenken und sich gleichzeitig auf eine erzählerisch reichhaltige Reise zu begeben.

Western Memory

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

door

Tap to Animate checkmark bullet

Learn how to add interactive elements that respond to player input, creating engaging and tactile gameplay.

View sample project

Behind the Build: Western Memory

Written by Taj Rauch

December 13, 2024


Introduction

Diese Erfahrung ist ein Web-AR-Spiel, das mit Niantic Studio erstellt wurde. Der Spieler wird in eine liminale, gaussisch gescannte Welt versetzt, in der er QR-Codes finden muss, die in das Erlebnis eingebettet sind. Um weiterzukommen, muss er mit einer anderen Person zusammenarbeiten, die ein Smartphone besitzt, um den Code zu scannen.

Es gibt 5 Levels, wobei das letzte Level ein QR-Portal enthält, das den Spieler zurück zum ersten Level schickt, wenn er sich entscheidet, Geheimnisse aufzudecken, die er beim ersten Mal nicht gefunden hat.

Jedes Level verfügt über unterschiedliche Interaktionsmöglichkeiten, die im Folgenden näher erläutert werden. Zu Beginn des Spiels "
" erhält der Spieler folgende Anweisungen: "

    " (
  1. Streck dich weit aus.) Der Bereich, in dem du spielst, muss weit um dich herum reichen.
  2. " " (
  3. Beweg
  4. dich
  5. !)
  6. Bestimmte Elemente können berührt oder angeklickt werden, um sie zu aktivieren.
  7. "
  8. "
  9. (Hör genau hin
  10. !) Der Ton ist entscheidend.
  11. "
  12. " (Stick und tap!) Hast du dich schon einmal festgefahren gefühlt? Versuchen Sie, die Seite zu aktualisieren.
  13. Niemand geht diesen Weg allein. Bringen Sie einen Freund mit... oder vielleicht ein Fremder.

Project Structure

Die Projektstruktur zwischen den Ebenen bleibt relativ einheitlich. Wir nutzen vor allem zwei Funktionen. Eine davon ist die Komponente "touchToAnimate ", mit der wir den Spielern die Interaktion mit Türen ermöglichen, die sich öffnen lassen. Die zweite ist eine Anpassung einer Boombox- Komponente, die es Spielern ermöglicht, mit Entitäten zu interagieren, die Audio abspielen.

Ein weiterer wichtiger Aspekt des Spiels ist die Verwendung von QR-Codes, um eine Multiplayer-Dynamik zu ermöglichen. Da ein Spieler den QR-Code nicht selbst scannen kann, benötigt er einen Partner, der das nächste Level auf sein Smartphone lädt.

3D-Szenen
-
  • -Basisentitäten: Wir portieren 3D-Assets, die in verschiedenen Levels verwendet werden. Einige davon sind reguläre .glb -Objekte, andere sind Gaussian Splat .spz -Modelle. Dazu gehören auch die Perspective AR-Kamera und Umgebungs-/Richtungslichter.
  • UI-Elemente für
  • " "
  • : Wir haben eine Benutzeroberfläche für das erste Level erstellt, die den Benutzer durch einen Startbildschirm und ein erstes Tutorial führt.
  • Assets
  • für
  • "

"
  • Die verwendeten 3D-Assets und .spz -Modelle befinden sich immer im Verzeichnis "Assets".
Skripte
  • Skripte werden aus Zeitgründen in der Regel im Stammverzeichnis abgelegt. Wenn wir mehr Zeit gehabt hätten, hätten wir jedes Skript ordentlich in einem klar definierten Verzeichnis abgelegt, aber das Hauptziel war es, ein ausgefeiltes Ergebnis zu erzielen.

Implementation

Hier werden die wichtigsten Skripte für die Kernlogik des Spiels erläutert.

touchToAnimate.ts

Eine der wichtigsten Komponenten, die wir selbst entwickelt haben, war die Komponente "touchToAnimate". Eine ausführliche Beschreibung des Skripts finden Sie in der Datei README.md im Beispielprojekt für die Komponente hier. Der Kürze halber werde ich die Anleitung hier nicht einfügen, da sie unter dem folgenden Link zu finden ist:

Dies war ein wichtiger Bestandteil, der für das taktile Gefühl des Spiels unerlässlich war. Es vermittelt dem Spieler das Gefühl, auf intuitive Weise mit einer Welt zu interagieren.

boombox.ts

Wir haben einige kleinere Anpassungen an der Komponente boombox.ts vorgenommen, die wir in einem anderen Beispielprojekt für "
Studio" gefunden haben. Die Art und Weise, wie wir dies verwenden, ermöglicht es einem Spieler, mit dem Level zu interagieren, um bei Berührung Audio zu initialisieren.

         
import * as ecs from '@8thwall/ecs'
ecs.registerComponent({
  name: 'boombox',
  schema: {
    screenRef: ecs.eid,
    // @asset
    imagePlay: ecs.string,
    // @asset
    imagePause: ecs.string,
  },
  stateMachine: ({world, eid, schemaAttribute}) => {
    // Initially set audio to off so we let player explore the arena first.
    ecs.defineState('off')
      .onEnter(() => {
        console.log('off')
        ecs.Audio.mutate(world, eid, (cursor) => {
          cursor.paused = true
      	})
        ecs.Ui.mutate(world, schemaAttribute.get(eid).screenRef, (cursor) => {
      	  cursor.image = schemaAttribute.get(eid).imagePause
        })
      })
      .onEvent(ecs.input.SCREEN_TOUCH_START, 'on', {target: schemaAttribute.get(eid).screenRef})
      .initial()
 
    // Define on state after a touch has been registered.
    ecs.defineState('on')
      .onEnter(() => {
        console.log('on')
        ecs.Audio.mutate(world, eid, (cursor) => {
          cursor.paused = false
        })
        ecs.Ui.mutate(world, schemaAttribute.get(eid).screenRef, (cursor) => {
          cursor.image = schemaAttribute.get(eid).imagePlay
        })
      })
      .onEvent(ecs.input.SCREEN_TOUCH_START, 'off', {target: schemaAttribute.get(eid).screenRef})
  }
})

      

 

UI-

Wir haben auch einige kleinere UI-Elemente für den Startbildschirm und die Tutorial-Bildschirme des Spiels entwickelt. Wir haben CSS und einige Ereignisausgaben verwendet, um von Bildschirm zu Bildschirm zu gelangen.

Um die ereignisbasierte Logik kurz zu veranschaulichen, lassen wir zunächst den Startknopf ein Ereignis auslösen, wenn er berührt wird:

         
// Add a click event listener to the start button
startButton.addEventListener('click', () => {
	console.info('Title screen clicked')
	// Uncomment this to stop audio after Title screen.
	// ecs.Audio.mutate(world, component.eid, (cursor) => {
		// cursor.paused = true
	// })
	world.events.dispatch(world.events.globalId, 'on-title-pressed') // Dispatches an event when the button is pressed

	backgroundContainer.classList.add('hidden') // Hides the background container
	buttonContainer.classList.add('hidden')
})

      

 

Dann greift der Tutorial-Bildschirm dieses Ereignis auf und blendet sich selbst ein:

         
backgroundContainer.classList.add('hidden')
instructionButton.classList.add('hidden')
world.events.addListener(world.events.globalId, 'on-title-pressed', handleIntructions)

// Add a click event listener to the start button
instructionButton.addEventListener('click', () => {
	console.info('Instructions screen clicked')
	buttonContainer.classList.add('hidden') // Hides the button container
	backgroundContainer.classList.add('hidden') // Hides the background container
})

      

 

Dies verleiht dem Spiel einen soliden Einstieg, in dem wir dem Nutzer unbewusst die Bedeutung des Erkundens und Berührens von Objekten innerhalb des Spiels vermitteln.

 

Portal

Wir recyceln das Skript "portal.ts" aus dem Beispielprojekt "Door Portal ", damit wir unsere eigenen Gaußschen Splats im Spiel verwenden können und erneut ein Erlebnis bieten können, bei dem der Spieler das Gefühl hat, die Welt zu erkunden:

         
import * as ecs from '@8thwall/ecs'
const portalHiderController = ecs.registerComponent({
  name: 'portalHiderController',
  schema: {
    camera: ecs.eid, // Reference to the camera entity
    hiderWalls: ecs.eid, // Reference to the Hider Walls entity
    exitHider: ecs.eid, // Reference to the Exit Hider entity
  },
  tick: (world, component) => {
  	const {camera, hiderWalls, exitHider} = component.schema
  	if (!camera || !hiderWalls || !exitHider) {
  		console.warn('Camera, hider, or portalHider entity not set in portalHiderController')
  		return
  	}
  	// Get the camera's position
  	const cameraPosition = ecs.Position.get(world, camera)
  	const threshold = -0.1 // Adjust the threshold as needed
  	if (cameraPosition.z < threshold) {
  		ecs.Hidden.set(world, hiderWalls, {}) // Hide the Hider Walls
  		ecs.Hidden.remove(world, exitHider) // Show the Exit Hider
  	} else {
  		ecs.Hidden.remove(world, hiderWalls) // Show the Hider Walls
  		ecs.Hidden.set(world, exitHider, {}) // Hide the Exit Hider
  	}
  }
})

      

 

Wir lassen die Welt verborgen bleiben, wenn der Spieler noch nicht an einem bestimmten Ort auf der Karte war.

 

Shader und Video-

Wir nutzen Shader auch speziell in Level 3. Dadurch können wir ein Video importieren und auf einer Ebene abspielen, um den Effekt eines Herzmonitors auf den Böden und Decken des Spiels zu erzielen:

         
add: (world, component) => {
  const {video, r, g, b, width, height, similarity, smoothness, spill} = component.schema
  if (video === '') {
    console.error('No video defined on chromakey component')
    return
  }
  const object3d = world.three.entityToObject.get(component.eid)
  const keyColor = new THREE.Color(`rgb(${r}, ${g}, ${b})`)
  const greenScreenMaterial = new ChromaKeyMaterial(video, keyColor, width, height, similarity, smoothness, spill)

  setTimeout(() => {
    object3d.material = greenScreenMaterial
  }, 0)
}

      

 

Wir erstellen ein ChromaKeyMaterial, das alle entsprechenden Parameter aus dem importierten Video festlegt, damit es auf der Ebene abgespielt werden kann.

 

Andere Skripte

Wir verwenden ein generisches Import-Skript, um unsere CSS-Seiten für unsere anfängliche Benutzeroberfläche wie folgt zu importieren:

         
import './CSS/utilities.css'
import './CSS/title.css'
import './CSS/instructions.css'
const fontLink = document.createElement('link')
fontLink.href = 'https://fonts.googleapis.com/css2?family=Rubik+Mono+One'
fontLink.rel = 'stylesheet'
document.head.appendChild(fontLink)

      
Your cool escaped html goes here.