西洋の記憶

このユニークな2台用ゲームでは、プレイヤーは友人や見知らぬ人と協力し、3Dスキャンされた環境を探索し、パズルを解きながら、不気味なデジタル世界の物語を解き明かしていきます。 QRコードの仕組みとガウススプラッシュを駆使し、Western Memoryはプレイヤーに画面の外側を考えるよう挑戦させながら、物語豊かな冒険へと導きます。

Western Memory

サンプルプロジェクトを参考に、自分好みにカスタマイズしてください。

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

この体験は、Niantic Studioを使用して作成されたWeb ARゲームです。 プレイヤーは、境界的なガウス分布のスplatスキャンされた世界に置かれ、体験内に埋め込まれたQRコードを発見すると、スマートフォンを持つ別のプレイヤーと協力してコードをスキャンし、前進する必要があります。

5つのレベルがあり、最終レベルにはQRポータルが設置されており、プレイヤーが初回プレイ時に発見しなかった秘密を解き明かす選択をした場合、レベル1に戻されます。

各レベルには異なるインタラクションの要素が用意されており、以下で詳細に説明されます。 『
』のゲーム開始時、プレイヤーに以下の指示が表示されます:

  1. プレイする場所は、周囲に広く広がっている必要があります。
  2. 特定の要素を触ったりタップしたりすることで、それらを目覚めさせることができます。
  3. 音は重要です。
  4. 行き詰まったと感じたことはありますか? ページを再読み込みしてみてください。
  5. この道を一人で歩く者はいない。 友達を連れてきて... あるいは、見知らぬ人かもしれません。

Project Structure

レベル間のプロジェクト構造は、おおむね一貫しています。 当社が活用している主な機能は2つあります。 一つはtouchToAnimateコンポーネントで、これを使用してプレイヤーが開くドアとインタラクションできるようにします。 2つ目は、ブームボックスコンポーネントのカスタマイズで、プレイヤーがオーディオを再生するエンティティとインタラクションできるようにする機能です。

ゲームの他の主要な機能の一つは、QRコードを使用してマルチプレイヤー機能を実現する点です。 プレイヤーは自分でQRコードをスキャンできないため、次のレベルをスマートフォンに読み込むためにパートナーが必要です。

3Dシーン
  • ベースエンティティ: 異なるレベルで利用される3Dアセットをインポートします。 そのうちの一部は通常の.glbオブジェクトであり、他のものはGaussian Splat.spzモデルです。 これには、Perspective AR カメラとAmbient/Directional Lightsも含まれます。
  • UI エンティティ:最初のレベル用のUIを作成し、ユーザーをスタート画面から初期チュートリアルまで案内する流れを実装しました。

アセット
  • 使用されている3Dアセットと.spzモデルは、常にAssetsディレクトリ内に配置されています
スクリプト
  • スクリプトは、時間的制約のため通常はルートディレクトリに配置されています。 時間があれば、各スクリプトを適切に定義されたディレクトリに整理して配置したいところですが、主な目的は完成度の高い体験を提供することでした。

Implementation

このドキュメントでは、ゲームのコアロジックを構成する主要なスクリプトの流れを説明します。

touchToAnimate.ts

私たちが独自に作成した主要なコンポーネントの一つが、touchToAnimate コンポーネントです。 コンポーネントのサンプルプロジェクト内のREADME.mdに スクリプトの非常に詳細な手順説明が記載されています。 簡潔さを重視して、ここではウォークスルーを貼り付けません。詳細は添付のリンクからご確認ください。

これは、ゲームの触覚的な感覚に不可欠な主要なコンポーネントでした。 プレイヤーに、直感的な方法で世界とインタラクションしている感覚を与えます。

boombox.ts

別の
Studioサンプルプロジェクトで発見したboombox.tsコンポーネントに対して、いくつかの minor customizations を行いました。 この機能の使い方により、プレイヤーがレベルとインタラクションすることで、タッチ操作時にオーディオを初期化することが可能です。

         
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

また、ゲームの開始画面とチュートリアル画面のUIを一部開発しました。 CSSと一部のイベント発火を使用して、画面間を移動するようにしました。

イベントベースのロジックを簡潔に説明すると、まずstartButtonがタッチされた際にイベントを発火します:

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

      

 

その後、チュートリアル画面がこのイベントを検出し、自身を表示します:

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

      

 

これにより、ゲームに堅実な導入部が生まれ、ユーザーが無意識のうちにゲーム内での探索とオブジェクトの操作の重要性を認識するようになります。

 

ポータル

私たちは、Door Portal サンプルプロジェクトから portal.tsスクリプトを卸売リサイクルし、ゲーム内で独自のガウススプラッシュを使用できるようにしています。これにより、プレイヤーが世界を探索しているような体験を提供できます。

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

      

 

プレイヤーがマップ上の特定の場所に行っていない場合、世界は隠れたままになります。

 

シェーダーとビデオ

レベル3では特にシェーダーを活用しています。 これにより、動画アセットをインポートし、平面上に再生することで、ゲームの床と天井に心電図のような効果を再現できます:

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

      

 

インポートした動画から適切なパラメーターをすべて設定したChromaKeyMaterialを作成し、その素材を平面上に再生できるようにします。

 

その他のスクリプト

初期のUI用にCSSページをインポートするために、汎用のインポートスクリプトを使用しています。具体的には次のように行っています:

         
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.