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


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関数があります。 ゲーム終了時のタイムと難易度モードを記憶する。