フローズン・コインハント・アドベンチャー

キャプテン・ドティと一緒に、フローズン・ワールドを冒険しよう。 金色のコインを集めて、アイスキノコから脱出しよう!

frozencoinhuntadventure

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

frozencointsplatter

スタジオコイン・スキャッター checkmark bullet

このプロジェクトでは、Niantic Studioのコンポーネントを使用して、指定された半径内にコインをランダムに拡散する方法を説明します。

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

スタジオドティ・スノーボール・パーティ checkmark bullet

このプロジェクトでは、Niantic Studio Physicsエンジンを使ってミニゲームを作成する方法を説明します。

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

Behind the Build: Frozen Coin Hunt Adventure

Written by Alex di Guida

May 16, 2025


Introduction

この体験は、2024年10月、Niantic Studio Betaを使用して作成されたWebARゲームです。

The Tiny Mushroom
- あなたを追いかけてくる小さなキノコを避けながら、20枚のゴールドコインを集めよう。
The Angry Mushroom
- あなたを捕まえようとする怒ったキノコを避けながら、20枚のゴールドコインを集めよう。
The Angry Mushroom
- あなたを捕まえようとする怒ったキノコを避けながら、20枚のゴールドコインを集めよう。

キングキノコ
- t彼の最後の挑戦では、勝つためにキングキノコと雪合戦を開始します。  グラウンドから雪玉を集め、王様に投げつけて倒そう。

Project Structure

3D Scene
  • ベースエンティティ:  パースペクティブARカメラとアンビエント/ディレクショナルライトを含む。 各チャレンジの間にチュートリアルが表示されます。

アセット
  • ゲーム中に使用されるすべての3Dモデル、オーディオファイル、画像が含まれています。
Scripts

Scriptsフォルダは3つのネストしたフォルダに分かれています:
 - コンポーネント:

メインゲームロジックが含まれます。

アセット
  • ゲーム中に使用されるすべての3Dモデル、オーディオファイル、画像が含まれています。

Implementation

このゲームのメインスクリプトに詳しく焦点を当て、この体験のすべてのコアロジックを管理する。

Coin

coin.ts

これは、シーン内で新しいコインの生成を処理するコイン・コンポーネントです。

         
   ecs.GltfModel.set(world, eid, {url: coinAsset})


   world.events.addListener(eid, ecs.events.GLTF_MODEL_LOADED, (result) => {
     // @ts-ignore
     const {data: {model}} = result


     storeCoinMaterial(model, eid)
     applyRandomPositionToCoin(world, eid)
   })

      

- コインモデルのURLを含むcoinAssetを使用して、コインモデルを設定します。

spread-coins.ts

spreadCoins関数はadd時に呼び出される関数で、シーン内のコインの生成を担当する:

         
const spreadCoins = (world, component) => {
 const {eid, schema: {totalCoins}} = component


 for (let i = 0; i < totalCoins; i++) {
   const coinEntity = world.createEntity()
   // @ts-ignore
   const floatingSpeed = THREE.MathUtils.randFloat(0.0005, 0.0015)
   // @ts-ignore
   const floatingAmplitude = THREE.MathUtils.randFloat(0.5, 2)
   Coin.set(world, coinEntity, {floatingSpeed, floatingAmplitude})
   world.setParent(coinEntity, eid)  // Set coin created as a children of Coins group
   coins.push(coinEntity)
 }
}

      

次に、checkSnowflakeProximityとcheckPlayerProximityがあり、これらはコンポーネントの毎フレームtick関数で呼び出されます。 プレイヤーがコインに近づくと、コインは回収されてシーンから隠され、次のチャレンジを開始する前にリセットを待ちます。

回収ブーリアンは、コインが回収されている間に関数を繰り返し呼び出すことを回避します。

         
const checkPlayerProximity = (world, component) => {
 coins.forEach((coin) => {
   ......
   const distanceToPlayer = vecCoinPosition.distanceTo(vecPlayerPosition)
   if (!collecting && distanceToPlayer <= collectionDistance) {
     collecting = true
     world.events.dispatch(eid, 'coin-collect')  // Updated UI score
     coins = coins.filter(c => c !== coin)  // Clear collected coin from the coins array
     disableCoin(world, coin)
     collecting = false
   }
 })
}

      

checkSnowflakeProximityも同様の機能である。 プレイヤーがコインに近づくにつれて、コインの素材の不透明度が徐々に増していきます。 プレイヤーがコインに近づいた場合、コインは回収され、最終的にその場から取り除かれる。

Player

player-controller.ts

これはプレーヤー・コンポーネントです:

         
const handleGameStart = () => {
     HoldDrag.set(world, eid, {factor: 12, lag: 0.25, distanceThreshold: 0.05, cone, help, timer})
   }


   const handleReset = () => {
     HoldDrag.remove(world, eid)


     const position = ecs.Position.cursor(world, eid)
     position.x = 0
     position.y = 0
     position.z = 0


     const quaternion = ecs.Quaternion.cursor(world, eid)
     quaternion.x = 0
     quaternion.y = 0
     quaternion.z = 0
     quaternion.w = 0
   }


   world.events.addListener(eid, 'reset', handleReset)
   world.events.addListener(eid, 'game-start', handleGameStart)

      

HoldDragコンポーネントは、開始時に設定され、Dotyを移動させることができます
handleReset関数は、次のチャレンジに移動するときに、Dotyを最初の位置と回転に戻します

hold-drag.ts

3つのメインリスナーを通して、Dotyを移動させる、ホールドとドラッグの指ジェスチャーを処理します:

         
world.events.addListener(eid, ecs.input.SCREEN_TOUCH_START, handleStart)
world.events.addListener(eid, ecs.input.SCREEN_TOUCH_MOVE, handleMove)
world.events.addListener(eid, ecs.input.SCREEN_TOUCH_END, handleEnd)

      

このコンポーネントには、Dotyの "Walk "アニメーションと "Idle "アニメーションも含まれており、それぞれDotyが動いているときや止まっているときにトリガーされる。

Projectile

projectile.ts

シーン内で新しいコインの生成を処理する projectile コンポーネントです。

投射物は、一度ロードされると、目盛り(scale)とtimeStartで初期化される。

         
// Wait for the Projectile's model to be fully loaded
world.events.addListener(eid, ecs.events.GLTF_MODEL_LOADED, initializeProjectile)

      

コリジョンリスナーは、キングキノコが雪玉でヒットされたかどうかをチェックする役割を担っており、その場合、そのスケールが特定のトリガーに達すると、ゲームの勝利、プレイヤーはキングキノコを倒したことになります。

         
   // Collision event listener
   world.events.addListener(eid, ecs.physics.COLLISION_START_EVENT, (event) => {
     const {data: {other}} = event
     // Check if there is a collision with the target "Tree"
     if (target && target.toString() === other.toString()) {
       const {x, y, z} = ecs.Scale.get(world, target)
       const toX = x - 0.2
       const toY = y - 0.2
       const toZ = z - 0.2
       ecs.ScaleAnimation.set(
         world,
         target, {
           fromX: x, fromY: y, fromZ: z, toX, toY, toZ, duration: 100, loop: false,
         }
       )
       if (toX <= 1) { console.log('GAME WIN!')
         ecs.GltfModel.set(world, target, {
           animationClip: 'Death',
           loop: false,
           paused: false,
         })
         world.events.dispatch(eid, 'game-win')
       }
     }
   })

      

projectile-spawner.ts

Projectile spawnerはDotyの中にあるエンティティで、Dotyのボディ・レベルの周りにあるボックスから、雪玉の "projectile "がスポーンされ、王に投げられる。 このイベントは発射体スポナーコンポーネントをトリガーし、シーンに新しい発射体を作成します。


         
     world.events.addListener(eid, 'spawn-projectile', () => {
     spawnProjectile()
   })

      

spawnProjectile関数は、シーンに新しい発射体のエンティティを作成し、発射します。 これは、スポーナーのワールドの位置と回転に基づいて、発射体の初期位置と向きを設定します。 この関数は、前方方向と放物線運動の垂直成分を含む発射体の軌道を計算する。 また、発射体の質量、半径、弾みやすさなどの物理プロパティを設定し、発射速度と発射方向を設定する。

雪玉

snowball.ts

これは、シーン内の凍った床に新しい雪玉を作成する雪玉コンポーネントです
雪玉がロードされると、与えられた半径内に設定されたスケールとランダムな位置を取得します。

         
world.events.addListener(eid, ecs.events.GLTF_MODEL_LOADED, () => {
     applyRandomPosition(world, eid)
     const snowballScale = ecs.Scale.cursor(world, eid)
     snowballScale.x = scale
     snowballScale.y = scale
     snowballScale.z = scale
   })

      


spread-snowballs.ts

このコンポーネントは、spread-coins.ts コンポーネントに似ており、2つの主な機能があります:
- checkPlayerProximity 関数は、プレーヤーと雪玉の間の距離を計算し、雪玉を収集できるかどうかを決定します。

 

UI - Screens

このフォルダ内の各UIコンポーネントは、ゲームセッション中に表示される画面、またはアイコンや1つのボタンなどのUI要素を表します。 <これらはすべてsetupCss関数を持っており、 <headの> styleタグ内に特定のCSSを追加する>.

game-over-screen.ts

ゲームオーバーのロゴを表示する
ゲームのすべてのオーディオを一時停止する
最後にゲームオーバーのオーディオを再生する

landing-screen.ts

これはゲームを実行するときに最初に表示される画面である
ユーザーが実験したい難易度を選択できる場所である。 ドロップダウンエレメントは、1つのオプションを選択すると難易度が変化するリスナーで作成されます:

         
// DIFFICULTY DROPDOWN
......
const dropdown = document.createElement('select')
......
const options = ['[ Game Difficulty ]', 'easy', 'normal', 'hard']
options.forEach((option) => {
  const optionElement = document.createElement('option')
  optionElement.value = option
  optionElement.textContent = option
  ......
  dropdown.appendChild(optionElement)
})


// Add an event listener to get the selected value when it changes
dropdown.addEventListener('change', (event) => {
 // @ts-ignore
 const {target: {value}} = event
 // Update mode
 updateMode(value)
 // Handle option title selection assigned normal as default selection in that case
 if (value === options[0]) defaultSelectedValue.selected = true
})

      

最後にこの画面は、最初のゲームチュートリアルを開始するために「START」ボタンをタップすることも担当する:

         
// ENTER BUTTON
const button = document.createElement('button')
button.id = 'enter-btn'
button.innerText = 'ENTER'
button.addEventListener('click', () => {
  ......
  world.events.dispatch(tutorial, 'show-game-tutorial', {speed, mode})
})

      

tutorial-screen.ts

- この画面は、ゲームが初めて起動するときと、各章の間に、新しい指示とゲームプレイの仕組みを示すために使用されます
- "OK "ボタンを押すと、ゲームセッションの実行を担当するさまざまなコンポーネントを起動するためのイベントをディスパッチします:

         
// Handle on press start game
   button.addEventListener('click', () => {
     const dataAttr = dataAttribute.cursor(eid)
     world.events.dispatch(hearts, 'game-start', {})
     world.events.dispatch(timer, 'game-start', {mode: dataAttr.mode})
 ......

      

スライダーや2つのナビゲーション・アロー・ボタンなどのUI要素の作成がある。

win-screen.ts

- ゲーム勝利のロゴを表示する
- ゲームのすべてのオーディオを一時停止する
- 最後にゲーム勝利のオーディオを再生する


 

UI - 要素

残りの要素は、主にゲームセッション中に表示される単一のボタンまたはアイコンです。 これらはすべて2つの異なるイベントを持っており、ユーザーにコンテンツを隠したり見せたりすることができる:

         
 world.events.addListener(eid, 'hide', () => handleHide(world, eid)) // hide
  world.events.addListener(eid, 'game-start', () => handleShow(world, eid)) // show

      

hearts.ts

画面左下にドッティの3つの人生を表す3つのハートアイコンUIを作成

help.ts

「?「
チュートリアルが表示されている間、ゲームは一時停止しているので、ユーザーは安全にもう一度説明を読むことができます

score.ts

このコンポーネントはスコアUIのために特別に設計されており、スコアを隠したり表示したりするのに役立ちます

snowball-button.ts

これは、ユーザーが最後のゲームパート中に雪玉を投げるイベントをトリガーできるようにするボタンです:




チュートリアルが表示されている間、ゲームは一時停止しているので、ユーザーは安全にもう一度説明を読むことができます

score.ts

このコンポーネントはスコアUIのために特別に設計されており、スコアを隠したり表示したりするのに役立ちます

snowball-button.ts

これは、ユーザーが最後のゲームパート中に雪玉を投げるイベントをトリガーできるようにするボタンです:



         
world.events.dispatch(spawner, 'spawn-projectile') 

      

timer.ts

このコンポーネントは、アクティブなゲームセッション中に実行されるタイマーUIの作成を担当する
このタイマーは一時停止と再開が可能である。

その他

ここに残っているコンポーネントは、ゲーム・マネージャー、マッシュルーム、スノー・パーティクル、そしてスノーフレークである。

         
   /**
    * STATES
    */
   ecs.defineState('default').initial().onEnter(() => {
     // When entering the 'default' state, add listeners for the required events
     world.events.addListener(world.events.globalId,
       'coin-collect', coinCollect)
     world.events.addListener(world.events.globalId,
       'heart-lost', heartLost)
     world.events.addListener(world.events.globalId,
       'game-win', gameWinState)
     world.events.addListener(world.events.globalId,
       'game-over', gameOverState)
     world.events.addListener(world.events.globalId,
       'game-pause', gamePauseState)
   }).onExit(() => {
     // When exiting the 'default' state, remove the listeners
     world.events.removeListener(world.events.globalId,
       'coin-collect', coinCollect)
     world.events.removeListener(world.events.globalId,
       'heart-lost', heartLost)
     world.events.removeListener(world.events.globalId,
       'game-win', gameWinState)
     world.events.removeListener(world.events.globalId,
       'game-over', gameOverState)
     world.events.removeListener(world.events.globalId,
       'game-pause', gamePauseState)
   })

      

mushroom.ts

きのこコンポーネントは、ゲームの敵のエンティティです。 前述したように、3種類のキノコがあります:小さいキノコ、怒っているキノコ、王様キノコ

このコンポーネントの主なロジックは次のとおりです:
- growMushroom関数:プレイヤーがキノコの原点から移動し、ゲームが開始されると、キノコを拡大します

- followPlayer関数:プレイヤーの動きに追従するために、キノコの次の位置と回転を設定します。

         
export const followPlayer = (world, component, ecsMushroomPosition) => {
 const {eid, data: {speed}} = component
 // Smoothly move mushroom towards player using lerp
 const lerpFactor = speed
 ecsMushroomPosition.x = lerp(ecsMushroomPosition.x, vecPlayerPosition.x, lerpFactor)
 ecsMushroomPosition.y = lerp(ecsMushroomPosition.y, vecPlayerPosition.y, lerpFactor)
 ecsMushroomPosition.z = lerp(ecsMushroomPosition.z, vecPlayerPosition.z, lerpFactor)
 // Update mushroom's position
 vecMushroomPosition = vec3.xyz(ecsMushroomPosition.x, ecsMushroomPosition.y, ecsMushroomPosition.z)
 // Make the mushroom face the player, but only rotate on the Y axis
 const direction = vecPlayerPosition.minus(vecMushroomPosition)  // Define the direction from mushroom to player
 // Calculate the angle based on the X and Z axes of the player position
 const angle = Math.atan2(direction.x, direction.z)
 // Set the Y rotation of the mushroom to face the player
 const rotationQuat = quat.yRadians(angle)
 // Directly set the new quaternion in ECS
 const currentQuaternion = getQuaternion(world, eid)
 currentQuaternion.x = rotationQuat.x
 currentQuaternion.y = rotationQuat.y
 currentQuaternion.z = rotationQuat.z
 currentQuaternion.w = rotationQuat.w
}

      

checkPlayerProximity:毎フレーム、キノコがプレイヤーから十分に近いかどうかをチェック。

         
const dispatchLosingHeart = (world, mushroomId, trigger = 500) => {
 if (hearts.length <= 0) return
 const currentTime = Date.now()  // Get the current time in milliseconds
 if (currentTime - lastExecutionTime >= trigger) {  // Check if 500 second has passed
   lastExecutionTime = currentTime  // Update the last execution time
   world.events.dispatch(mushroomId, 'heart-lost', {mushroomId})
 }
}


export const checkPlayerProximity = (world, component, distanceToPlayer) => {
 const {eid, schema: {distanceCatch}} = component
 // Check if the distance is less than distanceCatch
 if (distanceToPlayer <= distanceCatch) {
   dispatchLosingHeart(world, eid)
 }
}

      

dispatchLosingHeart関数にはスロットリング・ロジックがあり、これは関数がトリガー間隔よりも頻繁に実行されないようにするものです。 例えば1フレームで3つのハートをすべて失わないようにするため

snow-particles.ts

各ステージに降り注ぐ雪の粒を作成するコンポーネント
scripts/classes/SnowParticlesにあるSnowParticlesマテリアルを使用


snowflake.ts

Dotyの周りを浮遊している3Dエンティティで、第2章に登場します
このコンポーネントは、ゲームセッション中は必要ないため、コンポーネントの表示/非表示を担当します
 


Helpers

これらのヘルパーは、コードのさまざまな場所で呼び出されるロジックの内訳を提供するファイルのセットです ここでは、主なものを紹介します: 


data.ts

このファイルには、スピード値、キノコのデータ(再生するクリップ、サイズ、URL)など、コード全体で使用される設定やコンフィギュレーションがたくさんあります。

store.ts

このファイルにはstoreTimer関数とgetAllTimers関数があります。 ゲーム終了時のタイムと難易度モードを記憶する。

Your cool escaped html goes here.