[Web App] Witchadius(ウィッチャディウス)─ Three.js / R3Fで作った横スクロールSTGの全記録
はじめに
「Witchadius(ウィッチャディウス)」は、Three.js / React Three Fiber で構築したブラウザベースの横スクロールシューティングゲームです。
グラディウス、パロディウス、オトメディウスの系譜を意識した、ポップで可愛いダークファンタジーな世界観。
開発期間は約10日間。
全2ステージ+エンディングクレジット、ボス戦、6スロットのパワーアップシステムを一人で実装しました。
当初は5ステージを構想していましたが、開発熱と気力がここまでで限界を迎え、2ステージで完結としました。
Youtube紹介動画
設計思想 ─ 過去の資産を切り捨てない
Witchadiusは独立したゲームではなく、PROTOCOL.LAIN の世界の一部として設計されています。
ADVモードの舞台である「イレイナの部屋(アトリエ)」のベッドの向かい側の壁に設置された特大シネマモニターが、そのままゲームの盤面になります。
モニターのローカル座標系([-1.2 〜 1.2])の中で、完全にレスポンシブなスケールで駆動するため、どのような画面幅でも手触りが変わりません。
「過去の資産を切り捨てず、地続きの世界として繋ぐ」。ADVモードの部屋のGLBデータを Vanilla GLTFLoader で1回だけキャッシュロードする仕組みが、そのまま Witchadius の背景美術として100%流用されています。
LAUNCH_COMMAND: wit
魔女世界観(Witch) × シューティング(Gradiusの系譜)の融合
開発期間と経緯
開発は2026年6月中旬から約10日間。
先行して開発したSTG Protocol(全画面3Dシューティング)でWebXRの入力監視やバグ遮断のノウハウを蓄積していたこと、ADVモードで部屋のアセットやローディング基盤が完成していたことが、この短期間での開発を可能にしました。
当初の構想は5ステージでしたが、2ステージ+エンディングの時点で開発熱と気力が限界を迎え、ここで完成としました。開発終了後、ゲームプレイ動画をYouTubeに公開し、ブログ記事とnote.comのエッセイで完成を記録しています。
ステージ構成
ステージ1 ─ Night(夜)
月が浮かぶ夜空、遠景の森のシルエット、魔力の微粒子が漂うダークファンタジーの世界。
サイン波で蛇行する敵、上下から突入する編隊、地上を歩くダッカー型など、基本的な敵パターンが登場します。
中盤に鯖型ミッドボス(saba_midboss)が5方向弾幕で攻撃し、終盤のステージボスであるゴースト(ghost_boss)はワインダー弾幕で画面を制圧します。
ステージ2 ─ Dawn(夜明け)
空が紫からオレンジへ、地平線に太陽が昇り始める朝焼けの世界。
ステージ1のパターンに加えて、おたまヘリ(tadpolicopter)の楕円公転編隊、イースターエッグ(easter_egg)の円周配置弾幕、列車ボス(train)の車両ごとの時差ミサイル発射など、より複雑な敵パターンが展開されます。
ステージボスはWW1スタイルの飛行機(plane_boss)で、定位置からの誘導ミサイル投下で攻撃してきます。
敵AIパターン ─ enemyBehaviors.ts
開発初期、敵の行動ロジックはすべて WITManager.tsx に直書きされていました。
開発が進むにつれてこのファイルは2,400行を超える巨大ファイルに膨張。
これを解決するために、敵の行動パターンを enemyBehaviors.ts と enemyPatterns.ts の2層に分離しました。
enemyPatterns.ts が純粋な座標計算を担い、enemyBehaviors.ts がそれを呼び出しつつ射撃・SE再生を含む1フレーム更新を統括する構造です。
patternType をキーとしたディスパッチャが全敵タイプを振り分けます。
全敵パターン一覧
| patternType | 名前 | 挙動 |
|---|---|---|
top / bottom | 標準突入 | 右から左へ突入 → 後退 → 再ダッシュの3フェーズ |
girl_sine | リグ付き少女 | サイン波で蛇行飛行する通過型 |
pair_shooter | ペアシューター | 上下2体で登場 → 停止 → 3方向弾3連射 → 退場 |
dacker | ダッカー(アヒル型) | 地形に着地して歩行 → 停止 → プレイヤー狙撃、を繰り返す |
saba_midboss | 鯖型ミッドボス | 前進 → 停止してポーズアニメ → 5方向弾幕 → 退場 |
tadpolicopter | おたまヘリ | 3機が120度間隔で楕円公転しながらプレイヤーを狙撃 |
easter_egg | イースターエッグ | 上下2体が円周配置の10発弾幕を撃ち、高度スワップ交代 |
train | 列車ボス | 4両連結で登場 → 停車 → 各車両から時差でミサイル発射 → スクロールで退場 |
ghost_boss | ゴーストボス | ゆったりホバリングしながらワインダー弾幕(揺らぎ4レーン同時射撃) |
plane_boss | 飛行機ボス | イージングで接近 → 定位置ホバリング → 誘導ミサイル切り離し |
plane_missile | 誘導ミサイル | 飛行機ボスが投下。落下 → プレイヤー方向にロック → ブースト加速 |
ダッカーの地形着地ロジック
ダッカー型は地形オブジェクトの上に「着地」する特殊な挙動を持ちます。
enemyBehaviors.ts 内で、直下にあるBLOCK/FLOOR/GROUND/STEPを探索し、最も高い地形の上端Y座標に自身のY位置をクランプしています。
草やキノコなどの装飾オブジェクトはスルー対象として除外。
ワインダー弾幕(ghost_boss)
ゴーストボスの弾幕は、9フレームに1回のバーストで4レーンの弾を同時発射。各レーンは速度係数(speedMod)と角度オフセットが微妙に異なり、さらにサイン波(waveFreq: 6.4、waveAmp: 1.5)で射角全体が揺れるため、プレイヤーから見ると弾が「うねり」ながら迫ってくるワインダー弾幕になります。
パワーアップシステム
グラディウスのパワーアップゲージシステムを踏襲。敵を倒すとパワーカプセルがドロップし、取得するたびにゲージが1つ進む。Shiftキー(VRではAボタン)で現在選択中のスロットの能力を確定・発動します。各スロットはMAX到達で自動的に選択不可(発光遮断)になります。
| スロット | 能力 | 効果 | MAX条件 |
|---|---|---|---|
| 1. SPEED UP | 移動速度アップ | PLAYER_SPEED * (1.0 + speedLevel * 0.3) | レベル3 |
| 2. MISSILE | ミサイル | 斜め下方向への追加弾道 | レベル2 |
| 3. DOUBLE | ダブルショット | 上方向への追加弾道 | 装備済み |
| 4. LASER | レーザー | 貫通する高火力ビーム(装備するとDOUBLE解除) | 装備済み |
| 5. OPTION | オプション(随伴機) | 自機に追従し同時射撃 | 2機 |
| 6. ?(SHIELD) | シールド | HP3の被弾バリア | 展開中 |
LASERを装備するとDOUBLEが自動解除される排他仕様は、グラディウスの原作を踏襲しています。
パワーアップ発動時のShiftキーは e.repeat をインターセプトし、長押しによる誤連打を物理的にシャットアウト。1クリック1発動を保証しています。
HUD
スコアはグラディウス風に8桁ゼロパディング(00000100)。
残機は3Dの小さな魔女の帽子(円錐ジオメトリ)をループで並べて表示。
パワーアップゲージはネオン発光(#00f2fe)のセグメント式3Dパネルで、各スロットがMAXに達すると表示テキストが空文字化されて視覚的に無効化されます。
プロシージャル背景
背景は固定画像ではなく、ProceduralBackground.tsx で毎フレームCanvasに手続き的に描画し、THREE.CanvasTexture としてモニター面に投影しています。
テーマは night(ステージ1)と dawn(ステージ2)の2種。テーマオブジェクトにグラデーション停止色、天体パラメータ、雲、微粒子、森のシルエットの全情報を凝縮し、新ステージ追加時はテーマオブジェクトを1つ追加するだけの拡張設計です。
描画レイヤー構成(奥から手前):
- 空のグラデーション(リニアグラデーション、テーマごとに3〜5色)
- 天体(月:クレーター+三日月シャドウ / 太陽:光芒レイヤー+ラジアルグラデーション)
- 雲(サイン波+コサイン波の複合曲線、スクロール同期)
- 魔力微粒子(40〜60個、各パーティクルにサイン波揺れ+個別速度)
- 遠景の森シルエット(サイン波+コサイン波+樹木スパイク、スクロール同期)
テクスチャ生成
textureGenerator.ts で、床のウッドデッキテクスチャ、爆散エフェクト用の星型・ハート型アルファマップを、すべてCanvasから手続き的に生成しています。外部画像ファイルを使わず、コードだけでテクスチャを作る方針です。
音響システム ─ AudioController
AudioController.ts はシングルトンパターンで実装された音響管理クラス。THREE.AudioListener と THREE.AudioLoader を全世界で1つだけ保持し、BGMとSEを統括します。
BGMは THREE.Audio でループ再生し、同時に1曲だけ。SEはプリロードされた AudioBuffer からワンショットの THREE.Audio を生成して即座に発火。同時着弾時に重なって鳴るべきSEが正しく重畳されます。
音声アセットはIndexedDB(Dexie)にキャッシュされ、2回目以降のロードではネットワークリクエストが発生しません。
エンディングクレジット ─ 3Dスライダー式クレジットロール
全ステージをクリアすると、WITEdSequence.tsx によるエンディングクレジットシーケンスが再生されます。
このエンディングはテキストスクロールではなく、3Dモデルと2D画像のスライダー式演出です。クレジット対象のアセット(アバター、ステージオブジェクト、敵モデル、BGM作曲者、SE制作者)を1つずつ右から左へスライドインさせ、中央で4秒間停止して回転表示し、左へスライドアウト。次のアセットのGLBを先読みプリロードすることで、表示のラグを最小化しています。
クレジットデータは全てJSONファイル(Avatars.json、Enemies.json、Accessories.json、Sounds.json)から動的に生成。Set による重複排除で同一制作者が複数回表示されることを防ぎ、アセットの増減に対してメンテナンスフリーで追従します。
各クレジットアイテムのモーション状態は INCOMING → STAY → OUTGOING の3フェーズステートマシンで管理。STAYフェーズでは3Dモデルがゆっくり回転し、4秒経過またはEnterキーで次へ送られます。
最後のクレジットは二次創作ガイドラインの表示と「THANK YOU FOR PLAYING / Next wired. / Close the loop, open the world.」のメッセージで締めくくります。
VRMオプションドローン問題
パワーアップの「OPTION」として、VRMモデルを小さなドローンとして自機に追従させる構想がありました。しかし、VRMをそのままオプションとして複数インスタンス配置しようとしたところ、根本的な問題に直面しました。
失敗した試み
- ボーンクローンアプローチ — VRMのスケルトンをクローンして小型版を作ろうとしたが、VRMの内部構造が複雑すぎて正しくクローンできず
- メッシュスナップショットアプローチ — VRMのメッシュだけを抽出してスナップショットを取る方法も、マテリアルやモーフターゲットの依存関係で破綻
解決:プロシージャル魔法陣
VRMを使うことを完全に諦め、Three.jsのプリミティブジオメトリだけで魔法陣を手続き的に生成する方向に転換。リング、円、パーティクルを組み合わせた幾何学的な魔法陣が、自機に追従するオプションドローンになりました。
結果的に、VRMよりも世界観に合った見た目になり、パフォーマンスも大幅に改善。VRMの「マルチインスタンスに向かない」という根本的な特性を学んだ開発エピソードです。
VR対応 ─ デスクトップとVRのデュアル入力
Witchadiusはデスクトップブラウザだけでなく、Meta Quest(WebXR)でもプレイ可能です。
WITController.tsx 内で、キーボード入力とVRコントローラーのスティック/ボタン入力を統合。VRセッションが存在する場合は xrSession.inputSources から各コントローラーの gamepad を取得し、スティックの軸値を移動方向に、ボタン0をショットトリガーに変換しています。デッドゾーン(0.1)を設けてスティックのドリフトを防止。
パワーアップ発動は useGameControls.ts 内の tryPowerUp 関数を 'keyboard' / 'vr' のソース引数で共通化し、VRのAボタンの立ち上がり検出から呼び出す構造です。
操作プロトコル(キーマッピング)
| キー | 割り当てアクション | 実装仕様・備考 |
|---|---|---|
| W / A / S / D | 自機(魔女アバター)の上下左右移動 | スピードパワーアップ段階(0〜3速)に応じた移動速度をリアルタイムに乗算 |
| Space | ショット(魔法弾の発射) | 装備ステート(ノーマル / ダブル / レーザー)およびオプション機数に応じた弾道を同時展開 |
| Shift | パワーアップ(選択スロットの確定) | e.repeat インターセプトで長押し誤連打防止。1クリック1発動 |
| R | ゲームリセット | 全エンティティの一括パージ、残機・スコア・パワーアップ状態の完全初期化 |
| C | 俯瞰デバッグアングル(Toggle) | カメラが [2.0, 3.0, 7.0] に回り込み、モニター筐体の構造を可視化 |
| H | ヒットボックス可視化(Toggle) | 自機(緑)、敵(赤リング)、地形(青)の物理衝突判定をレントゲン表示 |
| L | クレジット情報一括ダンプ | 全アセットJSONを走査し、制作者名とURLをコンソールへ重複なし(Map構造)で出力 |
| Enter | エンディングクレジット送り | STAYフェーズのクレジットを次へスキップ |
| + / ; | モニター表示サイズ拡大 | 0.05刻み、最大2.0倍 |
| - | モニター表示サイズ縮小 | 0.05刻み、最小0.2倍 |
自機の挙動詳細
地形衝突(侵入不可クランプ)
WITController.tsx 内で、自機と地形オブジェクト(木など)のAABBリアルタイム衝突判定を実装。侵入方向を判定し、X方向またはY方向の押し戻しが大きい方にクランプする「最小侵入量軸での強制排斥」ロジック。草やキノコは装飾として当たり判定を免除しています。
ピッチ(傾き)シルエットシステム
WASD入力に応じて自機のピッチ角をリアルタイムに変化させ、移動方向に体を傾ける視覚フィードバック。目標角度へのLerp補間(dt * 8)で滑らかに遷移します。
コードアーキテクチャ
最終的なファイル構成:
src/components/PROJECT_LAIN/WIT/
├── WITManager.tsx 1054行(ゲームループ本体)
├── types.ts 90行(型定義)
├── constants.ts 70行(定数&純粋関数)
├── hooks/
│ ├── useGameAssets.ts 180行(GLB/VRM/SE/BGMロード)
│ └── useGameControls.ts 250行(キーボード&パワーアップ)
├── components/
│ ├── WITController.tsx (自機操作・VR入力統合)
│ ├── WITInstances.tsx (描画コンポーネント)
│ ├── WITEdSequence.tsx (エンディングクレジット)
│ ├── WITCreditBoard.tsx (インゲーム看板)
│ ├── PowerUpGauge.tsx (HUD)
│ └── ProceduralBackground.tsx (手続き的背景)
├── utils/
│ ├── enemyBehaviors.ts (敵AI行動1フレーム更新)
│ ├── enemyPatterns.ts (敵AI座標計算・純粋関数)
│ ├── textureGenerator.ts (Canvas テクスチャ生成)
│ └── AudioController.ts (音響シングルトン)
└── data/
├── Stage1.json, Stage2.json (ステージ定義)
├── Enemies.json (敵定義)
├── Accessories.json (地形・アイテム定義)
├── Avatars.json (アバター定義)
├── Sounds.json (BGM・SE定義)
└── SystemAssets.json (システム画像)
WITManager.tsx は2,400行から1,054行まで削減。敵AI更新は updateEnemyBehavior(enemy, ctx) の1行呼び出しに集約されています。
技術的な振り返り
patternType ディスパッチパターン
enemyBehaviors.ts のディスパッチャは、patternType を第一キー、enemyId をフォールバックとする2段階解決を行います。新しい敵を追加する手順は:①enemyPatterns.ts に座標計算の純粋関数 → ②enemyBehaviors.ts に updateXxx 関数 → ③ディスパッチャにcase追加。この3ステップが確立されたことで、敵の追加が定型作業になりました。
VRMの限界
VRMは単一のアバター表示には優れていますが、マルチインスタンスの用途には根本的に向いていません。ボーン構造、モーフターゲット、マテリアルの内部依存が複雑で、単純なクローンが効きません。
全データJSON駆動
敵パラメータ、ステージ構成、BGM/SE定義、地形配置のすべてがJSONファイルで定義されており、コードの変更なしにデータの差し替えだけでゲーム内容を調整できます。エンディングクレジットもこれらのJSONから動的生成されるため、アセットの追加削除に完全追従します。
今後の展望 ─ Wiredの円環
Witchadiusが完成したことで、次のフェーズが見えています。
- プレイ動画を
THREE.VideoTextureとして、ADVモード内の「イレイナの部屋の壁面モニター」に直接マッピング - 部屋の住人アバターに「ベッドに腰掛けてゲームを凝視する」専用アニメーションを配線
- **「自分が作ったSTGで、自分の作った世界の住人が遊んでいる」**という、過去作を一切切り捨てない完全なWiredの円環を完成させる
LAUNCH_COMMAND: wit
プロトコルに接続せよ。