推しアイデア
ベイ同士がぶつかり合うと、プレイヤーのスマホが振動! 激しくぶつかりあえば、脳汁振動演出だ!!!
ベイ同士がぶつかり合うと、プレイヤーのスマホが振動! 激しくぶつかりあえば、脳汁振動演出だ!!!
ウィンド→竜巻→回転→ベイブレード ウェーブ→振動→バイブレーション スマホでシュート!激熱振動ベイバトル! 3!2!1!へいらっしゃい!
phaser3を用いた滑らかなゲーム画面 バイブレーションだけではない、音声を用いたクロスプラットフォームなハプティクス発火
爆転振動!バイブレードは、PCをスタジアム表示、スマートフォンをモーションコントローラーとして使うクロスデバイス対戦ゲームです。
「スマホを振って発射」「傾けて操作」「衝突を振動で感じる」という一連の体験を、ブラウザだけで成立させることを狙っています。
デモ動画 https://youtu.be/rKRmavGqSMs
タイトル画面
マッチング画面
ゲーム画面
リザルト画面
遊び方

IDLE -> PULLING -> LAUNCH -> COOLDOWN の状態遷移で誤発射を抑制。sendInputはエッジトリガー化し、shakePower > 0を毎フレーム送っても発射は1回だけ有効化。ringoutRiskと外向き速度の合成で決定。 バランス / パワー / ディフェンス / スタミナごとに、
GAME_STARTがGAME_STATEより先に届くケースを考慮し、モバイル側は受信順が前後しても開始状態へ遷移可能。playerIdをローカル保存し、再接続時は同一IDでjoinRoomする設計。existingPlayerIdが一致するプレイヤーを再利用してsocketIdを差し替え、プレイヤー状態を引き継ぎ。impact / launch / specialの意図ごとに扱いを分離し、パターン長から演出強度も切り替え。private emitCollisionHaptic(playerIds: string[], kind: CollisionEventPayload['kind']) { if (!this.onCollision) { return } const now = this.game.loop.time || Date.now() const filtered = playerIds.filter((playerId) => { const lastAt = this.lastCollisionHapticAt.get(playerId) ?? 0 if (now - lastAt < HAPTIC_COLLISION_COOLDOWN_MS) { return false } this.lastCollisionHapticAt.set(playerId, now) return true }) if (filtered.length > 0) { this.onCollision({ playerIds: filtered, kind }) } }
socket.on(ClientEvents.TRIGGER_VIBRATE, (data: { roomId: string; targetSocketIds?: string[]; pattern?: number[] }) => { const room = roomManager.getRoom(data.roomId) if (!room) { socket.emit(ServerEvents.ERROR, { code: 'NOT_FOUND', message: 'Room not found' }) return } // ホストのみ振動トリガー可能 if (room.hostSocketId !== socket.id) { socket.emit(ServerEvents.ERROR, { code: 'UNAUTHORIZED', message: 'Only host can trigger vibration' }) return } const pattern = data.pattern && data.pattern.length > 0 ? data.pattern : [200, 100, 200] const requestedTargets = Array.isArray(data.targetSocketIds) ? data.targetSocketIds : [] const allowedTargets = new Set(room.players.map((player) => player.socketId)) const targetSocketIds = Array.from(new Set(requestedTargets.filter((targetId) => allowedTargets.has(targetId)))) if (requestedTargets.length > 0) { if (targetSocketIds.length === 0) { socket.emit(ServerEvents.ERROR, { code: 'INVALID_TARGETS', message: 'No valid target sockets in room' }) return } targetSocketIds.forEach((targetId) => { io.to(targetId).emit(ServerEvents.VIBRATE, { pattern, timestamp: Date.now() }) }) return } io.to(data.roomId).emit(ServerEvents.VIBRATE, { pattern, timestamp: Date.now() }) })
const onVibrate = (data: { pattern?: number[] }) => { const pattern = data.pattern || [200, 100, 200] const totalDuration = pattern.reduce((acc, ms) => acc + ms, 0) const intent: FeedbackFxIntent = totalDuration >= 1000 ? 'special' : 'launch' setLastFxIntent(intent) setFxPulse((prev) => prev + 1) triggerFeedback(pattern, intent === 'special' ? 'special' : 'launch') } const onCollision = () => { setLastFxIntent('impact') setFxPulse((prev) => prev + 1) triggerFeedback([100, 50, 100], 'impact') }
この3段で、ゲームイベント(衝突/必殺)を「通信で届けて」「手で感じる」まで一気通貫で実装しています。