Memoria occidental
En este juego único para dos dispositivos, los jugadores colaboran con un amigo o un desconocido para recorrer entornos escaneados en 3D, resolver puzles y descubrir la historia de un inquietante mundo digital. Con el uso de la mecánica de códigos QR y splats gaussianos, Western Memory reta a los jugadores a pensar fuera de la pantalla mientras se adentran en un viaje rico en narrativa.

Hazlo tuyo con el proyecto de ejemplo

Toque para animar
Aprenda a añadir elementos interactivos que respondan a las acciones del jugador, creando un juego atractivo y táctil.
Ver ejemplo de proyectoBehind the Build: Western Memory

Written by Taj Rauch
December 13, 2024
Introduction
Esta experiencia es un juego de realidad aumentada creado con Niantic Studio. El jugador se ve inmerso en un mundo liminal, escaneado con splats gaussianos, en el que, al encontrar códigos QR incrustados en la experiencia, debe colaborar con otra persona que tenga un teléfono para escanear el código y avanzar.
Hay 5 niveles, y el último tiene un portal QR que devuelve al jugador al nivel 1, si decide descubrir secretos que no encontró la primera vez.
Cada nivel tiene diferentes características de interacción que se explorarán a continuación. Al principio de
el juego, se dan instrucciones al jugador que dicen:
- El lugar donde juegues debe extenderse a tu alrededor.
- Se pueden tocar o golpear ciertos elementos para despertarlos.
- El sonido es clave.
- ¿Alguna vez te has sentido atascado? Intenta actualizar la página.
- Ningún alma camina sola por este sendero. Trae a un amigo... o tal vez un extraño.
Project Structure
La estructura de los proyectos entre niveles se mantiene relativamente constante. Hay dos características principales que aprovechamos. Uno es el componente touchToAnimate
, que utilizamos para que los jugadores interactúen con las puertas que se abren. El segundo es una personalización sobre un componente boombox
, que permite a los jugadores interactuar con entidades que reproducen audio.
Otro de los aspectos principales del juego es el uso de códigos QR para permitir una dinámica multijugador. Como un jugador no puede escanear el código QR por sí solo, necesita un compañero para cargar el siguiente nivel en su teléfono.
Escena
3D- Entidades Base: Portamos en 3D activos utilizados a lo largo de diferentes niveles. Algunos de ellos son objetos
.glb
normales, otros son modelos.spz
de Gaussian Splat. Esto también incluye la cámara Perspective AR y Ambient/Directional Lights. - Entidades UI: Creamos una interfaz de usuario para el primer nivel que guía al usuario a través de una pantalla de inicio y un tutorial inicial.
Activos
- Los assets 3D y los modelos
.spz
utilizados se encuentran siempre dentro del directorio Assets
Scripts
- Los scripts suelen colocarse en la raíz debido a limitaciones de tiempo. Si tuviéramos más tiempo, colocaríamos cada guión en un directorio bien definido, pero el objetivo principal era conseguir una experiencia pulida.
Implementation
Esto recorre los scripts principales para la lógica central del juego.
touchToAnimate.ts
Uno de los principales componentes que creamos nosotros mismos fue un componente touchToAnimate. Puede encontrar un detallado tutorial del script README.md
en el proyecto de ejemplo del componente aquí. En aras de la brevedad, no voy a pegar aquí el tutorial, que se puede encontrar en el enlace adjunto.
Este era un componente esencial para la sensación táctil del juego. Da al jugador la sensación de interactuar con un mundo de forma intuitiva.
boombox.ts
Hemos realizado algunas pequeñas modificaciones en el componente boombox.ts
que encontramos en otro proyecto de ejemplo de
Studio. La forma en que utilizamos esto permite que un jugador interactúe con el nivel para inicializar el audio al tocarlo.
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
También desarrollamos algunos UI menores para las pantallas de inicio y tutorial del juego. Hemos utilizado CSS y algunas emisiones de eventos para pasar de una pantalla a otra.
Para ilustrar brevemente la lógica basada en eventos, primero tenemos el startButton
emitir un evento cuando se ha tocado:
// 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')
})
A continuación, la pantalla del tutorial recoge este evento y se hace aparecer:
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
})
Esto da al juego una introducción sólida en la que subconscientemente registramos al usuario la importancia de explorar y tocar objetos dentro del juego.
Portal
Reciclamos al por mayor el script portal.ts
del proyecto de ejemplo Door Portal para poder utilizar nuestras propias salpicaduras gaussianas dentro del juego y, de nuevo, proporcionar una experiencia en la que parezca que el jugador está explorando el mundo:
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
}
}
})
Permitimos que el mundo permanezca oculto cuando el jugador no ha ido a un punto concreto del mapa.
Shaders y Video
También aprovechamos los shaders en el nivel 3 específicamente. Esto nos permite importar un activo de vídeo y reproducirlo en un plano para dar el efecto de un monitor cardíaco en el suelo y el techo del juego:
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)
}
Creamos un ChromaKeyMaterial
que establece todos los parámetros apropiados del vídeo importado para que pueda reproducirse en el plano.
Otros Scripts
Utilizamos un script de importación genérico para importar nuestras páginas CSS para nuestra interfaz de usuario inicial de la siguiente manera:
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)