リアルタイムの姿勢検出(MediaPipe)とThree.js 3Dトロッコアニメーションを組み合わせた、観客参加型クイズアプリ。
App.jsx(状態管理・ゲームロジック)
├── Start.jsx / ルート
├── Selector.jsx /quiz(難易度未選択時)
├── QuizPage.jsx /quiz(難易度選択済み)レイアウトシェル
│ ├── QuizBoard.jsx 上段:問題・選択肢・タイマー表示
│ ├── WhitchNarrator.jsx 左列:魔女キャラクター
│ ├── ARPanel.jsx 中央:タイマー/votes ロジック + ARラッパー
│ │ └── AR.jsx 3Dトロッコ + カメラ(実装済み)
│ │ └── PoseDetector.jsx MediaPipe 姿勢検出(実装済み)
│ └── BatteryMeter.jsx 右列:バッテリー残量
└── Finish.jsx /finish
ゲーム全体の状態管理・ルーティング。useHint() 実装済み(battery −10、isHintVisible = true)。
3列レイアウトのシェル。ロジックなし。
- TODO: ARPanel から
timeLeftを受け取り QuizBoard へ渡す(onTimeChangeコールバックを ARPanel に追加して連携)
上段の問題UI(問題文・選択肢ボタン・ヒントボタン・タイマー表示)。
- TODO: タイマー(
timeLeft)の表示スタイル(残り3秒以下で強調など) - TODO: 選択肢ボタンのホバー・押下スタイル
- TODO: ヒントボタンの disabled スタイル
- TODO: 問題文・ヒントテキストのスタイル
タイマー・votes・timeoutLabel のロジックと ARScene のラッパー。
- TODO:
timeLeftstate とカウントダウンを実装する - TODO:
currentDataが変わったらタイマーをリセットする - TODO: 時間切れ時に
latestVotesRefの多数決でonAnswerを呼ぶ - TODO:
handleVotesChangeコールバックを実装して ARScene のonVotesChangeに渡す - TODO:
timeoutLabel('左'|'右'|null)を state 管理して ARScene に渡す - TODO:
pendingBranchが null になったらtimeoutLabelをクリアする - TODO:
onTimeChange(timeLeft)を外部に公開して QuizBoard と連携する
Three.js 3Dトロッコシーン・カメラ制御。実装済み。
MediaPipe PoseLandmarker による姿勢検出・左右分布・手上げ検知。実装済み。
全員が手を上げると、hintText prop の内容をARオーバーレイに表示し、onHandRaised() を呼ぶ。
バッテリー残量の縦棒グラフ表示。
左列の魔女キャラクター+セリフバブル。
- TODO: セリフバブル・キャラクターのスタイルを実装する
難易度選択画面(easy / hard)。実装済み。
スタート・フィニッシュ画面。実装済み。
{
id: number,
text: string, // 問題文
choices: [string, string], // [左の選択肢, 右の選択肢]
currentDirection: 'left' | 'right', // 正解の方向
hint: string,
difficulty: 'easy' | 'hard',
}choices[0] = 左分岐、choices[1] = 右分岐 は固定。
正解かどうかは currentDirection と選ばれた方向を比較して判定する。
ユーザーが choices[0] を選択 → branchDir = 'left'
ユーザーが choices[1] を選択 → branchDir = 'right'
isCorrect = (branchDir === question.currentDirection)
| prop | 型 | 説明 |
|---|---|---|
battery |
number |
バッテリー残量(0〜100) |
currentData |
Question |
現在の問題オブジェクト |
onAnswer(choice, navigate) |
fn |
選択肢を選んだ時に呼ぶ |
onUseHint() |
fn |
ヒントボタン押下時(battery −10) |
isHintVisible |
boolean |
ヒント表示フラグ |
pendingBranch |
'left'|'right'|null |
アニメーション待ちの分岐方向 |
onBranchComplete() |
fn |
アニメーション完了後に呼ばれる |
questionIndex |
number |
現在の問題番号(1始まり) |
totalQuestions |
number |
総問題数 |
narrationLines |
string[] |
魔女のセリフ一覧 |
| prop | 型 | 説明 |
|---|---|---|
pendingBranch |
'left'|'right'|null |
分岐アニメーション起動指示 |
onBranchComplete() |
fn |
アニメーション完了コールバック |
onHandRaised() |
fn |
両手を上げたらヒント表示へ中継 |
onVotesChange({left, right}) |
fn |
最新の票数をQuizPageへ伝える |
timeoutLabel |
'左'|'右'|null |
タイムアウト時に表示する方向文字 |
hintText |
string |
現在の問題のヒント本文 |
| prop | 型 | 説明 |
|---|---|---|
onVotes({left, right}) |
fn |
人体の左右分布を毎フレーム通知 |
onHandRaised() |
fn |
全員が手を上げた瞬間に呼ばれる |
hintText |
string |
ARオーバーレイに表示するヒント本文 |
| state | 型 | 説明 |
|---|---|---|
battery |
number |
バッテリー残量(0〜100) |
currentIndex |
number |
現在の問題インデックス |
difficulty |
'easy'|'hard' |
選択中の難易度 |
status |
'playing'|'success'|'failed' |
ゲーム結果 |
scene |
'start'|'quiz'|'finish' |
現在のシーン |
isHintVisible |
boolean |
ヒント表示中か |
isDifficultySelected |
boolean |
Selector を通過したか |
showSelector |
boolean |
Selector を表示するか |
pendingBranch |
'left'|'right'|null |
アニメーション待ちの分岐方向 |
| ref | 説明 |
|---|---|
pendingNavigateRef |
アニメーション完了後に実行する { navigate, fn } を保持 |
1. ユーザーがボタン押下 or タイムアウト
↓
2. QuizPage: onAnswer(choice, navigate) 呼び出し
↓
3. App: handleAnswer
- branchDir = choice === choices[0] ? 'left' : 'right'
- isCorrect = branchDir === currentDirection
- setPendingBranch(branchDir) ← ARアニメーション起動
- pendingNavigateRef.current = { navigate, fn: ... } ← 後処理を保存
↓
4. AR.jsx: pendingBranch を検知してトロッコ分岐アニメーション開始
- branch フェーズ(3秒)→ return-arc フェーズ(5秒)→ return-straight フェーズ(2.5秒)
↓
5. return-straight 完了時: onBranchComplete() 呼び出し
↓
6. App: handleBranchComplete
- battery −5(問題1問ごとのエネルギー消費)
- pendingNavigateRef.current.fn(navigate) を実行
- 正解: battery加算、次の問題へ or /finish(success)
- 不正解: /finish(failed)
- setPendingBranch(null)
10秒経過した時点で、PoseDetector が検出している人体の左右分布(票数)を多数決で分岐方向を決定する。
latestVotesRef.current = { left: N, right: M } ← AR から毎フレーム更新
タイムアウト時:
goLeft = left >= right
timedOutChoice = goLeft ? choices[0] : choices[1]
setTimeoutLabel(goLeft ? '左' : '右') ← ARに大きな文字で表示
onAnswer(timedOutChoice, navigate) ← 通常の回答フローへ
| イベント | 変化 | 実装場所 |
|---|---|---|
| 正解(easy) | +5 | App.jsx handleAnswer |
| 正解(hard) | +20 | App.jsx handleAnswer |
| 問題完了(トロッコ1周) | −5 | App.jsx handleBranchComplete |
| ヒント使用(手を上げる) | −10 | App.jsx useHint |
PoseDetector.jsx allArmsRaised 検知
↓ ① ARオーバーレイに「ヒント」文字を表示(TODO: ヒント内容も表示する)
↓ ② onHandRaised() 呼び出し
AR.jsx prop を中継
↓ onHandRaised
ARPanel.jsx onUseHint として ARScene へ渡す
↓ onUseHint → useHint() in App.jsx
App.jsx battery −10 / isHintVisible = true
↓ isHintVisible = true
QuizBoard.jsx 問題文エリアにヒントテキストを表示
PoseDetector.jsx のオーバーレイ(allArmsRaised && ...)では現在「ヒント」という文字しか表示していない。
以下のいずれかの方法でヒント内容を表示する実装が必要:
| 方法 | 概要 | メリット / デメリット |
|---|---|---|
| A: prop で受け取る(シンプル) | 親から hintText prop を渡し、オーバーレイ内に表示する |
実装が最も簡単。prop チェーン(QuizPage → ARPanel → AR → PoseDetector)が伸びる |
| B: onHandRaised の戻り値を使う | onHandRaised() がヒント文字列を返し、PoseDetector 側で state に保持する |
prop チェーンを増やさずに済む。コールバックが値を返す設計はやや珍しい |
| C: Context で共有する | React Context にヒントテキストを載せ、PoseDetector が直接購読する | prop を一切増やさない。Context の設計コストがかかる |
現状の呼び出しチェーンを最小限に変えるなら A、PoseDetector の独立性を保ちたいなら C が適している。
トロッコが分岐先から戻ってくる return-arc フェーズ(5秒間)の間、AR シーンに正誤結果を全画面オーバーレイで表示する。
| フェーズ | オーバーレイ |
|---|---|
| branch(3秒) | 非表示(投票中) |
| return-arc(5秒) | 表示 |
| return-straight(2.5秒)以降 | 非表示 |
| 条件 | テキスト | 色 |
|---|---|---|
pendingBranch === correctBranch |
正解! | 緑 #22c55e |
pendingBranch !== correctBranch |
不正解... | 赤 #ef4444 |
App.jsx handleAnswer で setCorrectBranch(currentDirection)
↓ correctBranch: 'left' | 'right' | null
QuizPage.jsx
↓
ARPanel.jsx
↓
AR.jsx(DevTrolleyPlayScreen) → 正誤オーバーレイ表示
rAFループはuseEffect([], [])のクロージャ内で動くため、通常の state は古い値しか読めない。resultVisibleRef(ref)でループ内判定し、setResultVisible(state)でレンダリングをトリガーする二重管理方式を採用。既存のvotesRef+votesDisplayパターンと同一。- zIndex: 150(投票バッジ 100 より上、タイムアウトラベル 200 より下)
| ライブラリ | 用途 |
|---|---|
| React 19 | UIフレームワーク |
| React Router DOM v7 | ページ遷移(/, /quiz, /finish) |
| Three.js + @react-three/fiber | 3Dトロッコ・レール描画 |
| @mediapipe/tasks-vision | リアルタイム姿勢検出(PoseLandmarker) |
| Vite | ビルドツール |