推しアイデア
まるでサーファーが波を探すようにWifiという波を探し、ステージ選択を現実世界で行います! 入力方法はWiibordとmediapipe、全身を使って波に乗ります!
まるでサーファーが波を探すようにWifiという波を探し、ステージ選択を現実世界で行います! 入力方法はWiibordとmediapipe、全身を使って波に乗ります!
波というテーマに着目してネットの海でサーフィンをしたいと考えたから。 本物のネットサーフィンを体験できます!
LiveKitを使ったWebRTCリアルタイム対戦と、Wii Balance BoardのBluetooth HID通信をブラウザから直接制御し、体重移動でゲームを操作する仕組みを実装しました!
今回テーマの「ウインド・ウェーブ」のウェーブに注目し、サーフィンのようにWiibordというサーフボードでWifiの波を乗りこなします。
まずそこらへんを徘徊してWifiを探し、まるでサーファーが波を探すように乗りたいネットの波を探します。
そして、そのWifiの強度・速度・SSIDによって変化する波から落ちないようにWiibordを乗りこなし、様々なポーズをとってポイントを稼ぐゲームです。

wifiを収集するというアイデアからwifiを取得しやすいようにAndroidデバイスで周囲のssidと電波強度を取得してfirebaseに保存! これによりゲームのステージ作成のためのwifiをスマホで集めて、ゲームはWEB上で遊ぶことが可能!








MediaPipeで指定されたポーズを取ってScoreを稼ぎつつ、WiiBordでバランスを取って転覆しないように耐えながらハイスコアを目指そう!
フロントエンド
モバイル(React Native)
バックエンド
ブラウザAPI
ゲームのステージはWiFiの通信速度で決まるため、良いWiFi(波・ステージ)を探して外に出るという体験を成立させるためにReact Nativeでモバイルアプリも開発しました。
Webブラウザの Network Information API では接続中のWiFiしか測定できませんが、React Nativeでは端末のAndroid WifiManager APIを叩くことで周辺に飛んでいる全WiFiのSSIDと電波強度(RSSI)を一覧取得できます。RSSIはdBmからMbpsに変換してゲームの波パラメータに反映します。
モバイルアプリでできること
接続しているWiFiの情報をもとに、波のパラメータをリアルタイムで生成します。
電波強度(RSSI)→ 波の間隔
電波が強いほど波と波の間隔が狭くなります。間隔が狭いほど次の波への対応が素早く求められます。
SSIDの文字画数 → 波の高さパターン
SSIDを1文字ずつ画数に変換し、その配列が波ごとの高さパターンになります。
漢字については日本語の漢字情報を提供する KanjiAlive API を利用して画数を取得しています。ゲーム開始前に非同期でAPIを叩いて結果をキャッシュしておくことで、ゲーム中は追加の通信なしに即時参照できるようにしています。
変換した画数の配列が波の高さパターンとして使われます。画数の大きい文字が多いSSIDほど高低差の激しい複雑な波形に、シンプルな英数字SSIDほど単調な波形になります。
SSID: "大波-5G" 大(3画) 波(8画) -(1画) 5(2画) G(2画) → [3, 8, 1, 2, 2] → 波1が低め・波2が高め・波3が低い... と繰り返す
難易度ラベル:
< 5 Mbps → 湖のように穏やか < 20 Mbps → 穏やか < 50 Mbps → 普通 < 80 Mbps → 荒れ それ以上 → 嵐
スコアへの反映:
ステージの難易度が高いほどスコア倍率が上がります。バランスを維持し続けるだけでも倍率に応じたスコアが加算され続けるため、良いWiFiを探して挑むほど高スコアが狙えます。
PoseLandmarker で肩・肘・手首・膝などの座標を毎フレーム取得しています。
const lm = await PoseLandmarker.createFromOptions(vision, { baseOptions: { modelAssetPath: MODEL_URL, delegate: 'GPU' }, runningMode: 'VIDEO', numPoses: 1, })
const result = landmarker.detectForVideo(video, performance.now()) setPoseData(result)
この poseData.landmarks[0] を使って、ゲーム内で「両手を上げた」「敬礼した」などを判定しています。
const lm = currentPoseData.landmarks[0] const bothWristsAboveHead = lm[15].y < lm[0].y && lm[16].y < lm[0].y
1, ImageSegmenter で「この画素は人かどうか」のマスクを作る 2, 先に元のカメラ映像を canvas に描く 3, destination-in を使って、その上にマスクを重ねる 4, 人の部分だけ残り、背景は透明になる ImageSegmenter で人物マスクを作り、背景を透明にした canvas を作っています。
const seg = await ImageSegmenter.createFromOptions(vision, { baseOptions: { modelAssetPath: MODEL_URL, delegate: 'GPU' }, runningMode: 'VIDEO', outputCategoryMask: true, })
そのマスクを destination-in で元映像にかけて、「人だけ残す」処理をしています。
ctx.drawImage(video, crop.sx, crop.sy, crop.sW, crop.sH, 0, 0, canvas.width, canvas.height) ctx.globalCompositeOperation = 'destination-in' ctx.drawImage(maskCanvas, maskCrop.sx, maskCrop.sy, maskCrop.sW, maskCrop.sH, 0, 0, canvas.width, canvas.height) ctx.globalCompositeOperation = 'source-over'
Wii Balance BoardをWebHIDで実装している記事が無かったので辛かったです
通信の流れ:
ブラウザ (WebHID API) ←→ Bluetooth HID ←→ Wii Balance Board └─ 4点荷重センサー(TopLeft / TopRight / BottomLeft / BottomRight)
接続・初期化シーケンス:
navigator.hid.requestDevice({ filters: [{ vendorId: 0x057e, productId: 0x0306 }] }) → device.open() → sendReport(0x11, [0x10]) // LED 1点灯(スリープ防止) → sendReport(0x12, [0x04, 0x32]) // レポートモード 0x32(拡張コントローラ)に設定 → 10秒ごとに keepalive でレポートモードを再送(Bluetooth切断・レート低下防止)
データ受信(レポートID 0x32):
payload byte 0-1 : ボタン状態など(スキップ) payload byte 2-3 : TopRight センサー値 — getUint16(0, false) ← 16bit unsigned big-endian payload byte 4-5 : BottomRight センサー値 — getUint16(2, false) payload byte 6-7 : TopLeft センサー値 — getUint16(4, false) payload byte 8-9 : BottomLeft センサー値 — getUint16(6, false)
React Three Fiberで3Dの海を描画します。
波の頂点計算はGLSLシェーダーでGPU側に処理を移すことで軽量化しています。
人物の3Dシーンへの合成:MediaPipeをGPUモードで毎フレーム実行し、カメラ映像から人体だけを切り抜いたオフスクリーン canvas を生成しまていす。その canvas を THREE.CanvasTexture としてポリゴンに貼り付け、背景透過のまま3Dシーンに合成。毎フレーム texture.needsUpdate = true を呼んでGPUへ転送することで、カメラ映像がリアルタイムでポリゴンに反映されます。
(本来は全身を映すことでwiiboardの上に立っているように見える)
(wi-fiマークは顔隠し用に後からスクショに追加)
相手の映像は、送信側で人物だけを切り抜いてグリーンバック化し、受信側でGPUシェーダーのクロマキーで緑を抜いて、3D空間の平面に合成しています。
LiveKit(WebRTC)でリアルタイム2人対戦ができます。
Wii Balance Boardをブラウザから直接動かした点です ゲームコントローラーとしてほぼ誰も使わないWii Balance BoardをWebHID APIでブラウザから直接Bluetooth接続し、4点の荷重センサーから重心座標を計算してサーフィン操作に使っています。 WebHIDでwiibordを利用した記事が見つからなかったため、Wiimoteのリバースエンジニアリングドキュメントを参照し、受け渡しのデータの型を想定してデータの受け渡しをしました。
Naitive初めて使いました チームメンバーのうちの一人は就活で事前開発無理でした miroというオンラインホワイトボードアプリを使用してアイデアから設計までしました。 技術選定は完成度を上げるために工数が少なそうなものを選択しました(コア機能以外)
miroの使用例
画面遷移

使用技術の選定

レイアウトの構想

わしじゃよ:フロントエンド・バックエンド・モバイル(mobileでのwifi関係、wiibordのWebHID実装、WebRTCでカメラとScoreの双方向の通信確立、firebase関係、デプロイ) メギド:フロントエンド(three.js,wiibordの補正、オンライン対戦の画面製作) 伊勢田:フロントエンド(mediapipe関連のゲームのメインギミック、ゲーム画面UI、スコア計算)
mediapipeで下半身を使ったポーズの際に、正しく足の位置が判定されなかったこと。判定を緩くするなどの調整を行ったが、根本的な解決にはならなかった。
ゲーム画面が重くなる問題があった。
wiiboradが古く、センサーの値が不均一
webrtc(相手)の映像を自分と同じ3Dシーンに透過して配置
!