西洋の記憶

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

Western Memory

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

door

タップしてアニメーション checkmark bullet

プレイヤーの入力に反応するインタラクティブな要素を追加して、魅力的で手触りのよいゲームプレイを実現する方法を学びます。

サンプルプロジェクトを見る

Behind the Build: Western Memory

Written by Taj Rauch

December 13, 2024


Introduction

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

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

  1. プレイする場所は、周囲に広く広がっている必要があります。
  2. 音は重要です。 あるいは、見知らぬ人かもしれません。

Project Structure

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

3Dシーン
  • ベースエンティティ: 異なるレベルで利用される3Dアセットをインポートします。 そのうちの一部は通常の.glbオブジェクトであり、他のものはGaussian Splat.spzモデルです。 これには、Perspective AR カメラとAmbient/Directional Lightsも含まれます。

アセット
  • 使用されている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と一部のイベント発火を使用して、画面間を移動するようにしました。

         
// 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.