Mémoire occidentale

Dans ce jeu unique pour deux appareils, les joueurs collaborent avec un ami ou un inconnu pour explorer des environnements numérisés en 3D, résoudre des énigmes et découvrir l'histoire d'un monde numérique envoûtant. Grâce à l'utilisation de codes QR et de splats gaussiens, Western Memory met les joueurs au défi de sortir des sentiers battus tout en se plongeant dans un voyage riche en rebondissements.

Western Memory

Personnalisez-le grâce au projet exemple

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

Cette expérience est un jeu en réalité augmentée sur le Web créé à l'aide de Niantic Studio. Le joueur est plongé dans un monde liminal, balayé par des éclats gaussiens, où, après avoir trouvé des codes QR intégrés à l'expérience, il doit collaborer avec une autre personne disposant d'un téléphone pour scanner le code et avancer.

Il y a 5 niveaux, le dernier niveau comportant un portail QR qui renvoie le joueur au niveau 1 s'il choisit de découvrir des secrets qu'il n'a pas trouvés lors de sa première tentative.

Chaque niveau présente différentes fonctionnalités d'interaction qui seront explorées ci-dessous. Au début du jeu
, le joueur reçoit les instructions suivantes : «

    »
  1. (Où que vous jouiez, étirez-vous largement autour de vous). «
  2. »
  3. (Certains éléments peuvent être touchés ou tapés pour les réveiller).
  4. «
  5. » (Le son est essentiel).
  6. «
  7. » (Vous vous sentez bloqué ? Essayez de vous déplacer, de vous déplacer dans le jeu, de vous déplacer dans la pièce, de vous déplacer dans le monde, de vous déplacer dans le monde réel). Essayez de rafraîchir la page.
  8. Personne ne parcourt ce chemin seul. Amenez un ami... ou peut-être un inconnu.

Project Structure

La structure du projet entre les différents niveaux reste relativement cohérente. Nous tirons parti de deux fonctionnalités principales. L'un est le composant touchToAnimate, que nous utilisons pour permettre aux joueurs d'interagir avec l'ouverture des portes. La seconde est une personnalisation d'un composant boombox, qui permet aux joueurs d'interagir avec des entités qui diffusent du son.

L'un des autres aspects majeurs du jeu est l'utilisation de codes QR pour permettre une dynamique multijoueur. Comme les joueurs ne peuvent pas scanner le code QR eux-mêmes, ils ont besoin d'un partenaire pour charger le niveau suivant sur leur téléphone.

  • de scène 3D
    Entités de base: nous importons les ressources 3D utilisées dans les différents niveaux. Certains sont des objets .glb classiques, d'autres sont des modèles Gaussian Splat .spz. Cela inclut également la caméra AR Perspective et les lumières ambiantes/directionnelles.
  • Entités de l'interface utilisateur
  • d'
  • : nous avons créé une interface utilisateur pour le premier niveau qui guide l'utilisateur à travers un écran de démarrage et un tutoriel initial.
  • Ressources d'

  • Les ressources 3D et les modèles .spz utilisés se trouvent toujours dans le répertoire Assets.
Scripts
  • Les scripts sont généralement placés à la racine en raison de contraintes de temps. Si nous avions eu plus de temps, nous aurions soigneusement placé chaque script dans un répertoire bien défini, mais l'objectif principal était d'offrir une expérience aboutie.

Implementation

Ceci présente les principaux scripts de la logique centrale du jeu.

touchToAnimate.ts

L'un des principaux composants que nous avons créés nous-mêmes est le composant touchToAnimate. Une description très détaillée du script est disponible dans le fichier README.md du projet exemple pour le composant ici. Par souci de concision, je ne vais pas copier ici la procédure pas à pas, que vous trouverez dans le lien ci-dessous.

Il s'agissait d'un élément essentiel pour le rendu tactile du jeu. Cela donne au joueur l'impression d'interagir avec un monde de manière intuitive.

boombox.ts

Nous avons apporté quelques modifications mineures au composant boombox.ts que nous avons trouvé dans un autre exemple de projet
Studio. La manière dont nous utilisons cette fonctionnalité permet à un joueur d'interagir avec le niveau afin d'initialiser le son au toucher.

         
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})
  }
})

      

 

de l'interface utilisateur

Nous avons également développé quelques modifications mineures de l'interface utilisateur pour les écrans de démarrage et le didacticiel du jeu. Nous avons utilisé CSS et quelques émissions d'événements afin de passer d'un écran à l'autre.

Pour illustrer brièvement la logique basée sur les événements, nous avons d'abord le bouton startButton qui émet un événement lorsqu'il est touché :

         
// 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')
})

      

 

Ensuite, l'écran du didacticiel détecte cet événement et s'affiche :

         
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
})

      

 

Cela donne au jeu une introduction solide où nous faisons inconsciemment comprendre à l'utilisateur l'importance d'explorer et de toucher les objets dans le jeu.

 

Portail

Nous recyclons en gros le script portal.ts du projet exemple Door Portal afin de pouvoir utiliser nos propres splats gaussiens dans le jeu et, là encore, offrir une expérience qui donne au joueur l'impression d'explorer le monde :

         
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
  	}
  }
})

      

 

Nous permettons au monde de rester caché lorsque le joueur ne s'est pas rendu à un endroit précis de la carte.

 

Shaders et vidéo

Nous tirons également parti des shaders dans le niveau 3 en particulier. Cela nous permet d'importer une ressource vidéo et de la lire sur un plan afin de créer un effet de moniteur cardiaque sur les sols et les plafonds du jeu :

         
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)
}

      

 

Nous créons un ChromaKeyMaterial qui définit tous les paramètres appropriés à partir de la vidéo importée afin qu'elle puisse être lue sur le plan.

 

Autres scripts

Nous utilisons un script d'importation générique pour importer nos pages CSS pour notre interface utilisateur initiale, comme suit :

         
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.