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

Behind the Build: The Last Pin

Written by Sam Guilmard
May 5, 2025
Introduction
このプロジェクトは、Niantic Studio内でユーザーの物理的な入力を用いてコアゲームメカニクスを制御するゲームを作成する際の理解を深めるために作成されました。 この例では、ユーザーの頭部の回転がメインプレイヤーの操作に利用されています。 また、組み込みのアニメーションコンポーネントを使用して、敵の移動などゲーム内の多くの要素を制御しています。 生存時間が長ければ長いほど、獲得できるポイントが増えます。 プレイヤーがボウリングのボールに当たると、ゲームは終了します。 ゲーム中、ボウリングボールの出現率が徐々に上昇し、同時にボールの速度も速くなっていきます。
Project Structure
3Dシーン
- 全体的なシーンは比較的シンプルで、3Dモデルとプリミティブ形状を組み合わせて環境を構成しています。 また、3D UI要素を活用しており、これらの位置を3D空間内で操作可能です。 排水溝はボウリングレーンの両側に配置され、ピンはユーザーの頭部の回転で操作されます。 ピンとメインプレイヤーをイメージしてください。
- ピンがボールに当たった際に再生されるオーディオファイルが1つあります。
- テクスチャフォルダーには、モデルテクスチャとUI要素の組み合わせが含まれています。
- さらに、合計10種類の異なるボールテクスチャがあります。
- ピンがボールに当たった際に再生されるオーディオファイルが1つあります。
- さらに、合計10種類の異なるボールテクスチャがあります。
Implementation
このドキュメントのセクションでは、ゲーム内の主要なメカニクスについて説明します。
schema: {
// Add data that can be configured on the component.
pin: ecs.eid,
},
シーン階層内に入り、ゲームコントローラー コンポーネント("GameController" スクリプトがアタッチされているもの)を選択し、インスペクター パネル内のスクリプトに移動します。 ピン設定のオプションがありますので、ドロップダウンパネルから正しいオブジェクトを選択してください。
const {
pin,
} = component.s
また、add関数内では2つのイベントリスナーを作成します。 一つは「facecontroller.faceupdated」というイベントが発生した際に顔が表示されたことを検知し、もう一つは「facecontroller.facelost」というイベントが発生した際に顔が隠れたことを検知します。 イベントリスナーの最後に、このイベントが発生した際に呼び出したい関数の名前を定義します。
// listener for head rotation and pin movement
world.events.addListener(world.events.globalId, 'facecontroller.faceupdated', show)
// listener for losing head rotation
world.events.addListener(world.events.globalId, 'facecontroller.facelost', lost)
「show」関数は、ユーザーの頭の位置と回転の値を引数として受け取ります。 この関数内で「xRot」という変数が作成され、ユーザーの頭のX方向の回転角度の値が代入されます。 IF文内で、ゲームが現在再生中かどうかを確認する処理において、xRotの値がシーン内のピンXの位置に常に適用されています。 この値は、GameControllerシーンオブジェクト内で簡単に編集可能な別の変数「pinSpeed」と乗算されます。
// controls the movement of the pin
const show = ({data}) => {
const xRot = data.transform.rotation.x
// console.log(xRot)
if (playing == true) {
ecs.Position.set(world, pin, {x: Math.min(Math.max(xRot * -pinSpeed, -1.5), 1.5), y: 0, z: 8})
}
}
ピン速度
この数値が高いほど、ピンが「速く」移動します。 この値が否定される理由は、シーンとユーザーのポジションに対して正しい方向へ移動するためです。 この値をピンオブジェクトに適用すると、その値がMath.minとMath.max関数の中にネストされていることも確認できるでしょう。 これは、画面上のピン的位置を制限し、ゲーム領域内にとどまり、不正行為ができないようにするためです。 これにより、プレイヤーが顔を隠すことで不正行為を行うことができません。 各ボールにはランダムなX開始位置とランダムなX終了位置が設定され、これによりボールが対角線上に移動し、ゲームがより難しくなります。
schema: {
// Add data that can be configured on the component.
ball1: ecs.eid,
ball2: ecs.eid,
ball3: ecs.eid,
ball4: ecs.eid,
ball5: ecs.eid,
ball6: ecs.eid,
ball7: ecs.eid,
ball8: ecs.eid,
ball9: ecs.eid,
ball10: ecs.eid,
pin: ecs.eid,
},
再び、シーン内のGameControllerオブジェクトにアタッチされたGameControllerスクリプトのドロップダウン選択バーを使用して、各ボールを適用できます。
add: (world, component) => {
// Runs when the component is added to the world.
const {
ball1,
ball2,
ball3,
ball4,
ball5,
ball6,
ball7,
ball8,
ball9,
ball10,
pin,
} = component.schema
ゲームが開始される際、体験の開始時または再起動時に、「spawnBall」関数が呼び出されます。
function spawnBall() {
// checks if the game is currently playing
if (playing == true) {
// decrease the spawn time every time a ball is spawned
if (spawnTime > spawnFrequencyMinimum) {
spawnTime -= spawnFrequencyMultiplier
}
const randomxStart = getRandomStartX()
const randomxEnd = getRandomEndX()
// this calculates the difference between the x start position and x end position - the higher the difference the more rotation that is added const differenceInDistance = Math.abs(randomxStart - randomxEnd)
// if the x start position is bigger than the end then the ball is moving left
if (randomxStart >= randomxEnd) {
curveLeft = true
} else {
curveLeft = false
}
updateMaterial()
applyScaleAnimation()
world.setPosition(balls[currentBall], randomxStart, 0.375, -0.5) applyPositionAnimation(randomxStart, randomxEnd)
applyRotationAnimation(differenceInDistance)
// Increase current ball or reset
if (currentBall == 9) {
currentBall = 0
} else {
currentBall++
}
// time out function to delay the next spawning of a ball
world.time.setTimeout(() => {
if (playing == true) {
spawnBall()
}
}, spawnTime)
}
}
「spawnBall」関数内のすべてのコードは、ゲームが現在プレイ中かどうかを確認するif文の中にネストされています。 これは、ゲームが終了した場合に、さらにボールが生成されないようにするためです。 「spawnTime」の値は、関数が呼び出されるまでに経過する必要のある時間です。 この値は、関数が呼び出されるたびに、関数の開始時に変更されます。
if (spawnTime > spawnFrequencyMinimum) {
spawnTime -= spawnFrequencyMultiplier
}
この機能は、まず現在のspawnTimeがspawnFrequencyMinimumよりも大きい場合のみ動作します。 もしそうであれば、spawnTime から spawnFrequencyMultiplier を引きます。 この値を制限することで、画面上に同時に表示されるボールの数が過剰になり、ゲームがプレイ不能になるのを防ぎ、フレームレートに大幅な影響を与えることを防止します。
次に、これらの2つの関数を呼び出して、Xの開始位置と終了位置を割り当てます。
const randomxStart = getRandomStartX()
const randomxEnd = getRandomEndX()
これらの2つの関数は同じように動作し、まず「negativeStart」という値を0または1のランダムな値で定義します。 これは、X座標が負か正かを判断するために使用されます。 次に、「xValue」に0から2の間のランダムな値が割り当てられます。 この値は返されますが、もし「negativeStart」が0に等しい場合、xValueは負の値に反転されます。
// Get a random x value for ball start position
function getRandomStartX() {
const negativeStart = Math.floor(Math.random() * (2))
const xValue = Math.random() * (2)
if (negativeStart == 0) {
return xValue * -1
} else {
return xValue
}
}
次に、2つの新しいx座標を使用して「differenceInDistance」に値を割り当て、X軸上の距離を計算します。 これは、後でスピン回転を適用する際に使用されます。
const differenceInDistance = Math.abs(randomxStart - randomxEnd)
次に、ボールがどの方向へ動いているかを計算します。 このアニメーションを回転アニメーションの適用にも使用します。ボールが左に移動している場合、ボールも左に回転する必要があります。 ボールが左に移動している場合、ボールも左に回転する必要があります。
// if the x start position is bigger than the end then the ball is moving left
if (randomxStart >= randomxEnd) {
curveLeft = true
} else {
curveLeft = false
}
次に呼び出される関数は「applyMaterial」関数です。 add関数の開始時に、テクスチャ名用の配列が作成されました。 別の配列が定義され、すべてのボールが含まれます。
const textures = ['ballTexture1', 'ballTexture2', 'ballTexture3', 'ballTexture4', 'ballTexture5', 'ballTexture6', 'ballTexture7', 'ballTexture8', 'ballTexture9', 'ballTexture10']
const balls = [ball1, ball2, ball3, ball4, ball5, ball6, ball7, ball8, ball9, ball10]
これらの名前はアセットフォルダー内のテクスチャと一致している必要があります。
「applyMaterial」関数の最初で、0から9までのランダムな数値が生成されます。 その数値に応じて、ボールに特定のテクスチャが適用されます。
currentTexture = Math.floor(Math.random() * 10)
// Apply the texture to the object
ecs.Material.set(world, balls[currentBall], {
textureSrc: `${require(`./assets/${textures[currentTexture]}.png`)}`, roughness: 0.5,
metalness: 0.5,
})
これらの名前はアセットフォルダー内のテクスチャと一致している必要があります。
「applyMaterial」関数の最初で、0から9までのランダムな数値が生成されます。 その数値に応じて、ボールに特定のテクスチャが適用されます。
currentTexture = Math.floor(Math.random() * 10)
// Apply the texture to the object
ecs.Material.set(world, balls[currentBall], {
textureSrc: `${require(`./assets/${textures[currentTexture]}.png`)}`, roughness: 0.5,
metalness: 0.5,
})
次に「applyScaleAnimation」が呼び出されます。
function applyScaleAnimation() {
ecs.ScaleAnimation.set(world, balls[currentBall], {
autoFrom: false,
fromX: 0,
fromY: 0,
fromZ: 0,
toX: 1,
toY: 1,
toZ: 1,
duration: 500,
loop: false,
easeIn: false,
})
}
次に「applyScaleAnimation」が呼び出されます。
world.setPosition(balls[currentBall], randomxStart, 0.375, -0.5)
「applyPositionAnimation」関数が呼び出され、ボウリングボールの運動が追加されます。 「randomxStart」と「randomxEnd」の値がこの関数に渡しされます。 これは、Nianticスタジオに組み込まれたアニメーションシステムがどのように機能するかを示す典型的な例です。 アニメーションシステムを使用する利点は、物理システムのように力を適用するのではなく、速度をより細かく制御できる点です。
applyPositionAnimation(randomxStart, randomxEnd)
// Apply the position animation to the new ball
function applyPositionAnimation(xStart, xEnd) {
ecs.PositionAnimation.set(world, balls[currentBall], {
autoFrom: false,
fromX: xStart,
fromY: 0.375,
fromZ: -0.5,
toX: xEnd,
toY: 0.375,
toZ: 10,
duration: getRandomSpeed(),
loop: false,
easeIn: true,
})
}
ご覧の通り、このアニメーションの「持続時間」は「getRandomSpeed」関数を呼び出すことで定義されています。 期間はミリ秒単位で、この値が低いほどアニメーションが速くなります。 この値は、3000に設定されている「initialSpeed」変数に追加されます。 これは、アニメーションの再生時間が3000ミリ秒から3200ミリ秒の間(若干の変動を含む)になることを意味します。 この関数が呼び出されるたびに、初期速度("initialSpeed")が最大ボール速度("maximumBallSpeed")よりも大きい場合、ボール速度増加値("ballSpeedIncreaseValue")の値が引き算されます。これにより、時間の経過とともにボールの位置アニメーションが速くなり、ゲームが長引くほど難易度が上がります。 これにより、時間の経過とともにボールの位置アニメーションが速くなり、ゲームが長引くほど難易度が上がります。
function getRandomSpeed() {
newSpeed = (Math.random() * ballSpeedVariation) + initialSpeed
if (initialSpeed > maximumBallSpeed) {
initialSpeed -= ballSpeedIncreaseValue
}
return newSpeed
}
次に、「applyRotationAnimation」関数が呼び出され、この関数に「differenceInDistance」の値が渡されます。 ここでも「curveLeft」変数が使用されています。 ボールに適用されている回転アニメーションはZ軸のみに適用されています。 関数の開始時に「curveLeft」がtrueかどうかを確認します。trueの場合、「endRotation」の値を720(左曲線)に設定し、そうでない場合は-720(右曲線)に設定します。
applyRotationAnimation(differenceInDistance)
function applyRotationAnimation(differenceInDistanceValue) {
const rotationValue = 4000 - (Math.floor(differenceInDistanceValue * 500))
// this changes the end rotation based on if the ball is moving left or right let endRotation
if (curveLeft == true) {
endRotation = 720
} else {
endRotation = -720
}
ecs.RotateAnimation.set(world, balls[currentBall], {
autoFrom: false,
fromX: 0,
fromY: 0,
fromZ: 0,
toX: 0,
toY: 0,
toZ: endRotation,
shortestPath: false,
duration: rotationValue,
loop: true,
})
}
回転の速度は、アニメーションの「持続時間」値を変更することで調整されます。 関数の開始時に、「rotationValue」という変数が、4000から「differenceInDistance」の値に50を掛けた値を引いて定義されます。 これは、xの開始位置と終了位置の差が大きいほど、アニメーションの持続時間に適用される回転値が小さくなることを意味します(これにより、アニメーションがより短い時間で完了します)。 このアニメーションはループしているため、ボールが常に回転し続けます。 そうしないと、何も起こっていないように見えてしまいます。
// Increase current ball or reset
if (currentBall == 9) {
currentBall = 0
} else {
currentBall++
}
衝突検出
ゲーム中、衝突が検出されるのは2回あります。 ボールがピン後方に配置された「CollisionBox」と衝突した際。 この衝突が検出された場合、プレイヤーがボウリングのボールを回避に成功したことを意味し、ポイントが加算されるべきです。 もう1つのケースは、ボールがピンに当たると、ゲームが終了します。 確認したいオブジェクトの名前が他の何かと衝突している状態を「衝突」と呼びます。 次に、このオブジェクトをシーンに配置し、add関数の開始時に参照を作成します。
schema: {
collision: ecs.eid,
},
add: (world, component) => {
// Runs when the component is added to the world.
const {
collision,
} = component.s
すべてのボールには物理コライダーが添付されていますが、ボールが移動しているため、ここでの「リジッドボディ」は「ダイナミック」に設定されています。 「イベントのみ」チェックボックスもここでもチェックされています。
world.events.addListener(collision, ecs.physics.COLLISION_START_EVENT, handleCollision)
「add」関数内でイベントリスナーが作成されます - 「ecs.physics.COLLISION_START_EVENT」。 このコンポーネントは「collision」オブジェクトに追加され、衝突が検出されると「handleCollision」関数を呼び出します。 このプロジェクトでは、衝突するオブジェクトをトラッキングする必要はありません。
ボールとピンとの衝突処理は同じように行いますが、衝突イベントリスナーを「collision」オブジェクトに設定する代わりに、ユーザーのヘッド回転で位置を制御している「pin」オブジェクトを参照してチェックします。
world.events.addListener(pin, ecs.physics.COLLISION_START_EVENT, hitPin)
カスタマイズ
これらの値は、シーン階層で「GameController」を選択し、インスペクションパネルに移動し、その下の「GameController」スクリプト内で見つけることができます。 この値が高いほど、動きが速くなります。 この操作でピンが画面外に表示されることはありませんのでご安心ください。ピンの位置は固定されています。
初期生成頻度 - これは、前のボールが生成された後、新しいボールが生成されるまでの時間(ミリ秒単位)です。 ピンの位置は固定されています。 この値は、ゲームの難易度が上がる速度と考えることができます。 これにより、ゲームが最初から過度に困難にならないようにできます。 この数値が高いほど、スポーン時間が短縮され、最大ボールスポーン頻度に到達する速度が速くなります。 再び、この数値を比較的低く保つことがおすすめです。ゲームが急に難しくなりすぎないようにするためです。
スポーン頻度最小値 - これは、スポーン頻度の最小値として設定したい値です。 デフォルトでは500ミリ秒に設定されており、これにより各ボールの生成間隔は決して0.5秒未満にはなりません。 この値は最大難易度と考えることができます。プレイヤーのスキルを本当に試したい場合は、この値を低く設定すると、ボールがより速いペースで生成されます。
初期ボール速度 - ボールがプレイヤーに向かって移動するまでの初期時間です。 ゲームが急に難しくなりすぎないようにするためです。 デフォルト値は、体験の開始時にボールがプレイヤーに到達するまでに約3秒かかることを意味します(追加されるバリエーションによって異なります)。
ボール速度バリエーション - これは、画面上のすべてのボールが同じ速度で移動しないように、ボールの速度に適用されるランダムな値です。 デフォルト値が200の場合、ボールの速度は0から200ミリ秒速くなる可能性があります。 プレイヤーのスキルを本当に試したい場合は、この値を低く設定すると、ボールがより速いペースで生成されます。 これは、ボールがスポーンされるたびに速度が10ミリ秒ずつ増加することを意味します。 これにより、ボールの速度が上昇します。 デフォルト値は、体験の開始時にボールがプレイヤーに到達するまでに約3秒かかることを意味します(追加されるバリエーションによって異なります)。 この数値を減少させると、最大速度が速くなります。
次に進む
このテンプレートの目的は、ゲームの基本的なコアメカニクスを提供することです。 ゲームを作る一番の良いところは、好きな物語を自由に描けることだ! 値が低いほど、ボールの速度が安定します。 現在、あなたはボウリングレーンでボウリングボールを避けようとしているボウリングのピンです。しかし、あなたは山を滑り降りながら巨大な雪玉を避けようとしているスキーヤーになることも、小惑星を避けなければならない宇宙船になることもできます。 雪玉に変更すれば、回転方向を横ではなく前方に変更できるかもしれません。または、宇宙船と小惑星のアイデアがお好みなら、小惑星が生成される際にサイズをランダム化して、すべて同じサイズにならないようにすることも可能です。
最大ボール速度 - これはボールが移動できる最大速度です。 現在、この値は2000ミリ秒に設定されています。