寿司 ビーツ ドロップ
頭を動かして、リズムに合わせて落ちてくる寿司を食べよう! ポイントを獲得し、コンボを組み立て、特別な寿司を捕まえて報酬を手に入れよう!楽しいで夢中になれる体験!

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

Head Touch Input
This project demonstrates an input system for a game that utilizes head tilt controls and touchscreen controls to trigger events.
View sample project
Object Cloning and Detection
Cloning existing scene objects multiple times and making them fall. Using colliders and events to detect when sushi touches a plate.
View sample projectBehind the Build: Sushi Beat Drop

Written by Saul Pena Gamero & Mackenzie Li
May 8, 2025
Summary
過去2ヶ月間、当社のチームは、伝統的なリズムゲームプレイと革新的な拡張現実(AR)要素を融合させたリズムゲームを開発してきました。 クラシックなゲームメカニクスとAR技術の融合は、当社ゲームの特徴となる唯一無二で没入感のある体験を生み出しています。 私たちの主な目標は、楽しく没入感のあるゲームを設計し、プレイヤーがリズムの挑戦を全く新しい形で楽しむことを促すことでした。
このプロジェクトは、前回のNianticコンテストで始めた取り組みを基盤に、AR顔フィルターの一風変わった応用を追求したものです。 私たちのインスピレーションは、Niantic Studioのサンプルプロジェクトから得たもので、ARにおけるヘッドムーブメントのメカニクスを展示したものでした。 私たちは、このコンセプトがどれほどインタラクティブでエンターテインメント性が高いかに魅了され、これをゲームの中心的なメカニクスとして採用することを決定しました。
このメカニクスの最適なゲームプレイスタイルを決定するため、私たちはアイデアを出し合い、Taiko no TatsujinやOsuのようなリズムゲームからインスピレーションを得ました。 ジャンル愛好家として、私たちは単にタイミングとリズムだけでなく、頭部の動きを通じた物理的なインタラクションを組み込んだゲームを描いてきました。 これらの要素を融合させることで、私たちは独自のゲームプレイと身体的な没入感を特徴とした、他に類を見ないリズムゲーム体験を提供するゲームを生み出しました。
Gameplay
概要
当社のリズムゲームは、シンプルながら魅力的なコンセプトを軸にしています:画面の上部から音楽のリズムに合わせて寿司が落ちてきます。 プレイヤーの目標は、下の皿に落ちる瞬間に寿司をキャッチすることです。 画面の左右の
側に2枚のプレートが配置されており、音楽のリズムに合わせて寿司のピースが各プレートに向かって落ちてきます。 寿司が皿と一致した瞬間、プレイヤーは「キャッチ」する動作を行い、正確なタイミングでポイントを獲得します。
コントロールとユーザー入力
プレイヤーは、柔軟性を提供し、全体的な体験を向上させるために設計された2つの異なるコントロール方法を使用してゲームとインタラクションできます。 主な操作方法は、ヘッドティルト(頭部の傾け)を使用したハンズフリーで没入感のある操作方法です。 寿司が左の皿に向かって落ちている場合、プレイヤーは左に頭を傾けてキャッチします。右の皿の場合も同様で、右に頭を傾けます。 このコントロール方式は、拡張現実(AR)のメカニクスを活用して、物理的な操作感と楽しさを追加しています。
長時間のプレイで頭部の傾けが困難または不快に感じるプレイヤーに対応するため、タップでプレイ可能なオプションも実装しています。 このモードでは、プレイヤーは画面の左側または右側をタップして、対応するお皿に寿司をキャッチできます。 この代替案は、ゲームが楽しくプレイしやすく、多様な好みやプレイスタイルに対応できるように設計されています。
リズムベースのゲームプレイと直感的でカスタマイズ可能なコントロールを組み合わせることで、幅広い層にアピールする楽しいインタラクティブな体験を提供します。
Project Structure
3Dシーン
当社のゲームシーンには、スクリプト、3Dアセット、顔追跡システム、カメラ、照明など、複数の重要なコンポーネントが含まれています。 これらのうち、カメラや照明などは比較的直感的に理解できるものもありますが、本セクションでは3Dアセットとフェイストラッカーに焦点を当てて説明します。
フェイストラッカーは、プレイヤーの頭の位置と動きを検出する上で重要な役割を果たします。 この機能により、ゲームは頭の傾きの方向とタイミングを判定し、ゲームプレイメカニクスとの正確なインタラクションを可能にします。
3Dアセットには、2枚のプレートと寿司のピースが含まれており、いずれもリズムゲーム体験に不可欠な要素です。 プレートは静的オブジェクトであり、それぞれに物理コライダーと静的リジッドボディコンポーネントが装備されています。 各プレートには、寿司が乗った際にスコアリング処理を扱うスクリプト「ScoreArea」が添付されています。
一方、寿司のピースは動的なオブジェクトです。 各寿司には物理コライダーと動的リジッドボディが設定されており、落下する際に環境と自然に相互作用します。 さらに、各寿司のピースには、その動作とゲームプレイへの統合を制御する「Sushi スクリプト」が関連付けられています。
スクリプトの具体的な機能については後述しますが、これらのコンポーネントが総合的に、当リズムゲームのインタラクティブで没入感のある体験を構築しています。
アセット
当ゲーム内のすべてのアセットは、手描きまたはカスタム作成されたもので、プロジェクトに注がれた芸術的な努力と細部へのこだわりが反映されています。 3DモデルはBlenderを使用して作成され、ゲームに独自の個性と独自性を加えています。アセットには、カバーページ、プレイヤーをガイドする説明用GIF、ガチャシステムのアニメーションなど、多様なビジュアルとインタラクティブな要素が含まれています。 さらに、プレイヤーの達成を祝うための祝賀ページを作成し、ゲームプレイの主要な要素として機能する2つの可愛い猫の寿司3Dモデルも作成しました。
スクリプト
GameManager:- GameManagerは、ゲームの状態移行を管理し、開始、レベル選択、チュートリアル、ゲーム内、報酬、終了画面などの処理を担当します。 状態マシンは、各状態ごとにUI要素、背景、イベントリスナーを動的に管理し、ゲーム全体を通じて滑らかな移行とユーザーインタラクションを保証します。
- 次の2つの画像は、定義されている状態マシンから異なる状態の例を示しています。
- 注目すべきポイントとして、他のイベントに移行する予定のイベント、および各状態がページから独自のHTML UIを作成し削除する方法があります。
ecs.registerComponent({
name: 'gameManager',
stateMachine: ({ world, eid }) => {
let startButton = null
let levelButtons = []
let endButton = null
// Function to clean up the start button
const removeStartButton = () => {}
// Function to clean up level selection buttons
const removeLevelButtons = () => {}
// Function to clean up the end button
const removeEndButton = () => {}
ecs.defineState('startGame')
.initial()
.onEvent('interact', 'levelSelection', { target: world.events.globalId })
.onEnter(() => {
if (activeGameManagerEid !== null && activeGameManagerEid !== eid) {
return
}
activeGameManagerEid = eid
// Create the background and image
createBackground(world)
})
}
})
ecs.defineState('levelSelection')
.onEvent('levelSelected2', 'inGame', { target: world.events.globalId })
.onEvent('showTutorial', 'tutorial', { target: world.events.globalId })
.onEnter(() => {
const levels = [
{ label: 'Slow', event: 'gameStartedSlow' },
{ label: 'Mid', event: 'gameStartedMid' },
{ label: 'Fast', event: 'gameStartedFast' },
]
})
顔追跡:
- 顔追跡コンポーネントは、頭部の動きと画面のタッチに基づいてスコアとコンボの更新を管理し、イベントをトリガーし、UIを動的に更新します。 ヘッドの回転は、ティックごとにヘッドのZ軸の位置を追跡することで処理しています。 回転に応じて、異なるチェックを実施します。 また、チェックを再度行う前に、頭が中立位置に戻ることを期待しています。 これにより、ユーザーがゲーム中ずっと頭を横に向けたままにすることを防ぎます。
tick: (world, component) => {
const { touchTimerLeft, touchTimerRight } = component.data
const rotation = ecs.quaternion.get(world, component.eid)
if (component.data.touchTriggeredLeft && world.time.elapsed > touchTimerLeft) {
component.data.touchTriggeredLeft = false
}
if (component.data.touchTriggeredRight && world.time.elapsed > touchTimerRight) {
component.data.touchTriggeredRight = false
}
if (rotation) {
const z = rotation.z
// Handle right-side logic
if (z > 0.20) {
component.data.hitLeft = false
if (!component.data.hitRight) {
component.data.hitRight = true
component.data.canHitRight = true
}
if (component.data.hitRight && component.data.canHitRight) {
handleRightSide(world, component)
}
} else if (z < -0.20) {
// Handle left-side logic
component.data.hitRight = false
if (!component.data.hitLeft) {
component.data.hitLeft = true
component.data.canHitLeft = true
}
if (component.data.hitLeft && component.data.canHitLeft) {
handleLeftSide(world, component)
}
} else {
// Reset state when head returns to neutral
resetHeadState(component)
}
}
}
- また、プレイヤーが指で画面に触れているかどうかを監視しています。 ユーザーが画面の左側または右側をタップしたかどうかによって、グローバルイベントをトリガーしてイベントを発火します。 ユーザーが指を離す必要があるようにタイマーを実装します。 これは、ユーザーが中立的な状態に戻るために必要です。
// Dispatch global events on touch
world.events.addListener(world.events.globalId, ecs.input.SCREEN_TOUCH_START, (event) => {
const touchX = event.data?.position?.x
if (touchX < 0.5) {
world.events.dispatch(eid, 'touchLeft')
} else {
world.events.dispatch(world.events.globalId, 'touchRight')
}
})
// Listen for global touch events within this component
world.events.addListener(eid, 'touchLeft', () => {
const data = dataAttribute.cursor(eid)
data.touchTriggeredLeft = true
data.touchTimerLeft = world.time.elapsed + 1000
})
world.events.addListener(world.events.globalId, 'touchRight', () => {
const data = dataAttribute.cursor(eid)
data.touchTriggeredRight = true
data.touchTimerRight = world.time.elapsed + 1000
})
ObjectSpawner:
- ObjectSpawner コンポーネントは、事前に定義されたタイムスタンプで寿司オブジェクトの生成を処理し、異なる速度のオーディオトラックと同期します。 ランダムにスポーン位置と寿司の種類を決定し、異なるゲームモード用のイベントリスナーを設定し、オーディオ再生を管理して、グローバルな
イベントによって駆動されるダイナミックなゲームプレイ体験を提供します。 - 寿司をスポーンするために、シーン内に両方のタイプの参照があり、既に適切なコンポーネントが設定されています。 以下のコードを使用して、それらを複製し、その後下に配置します。
- 新しいエンティティ(ターゲットエンティティ)を作成し、ソースからすべてのコンポーネントをコピーします。
function spawnSushi(world, objectToSpawn, objectToSpawnSuper, spawnY, spawnZ, timeStamps) {
if (currentTimestampIndex >= timeStamps.length)
return
const sushiType = randomizeSushi()
const newEid = world.createEntity()
const spawnX = randomizeSpawnLocation()
const clonedSuccessfully = sushiType === "regular"
? cloneComponents(objectToSpawn, newEid, world)
: cloneComponents(objectToSpawnSuper, newEid, world)
if (!clonedSuccessfully) {
world.deleteEntity(newEid)
return
}
}
// Clone components from the source to the target entity
const cloneComponents = (sourceEid, targetEid, world) => {
const componentsToClone = [
Position, Quaternion, Scale, Shadow, BoxGeometry, Material,
ecs.PositionAnimation, ecs.RotateAnimation, ecs.GltfModel,
ecs.Collider, ecs.Audio, Sushi
]
let clonedAnyComponent = false
componentsToClone.forEach((component) => {
if (component && component.has(world, sourceEid)) {
const properties = component.get(world, sourceEid)
component.set(world, targetEid, { ...properties })
clonedAnyComponent = true
}
})
return clonedAnyComponent
}
- 私たちは、寿司をいつ生成するかを判断するためにタイムスタンプシステムを使用しています。 各曲には、次のように使用される独自の
タイムスタンプ配列が用意されています:
currentTimestampIndex++
if (currentTimestampIndex < timeStamps.length) {
const delay = (timeStamps[currentTimestampIndex] - timeStamps[currentTimestampIndex - 1]) * 1000
setTimeout(() => spawnSushi(world, objectToSpawn, objectToSpawnSuper, spawnY, spawnZ, timeStamps), delay)
}
- 以下のタイムスタンプの例です:
// Slow - One
const timeStampsSlow = [
0.22, 1.31, 2.5, 3.8, 5.08, 6.37, 7.66, 8.95, 10.23, 11.53, 12.76, 14.1, 15.39,
16.68, 17.97, 19.26, 20.55, 21.85, 23.14, 24.43, 25.72, 27.01, 28.3, 29.59,
30.88, 32.16, 33.45, 34.74, 36.03, 37.32, 38.61, 39.9, 41.19, 42.49, 43.79,
45.07, 46.36, 47.65, 48.94, 50.23, 51.52, 52.79, 54.1, 55.39, 56.68, 57.97,
59.26, 60.55, 61.85, 63.14, 64.43, 65.72, 67.01, 68.3, 69.59, 70.88, 72.17,
73.66, 75.01, 76.18, 77.59, 78.7, 79.93, 81.19, 82.49, 83.78, 85.07, 86.36,
87.65, 88.94, 90.23, 91.52, 92.81, 94.1, 95.39, 96.68, 97.97, 99.26, 100.55,
101.83, 103.14, 104.43, 105.72, 107.01, 108.3, 109.59, 110.88, 112.17, 115.52
]
スコアエリア:
● スコアエリア コンポーネントは、寿司エンティティとの衝突を検出し、寿司の種類に応じてスコアを更新し、エリア内の寿司の状態を管理します。 寿司のエンティティを収集または削除した際に破壊し、スコア領域のビジュアルフィードバックを動的に更新してその状態を反映します。
● 寿司がスコア領域に入った際のスコアリングロジック、ビジュアル更新、およびインタラクション管理の処理方法は次のとおりです:
const handleCollisionStart = (e) => {
if (Sushi.has(world, e.data.other)) {
const areadata = component.schemaAttribute.get(eid)
areadata.hasSushi = true
areadata.sushiEntity = e.data.other
ecs.Material.set(world, eid, { r: 225, g: 225, b: 0 })
const rightScoreAreaData = Sushi.get(world, areadata.sushiEntity)
if (rightScoreAreaData.type === "regular") {
areadata.score = Math.floor(Math.random() * 3) + 1
} else if (rightScoreAreaData.type === "super") {
areadata.score = 10
} else {
areadata.score = -1
}
}
}
寿司:
● Sushi コンポーネントは、移動速度、タイプ、状態(移動中または静止中)などの属性を持つ寿司エンティティを定義します。 そのシステムは、寿司の配置更新を管理し、画面外に移動した寿司を自動的に削除し、アクティブな寿司に対してタイマーによる破壊メカニズムを実装することで、効率的なゲームプレイのダイナミクスを実現します。
● 寿司のエンティティがゲーム世界で正しく移動し、画面外に移動した際に適切に削除されることを確認します。
if (ecs.Position.has(world, eid)) {
const currentPosition = ecs.Position.get(world, eid)
if (currentPosition) {
currentPosition.y -= sushiData.speed
if (currentPosition.y < -5.0) {
// console.log(`Sushi (${eid}) went off-screen, deleting entity.`)
world.deleteEntity(eid)
} else {
ecs.Position.set(world, eid, currentPosition)
}
}
}
UIRewardController:
● UIRewardController コンポーネントは、報酬表示システムを管理し、S、SS、SSS などの報酬を対応する GIF とポイントと共にアニメーション表示し、
で表示します。
は、UIを動的に更新し、イベントベースの報酬追加を処理し、
の報酬シーケンスをトリガーし、魅力的なポストゲーム体験を確保します。
の報酬:
● 報酬コンポーネントは報酬システムを管理し、
Combo3のようなイベントでポップアップを表示し、Combo10の報酬をトリガーします。これには、
でランダムな報酬(S、SS、またはSSS)を含むメガボーナスガチャが含まれます。 報酬データを追跡し、グローバルイベントを監視し、
を通じて視覚的およびイベント駆動型の報酬でプレイヤーに魅力的なフィードバックを提供します。
SushiKiller:
● SushiKiller コンポーネントは、寿司エンティティとの衝突を検出し、衝突が発生した場合にグローバルな
comboResetイベントを送信します。 このシステムは、寿司の在庫状況と「
」の収集状態を追跡するための状態データを管理し、プレイヤーの操作に応じてゲームメカニクスが適切に更新されるようにします。
Implementation
ゲーム状態(ステートマシン):
ゲームは、それぞれが
のゲームプレイ体験の特定の部分を制御する独立したゲーム状態によって管理されています。 各状態は独自のUI要素を管理し、必要に応じて
などの他のコンポーネントにイベントをディスパッチします。

❖ ゲーム開始
遷移先: レベル選択
説明: プレイヤーが冒険を始める最初の画面。
UI 要素:背景と、レベル選択画面(
)へ移行するボタン。
❖ レベル選択
移行先: チュートリアル、メインゲームプレイ
説明: プレイヤーは3つの曲から選択でき、各曲には
特定の難易度レベルが設定されています。
UI要素:背景、利用可能な曲を表す3つのボタン、および
チュートリアル状態へ移行する単一のボタン。
❖ チュートリアル
移行先: レベル選択
説明:この状態では、プレイヤーがゲームとインタラクトする方法を、
2つのメカニクスを通じて教えます:
■ 頭を左右に動かして、皿に落ちる寿司をキャッチする。
■ 皿に寿司が触れた際に画面をタップする。
アニメーションGIFがこれらの動作をデモし、ボタンでプレイヤーが
レベル選択画面に戻ることができます。
➢ UI要素:ゲームプレイメカニクスを示すアニメーションGIFと、
レベル選択画面に戻るボタン。
➢ メインゲームプレイ
移行先: 報酬画面
説明: プレイヤーがゲームとインタラクトするコアゲームプレイセクション:
● プレイヤーは画面上に寿司として表示され、音楽のリズムに合わせて寿司が落ちてきます。
● 目標は寿司を収集してスコアとコンボカウンターを増加させることです。
● より高いコンボはガチャシステムでより良い報酬を付与します。
UI要素:ライブプレイヤーフィード、落下する寿司のビジュアル、スコアカウンター、および
コンボトラッカー。
➢ 報酬画面
移行先: エンディング画面
説明: 曲をクリアすると、プレイヤーは
でのパフォーマンスに基づいて報酬を受け取ります:
● 画面には獲得したガチャのアニメーションが表示されます。
● プレイヤーは最終スコアと、
セッションからのスコア増加を確認できます。
● アニメーションが終了すると、ゲームはエンディング画面
に移行します。
UI要素:報酬アニメーション、スコア要約、および
End Game Screenへの移行トリガー。
➢ End Game Screen
移行先: Level Selection
説明: ゲームセッションの最終画面で、プレイヤーにプレイを感謝します。
● プレイヤーは、
Level Selection画面に戻って別のラウンドを開始するように促されます。
UI要素: 感謝のメッセージとLevel
Selection画面に戻るボタン。