[Web App] LAYER_PUZZLE_PROTOCOL ─ VR対応 対戦型落ちものパズルゲーム

[Web App] LAYER_PUZZLE_PROTOCOL ─ VR対応 対戦型落ちものパズルゲーム

はじめに

「LAYER_PUZZLE_PROTOCOL」は、Three.js / React Three Fiber で構築したブラウザベースの対戦型落ちものパズルゲームです。

ぷよぷよの系譜を意識した「同色4つ以上の連結で消去+連鎖」を基盤に、serial experiments lainの世界観を重ねた「電脳空間の暗号解読(DECRYPT)」をテーマとしています。

全5ステージ、ステージごとに異なるVRMアバターのCPU対戦相手が登場し、難易度可変型AIが連鎖を組んでノイズブロックを送り込んできます。PCブラウザとMeta Quest(WebXR)の両方でプレイ可能です。

Youtube紹介動画

LAUNCH_COMMAND: pzl

serial experiments lain 世界観 × 対戦型落ちものパズル

LAYER_PUZZLE_PROTOCOL // LAUNCH_COMMAND: pzl

設計思想 ─ 暗号解読としてのパズル

LAYER_PUZZLE_PROTOCOL //  タイトル画面

STG ProtocolやWitchadiusがアクション系だったのに対し、LAYER_PUZZLEは思考系のゲームです。

「ブロックを消す」行為を「電脳空間の暗号を解読(DECRYPT)する」と読み替え、連鎖で相手に送り込むノイズブロックを「攻撃」ではなく「暗号妨害」として世界観に統合しています。

盤面は6列×12段の3Dグリッド。1P(プレイヤー)と2P(CPU)が左右に並んで同時進行する対戦形式で、先にスコアが目標値(50,000点)に達した側が勝利、盤面が溢れた側が敗北です。

VRMアバターが盤面の横に立ち、連鎖数やゲームの勝敗に応じてリアルタイムにアニメーションと表情が変化します。

ステージ構成 ─ 全5ステージ+エンディング

各ステージには固有のVRMキャラクターが対戦相手として登場し、ステージが進むほどCPUのAIレベルと落下速度が上がります。

ステージ対戦相手制作者AIレベル落下間隔(秒)
Stage 01Cat ear hairstyle桜田とまこ1(激弱)1.5
Stage 02レイチェルちゃんマユラ2(通常)1.1
Stage 03BunyaTamanoXCross3(連鎖脳)0.9
Stage 04Chen QianyuT-T4(本気)0.7
Stage 05AuralithisJustAPal5(極悪神)0.35

全5ステージを突破するとエンディングBGM(Share Your Life)に切り替わり、エンディングクレジットシーケンスが再生されます。

LAYER_PUZZLE_PROTOCOL // ステージ構成 ─ 全5ステージ+エンディング LAYER_PUZZLE_PROTOCOL // ステージ構成 ─ 全5ステージ+エンディング

ゲームメカニクス

ブロックシステム

5色の通常ブロック(マゼンタ・シアン・イエロー・パープル・グリーン)と、特殊なノイズブロック(暗灰色、トーラスジオメトリで回転表示)の計6種。

通常ブロックは同色4つ以上が縦横に連結すると消去。ノイズブロックは通常ブロックの消去に隣接していると巻き込まれて消える仕組みで、noiseChainDepth: 2 の設定により隣接ノイズの連鎖消去が2段階まで伝播します。

LAYER_PUZZLE_PROTOCOL // ステージ構成 ─ ゲームメカニクス ブロックシステム LAYER_PUZZLE_PROTOCOL // ステージ構成 ─ ゲームメカニクス ブロックシステム

連鎖とスコア

スコア計算は (消去ブロック数 × 10) × 連鎖ボーナス倍率 で算出されます。

連鎖数ボーナス倍率
1連鎖×1
2連鎖×8
3連鎖×16
4連鎖×32
5連鎖×64
6連鎖以上×128

2連鎖以上を達成すると、(連鎖数 - 1) × 3 個のノイズブロックが相手の盤面最上段に投下されます。

勝敗条件

目標スコア(50,000点)に先に到達した側が勝利。盤面の列2・段10(スポーン座標)がブロックで埋まると窒息敗北。

LAYER_PUZZLE_PROTOCOL // ステージ構成 ─ 勝敗条件

CPU AI ─ 難易度可変型・未来視思考エンジン

puzzleCpuAI.ts に実装されたCPU思考エンジンは、全配置パターン(4回転 × 6列 = 24通り)を総当たりで評価し、最高スコアの配置を選択するブルートフォース型です。

難易度別の思考パラメータ

パラメータLv.1(激弱)Lv.2(通常)Lv.3(連鎖脳)Lv.4(本気)Lv.5(極悪神)
連鎖ボーナス0010,00050,000200,000
隣接ボーナス00100300800
ノイズ消去ボーナス005,00050,000500,000
未来視(NEXT読み)
認知の揺らぎ大(±40)大(±40)微小(±0.5)微小(±0.5)微小(±0.5)

未来視カンニング(レベル4以上)

レベル4以上のAIは「現在のペア」だけでなく「NEXTペア」まで見て、2手先の連鎖シミュレーションを完全に実行します。つまり、24通り × 24通り = 576通りの配置を毎ターン評価して最適解を選ぶ「脳内ドッキング」型の思考です。

評価関数の構成

  1. 連鎖ボーナス — シミュレーション結果の連鎖数に応じて 10^連鎖数 × chainBonusBase を加算
  2. ノイズ消去ボーナス — 消去したノイズブロック数 × noiseEraseBonus
  3. 同色隣接ボーナス — 連鎖が0の場合のみ、盤面上の同色隣接ペア数をカウントしてクラスター形成を促進
  4. 地形ペナルティ — 着地高さ(Y座標)にペナルティ。低レベルほど大きな係数で「適当に低く積む」動きに
  5. 窒息回避 — 列2・段9以上にブロックが積まれる配置は -99,999,999 の即死ペナルティ
  6. 認知の揺らぎ — レベル1〜2は (Math.random() - 0.5) × 80 のノイズを加えて適度にミスさせる

ブロック結合表示 ─ calculateMatrixBounds

ぷよぷよのように、同色のブロックが隣接すると見た目が結合して1つの大きな塊に見えるシステム。

puzzleUtils.tscalculateMatrixBounds() が各ブロックの上下左右の隣接状態を判定し、BoxGeometry のサイズとオフセットを微調整することで、隣接ブロック間の境界線を消して視覚的に一体化させています。

ノイズブロック(colorId 6)と空気(colorId 0)は結合判定から除外され、常に独立した1マスとして表示されます。

VRMアバター連動 ─ PuzzleAvatar.tsx

1Pと2Pそれぞれに専用のVRMアバターが配置され、ゲームの状態に応じてリアルタイムにアニメーションと表情が変化します。

アニメーション切り替え

PuzzleAnimations.json に味方(ally)・敵(enemy)それぞれのアニメーションセットが定義されており、.vrma ファイルを @pixiv/three-vrm-animationVRMAnimationLoaderPlugin + createVRMAnimationClip() で変換して再生します。

状態アニメーショントリガー
待機(idle)ランダム4種から1つを初期選出ゲーム開始時
連鎖(chain)チェイン専用モーションchainCount >= 2
勝利(win)ジャンプ / 立ち上がりwinnerStatus === 'WIN'
敗北(lose)敗北ポーズwinnerStatus === 'LOSE'

表情(Expression)制御

VRMの expressionManager を毎フレーム更新し、勝利時は happy: 1.0、敗北時は sad: 1.0、3連鎖以上で happy: 0.8、それ以外は neutral: 1.0 をセット。


消去エフェクト ─ PuzzleEffects.tsx

ブロック消去時にトーラス型パーティクルが飛散するエフェクト。

InstancedMesh で14個のパーティクルを1つのドローコールで描画し、VR環境でのパフォーマンスを確保しています。

各パーティクルは消去されたブロックの色を継承し、emissiveIntensity: 1.2 のワイヤーフレームマテリアルでネオン発光。

0.8秒間で放射状に飛散しながらフェードアウトし、持続時間が過ぎると alive フラグで自己消滅します。


連鎖処理パイプライン

1P・2Pそれぞれに独立した非同期連鎖シーケンス(runChainSequence1P / runChainSequence2P)が実装されています。

ステップ処理待機時間
1ペアをグリッドに固定 → 重力適用180ms
24つ以上の連結探索 → 消去 → エフェクト生成500ms
3重力再適用250ms
4ステップ2に戻る(連鎖が続く限りループ)
5ノイズストックがあれば投下 → ステップ1に戻る
6窒息判定 → 次のペア生成

この sleep() ベースの非同期パイプラインにより、連鎖の各ステップが視覚的に順序立てて表示され、プレイヤーが連鎖の進行を追えるようになっています。


エンディング ─ PuzzleEndingScene.tsx

全5ステージクリア後、エンディングBGM(Share Your Life)に切り替わり、3Dエンディングシーケンスが再生されます。

演出構成

中央に大型プレゼンスクリーン(半透明の PlaneGeometry)を配置し、その上をクレジットテキストがスクロール。スクリーンの周囲に全5面のCPU対戦相手VRMアバターが立ち並び、idle アニメーションで待機しています。

クレジットデータは CONFIG.stages から5人のキャラクター名と制作者を自動展開しており、ステージ構成の変更に追従します。

スクロール完了後、「Thank you for playing. / See you in the Wired.」の感謝テキストが表示され、Enter(またはVRトリガー)の入力を待機。入力後、約1.5秒かけて暗転フェードアウトし、タイトル画面に戻ります。


入力システム ─ PuzzleController.tsx

操作プロトコル(キーマッピング)

キー割り当てアクション実装仕様
← / → / A / Dブロックペアの左右移動長押し時は0.15秒間隔でリピート移動
↓ / S高速落下0.04秒間隔で1マスずつ落下、着地で即固定
Space / W / ↑ペア回転壁際で回転不可の場合、軸ブロックをずらして補正(壁キック)
Enterゲーム開始 / 画面遷移フェーズに応じた遷移制御

VR入力

VRコントローラー割り当て
アナログスティック左右ブロックペアの左右移動(デッドゾーン0.5)
アナログスティック下高速落下
トリガー / Aボタンペア回転
トリガー(勝敗画面)リスタート / 次ステージ

すべてのVR入力は window グローバル経由で取得し、チャタリング防止フラグ(stickTriggeredX, buttonTriggeredRotate 等)で押しっぱなし連打を遮断しています。


コードアーキテクチャ

src/components/PROJECT_LAIN/PUZZLE/
├── PuzzleManager.tsx        586行(ゲームループ本体・フェーズ管理)
├── PuzzleController.tsx     195行(1P入力制御・VR入力統合)
├── PuzzleRenderers.tsx      170行(盤面・ブロック・外枠の描画)
├── PuzzleEffects.tsx         95行(パーティクル消去エフェクト)
├── PuzzleAvatar.tsx         175行(VRMアバター・アニメーション連動)
├── PuzzleEndingScene.tsx    246行(エンディングクレジットシーケンス)
├── PuzzleCreditBoard.tsx     65行(インゲームクレジット看板)
├── puzzleCpuAI.ts           140行(難易度可変型CPU思考エンジン)
├── puzzleUtils.ts           185行(重力・消去・結合判定ユーティリティ)
├── hooks/
│   └── usePuzzleAudio.ts   100行(BGM・SE管理フック)
└── data/
    ├── puzzleConfig.json         (ステージ・グリッド・ゲームプレイ定数)
    ├── puzzleAssets.json          (BGM・SE・画像アセット定義)
    └── PuzzleAnimations.json     (味方・敵VRMアニメーション定義)

全データJSON駆動

ステージ構成(対戦相手VRM・AIレベル・落下速度)、グリッドサイズ、ゲームプレイ定数(連鎖待機時間・目標スコア)、オーディオアセット、アニメーション定義のすべてがJSONファイルで外部定義されています。新しいステージの追加は puzzleConfig.jsonstages 配列に1エントリ追加するだけで完了し、エンディングクレジットにも自動反映されます。


レンダリング最適化

マテリアル共有

PuzzleRenderers.tsx では、5色+ノイズの6種類の MeshStandardMaterial をモジュールスコープでキャッシュし、全ブロックで共有。外枠のマテリアルも同様に事前生成して使い回すことで、ブロック数が増えてもマテリアル生成のオーバーヘッドが発生しません。

ジオメトリリーク防止

ChipMesh コンポーネントでは、ブロックの結合状態が変わるたびにジオメトリが再生成されますが、useMemo でサイズが同じ間はキャッシュし、コンポーネント破棄時に useEffect のクリーンアップで dispose() を呼んでWebGLメモリリークを防いでいます。

InstancedMesh エフェクト

消去エフェクトは InstancedMesh で14個のパーティクルを1ドローコールに集約。ジオメトリ(TorusGeometry)もモジュールスコープで1つだけ生成し、全エフェクトインスタンスで共有しています。


音響システム ─ usePuzzleAudio.ts

BGMとSEは puzzleAssets.json で定義され、IndexedDB(Dexie)にキャッシュ。各アセットに個別の volume 値が設定されており、bgmMasterVolume / seMasterVolume のマスターボリュームとの積算で最終音量が決まります。

連鎖SEは段階的に変化し、2連鎖で se_pzl_chain1、3連鎖で se_pzl_chain2 と上がっていき、6連鎖以上は se_pzl_chain5 で固定。連鎖の興奮を聴覚的にも伝える設計です。


技術的な振り返り

連鎖シーケンスの非同期設計

runChainSequence1P / 2Pasync/await + sleep() で書かれた非同期関数で、各ステップの間に明示的な待機時間を挟んでいます。Reactの setState は即座に反映されるわけではないため、setTimeout(() => runChainSequence(...), 0) でイベントループに戻してからReactの再レンダリングを挟む設計としています。

ノイズブロックの連鎖消去

通常のぷよぷよではおじゃまブロックは隣接する通常ブロックの消去時に1個ずつ消えますが、LAYER_PUZZLEではノイズブロック同士の隣接伝播が noiseChainDepth で制御される連鎖消去方式です。これにより、大量のノイズブロックが一気に消える爽快感を生んでいます。

VRMのマルチインスタンス

1Pと2Pで異なるVRMモデルを同時にロード・アニメーションする必要がありますが、各 PuzzleAvatar が独立した useWiredVRM フックと AnimationMixer を持つことで、モデル間の干渉を防いでいます。エンディングシーンでは5体のVRMが同時表示されますが、各アバターが独自のミキサーでidle アニメーションを個別に再生する構造です。

React Reconciliation と Three.js の衝突

ブロックの key${x}-${y}-${colorId} を含めることで、同じ座標にブロックが置き換わった場合にReactが正しくDOMノード(Three.jsメッシュ)を再生成するようにしています。colorIdkey に含めないと、前のブロックのジオメトリが残り続けるバグが発生していました。


LAUNCH_COMMAND: pzl プロトコルに接続せよ。