はじめに
Claude Code(AnthropicのAIコーディングエージェント)との対話だけで、ソウルライク風のボス戦特化3Dゲーム「HALL OF KINGS」を作ってみたので、その制作記録です。モデルにはClaude 5ファミリーの Claude Fable 5 を使用しています。
特徴は次の2点です。
- index.html をダブルクリックするだけで起動する(サーバー不要・ビルド不要・スマホ対応)
- コーディングだけでなく、表示確認などのテストもAIに任せた
最初は線だけのワイヤーフレームだったキャラクターが、最終的にはプロのモーションで動く「フロム風」の見た目になるまでの変遷と、その過程でAIと一緒に越えた技術的な壁・数々の失敗談をまとめます。
なお、本記事自体もほぼすべてClaude(Claude Code + Claude Fable 5)との対話で作成しています。
対象読者
- AIエージェント(Claude Code等)を使った開発の実際の進み方・つまずき方に興味がある方
- Three.js でブラウザゲームを作ってみたい方
- 「サーバーを立てずに配れるWebゲーム」の作り方に興味がある方
ゲーム開発・3Dの前提知識は不要です。筆者自身、2D/3D問わずゲーム制作は初めてでした。
使用環境
| 項目 | 内容 |
|---|---|
| 開発 | Claude Code(VSCode拡張)+ Claude Fable 5 |
| 描画 | Three.js r147(クラシック形式・同梱) |
| 3D素材 | Mixamo(Adobe)のモデル・アニメーション |
| 変換ツール | FBX2glTF + 自作Pythonスクリプト |
| 動作環境 | iOS 16.4+ / モダンブラウザ(非対応環境はプロシージャルキャラに自動フォールバック) |
| コード規模 | index.html 約2,100行 + boss-data.js 約270行 + モデルデータ約3.7MB |
完成品の紹介
「HALL OF KINGS」は、ソウルライクの「ボス戦」だけを切り出したボスラッシュ形式のゲームです。スマホ縦持ちでのプレイを想定しています。
実装したシステムは次のとおりです。
- スタミナ制(攻撃・回避・ガードで消費)
- 無敵時間つき回避ロール
- ガード / パリイ(1ボタン統合: 長押しでガード、攻撃が当たる直前のタップでパリイ)
- 体幹(スタッガー)ゲージ
- エスト瓶風の回復(回数制限つき・使用中は無防備)
- HP50%以下で行動が変わる第2形態
- 例の「YOU DIED」的な演出
ボスは大剣使いの「骸ノ騎士 グラム」と、三連落雷・超高空急襲を使う「雷ノ王 ヴォルガ」の2体。後述するデータ駆動設計により、定義を1つ追加すればボスを増やせる構造になっています。
権利面について
- フロムソフトウェアの名称・キャラクター・音源・モデルは一切使用していません。参照したのは「スタミナ管理」「攻撃の予告」「死亡演出」といったジャンルの文法のみで、キャラクター・名称はすべてオリジナルです
- Mixamo素材は利用規約上、ゲームへの組み込みは可能ですが素材単体の再配布は不可のため、元のFBX/glbはリポジトリから除外し、ゲームに組み込んだ埋め込みJSのみを公開しています
- Three.js(MIT License)は同梱、効果音はWeb Audio APIによるリアルタイム合成で音源ファイルを使っていません
進化の4段階
このゲームは最初から3Dモデルありきで作ったわけではなく、4段階の作り直しを経ています。この変遷が、AI協働開発の実際の進み方として一番面白いところだと思うので、順に紹介します。
第1段階: ワイヤーフレーム版(Canvas 2D + 自前3D投影)
最初のバージョンは Three.js すら使わず、Canvas 2D に透視投影の計算を自前で書いて、線だけでキャラクターを描画するものでした。骨格アニメーション(歩き・攻撃モーション)もすべて手書きの計算です。
「まず依存ゼロで動くものを」という方針でAIに依頼したところ、想像以上にちゃんと「動き」が出たのがこの段階です。このバージョンは classic.html として今もリポジトリに残してあります。
第2段階: カプセル体版(Three.js導入)
線だけでは限界があるので Three.js を導入し、円柱と球でキャラクターに「肉付け」しました。アニメーションは引き続き手続き生成(コードで関節角度を計算する方式)です。
この方式は今も生きていて、モデル読み込みに失敗した環境では、このカプセル体キャラに自動フォールバックします。
第3段階: CC0モデル版 — 「かわいすぎる」問題
次に、無料で使えるCC0の3Dモデル(KayKit → Quaternius)を導入しました。ここで直面したのが「かわいすぎる」問題です。
KayKitのモデルはデフォルメが強く、ソウルライクの重厚な雰囲気にはどうしても合いません。Quaterniusに替えても物足りず、「見た目の雰囲気」は素材選びの段階でほぼ決まってしまうことを痛感しました。
ここで挟んでおきたいのが武器生成の失敗談です。モデルに持たせる剣はコードで生成する方針だったのですが、
- AIが生成した剣が「厚み8cmの角材」にしか見えない
- 薄くして先端に円錐を付けるよう指示したら、今度は「矢印」になった
- 菱形断面の一体型ジオメトリにして、ようやく剣に見えた
という三段オチでした。「剣らしさ」のような感覚的な要件を言葉でAIに伝える難しさが出た場面です。
第4段階: Mixamo版(最終形)
最終的に採用したのが、Adobeの Mixamo です。リアル頭身のキャラクターモデルと、プロが作ったモーションキャプチャ由来のアニメーション約25本を組み込んだ結果、一気に「それらしい」見た目になりました。
ここでの失敗談がマネキン事件です。Mixamoはキャラクターを選択した状態でアニメーションをダウンロードする仕様なのですが、キャラ未選択のままダウンロードすると、灰色のマネキン(Y Bot)が付いてきます。1回目に依頼したダウンロード分が全部マネキンでした。
面白かったのは発覚の経緯で、変換後のモデルを組み込んだClaude Codeが「マテリアル名がすべて Beta_* になっている」ことから異常を検出したことです。人間側の操作ミスを、AIがデータの中身から見つけてくれた形になります。
AIとの協働で越えた技術の壁
技術的な工夫は多数あるのですが、AIとのやり取りとして面白かった3つに絞って紹介します。
壁1: file:// 直開き対応(この設計の柱にして最大の壁)
「index.htmlをダブルクリックするだけで起動する」を設計の柱にすると、モダンなWeb 3D開発の常識といくつも衝突します。
問題1: Three.js が読み込めない。 Three.js はr148以降ESモジュール専用になっており、file:// プロトコルではCORS制約でモジュール読み込みがブロックされます。ここは割り切って、クラシック形式(scriptタグ読み込み)が使える最後のバージョンであるr147を採用しました。
問題2: 3Dモデルが読み込めない。 GLTFLoaderのURL読み込みは内部でfetchを使うため、これも file:// では動きません。対策として、モデルをbase64文字列としてJSファイルに埋め込み、loader.parse() で直接パースする方式にしました。
// URLからの読み込み(file://では動かない)ではなく
// loader.load('models/knight.glb', ...);
// 埋め込みデータを直接パースする
const binary = base64ToArrayBuffer(EMBEDDED_MODELS.knight);
loader.parse(binary, '', (gltf) => { /* ... */ });
問題3: 色が白飛びする。 r147は色管理が旧仕様のため、そのままでは新しいワークフローで作られたテクスチャが白飛びします。ColorManagement.legacyMode = false と outputEncoding = sRGBEncoding を設定して新仕様に合わせることで解消しました。
「file://で動くこと」という制約を最初にAIへ明確に伝えたことで、以降の技術選定(r147採用、埋め込み方式)をAI側が一貫して制約込みで提案してくれたのが良かった点です。
壁2: アニメーションと当たり判定の同期 — 「剣が当たる前にダメージ」問題
このゲームは当たり判定の設計が先で、アニメーションは後から載せています。ボスの攻撃は「予告円の表示 → 一定時間後に判定発生」という時間窓で管理していたため、Mixamoのアニメを単純に再生すると、剣が振り下ろされる前にダメージが発生するという間抜けな状態になりました。
解決策は、アニメーションクリップの「命中の瞬間」(クリップの55%地点と仮定)が判定開始時刻に重なるよう、再生開始位置を逆算することです。判定側の設計を変えずに、アニメ側を時間軸上でスライドさせる発想です。
このほか、見た目の「スカスカ感」への対策として、
- 足滑り防止(移動速度と歩幅アニメの同期)
- クリップを引き伸ばして尺を合わせるのではなく、予備動作をスキップして自然な速度で再生
- 被弾時のヒットストップ
を入れています。この辺りは「なんかスカスカする」「手応えがない」といった感覚的なフィードバックを投げ、AIに原因の仮説を出させて潰していく進め方でした。
壁3: 通信容量 32MB → 3.7MB(89%削減)
Mixamo素材をそのまま埋め込むと、モデルデータだけで32MBになりました。スマホでの読み込みを考えると論外のサイズです。
まずAIに内訳を分析させたところ、容量の8割がテクスチャPNGで、しかもPNGはすでに圧縮済みのためgzipではほぼ縮まないことが分かりました。対策は次の2段構えです。
- テクスチャを1024pxのJPEGに変換: 24MB → 5.1MB。ゲーム中の表示サイズでは画質差は視認できませんでした
-
gzip + base64埋め込み: ブラウザ側では
DecompressionStreamAPI(iOS 16.4+対応)で展開
const stream = new Blob([gzippedData]).stream()
.pipeThrough(new DecompressionStream('gzip'));
const buffer = await new Response(stream).arrayBuffer();
結果、32MB → 3.7MB(89%減)まで削減できました。
なお、Mixamoは1アニメーション=1FBX(各約2MB)で、メッシュとテクスチャが毎回重複して含まれる仕様です。そこで FBX2glTF でglb化した後、自作のPythonスクリプトでアニメーショントラックだけをベースglbに移植するパイプラインを組みました(1アニメあたり数十KBになります)。
ここでの失敗談が浮遊ボス事件です。アニメ移植時に腰ボーンの移動をすべて初期値に固定したところ、しゃがみポーズでも腰の高さが変わらず、ボスの足が地面から浮きました。かといって全部残すと移動処理と二重になって滑ります。正解は「水平移動(X/Z)は固定し、上下(Y)だけ残す」でした。
テストもAIでやった
このプロジェクトではコーディングだけでなく、表示確認もAI自身にやらせる体制にしました。
ヘッドレスブラウザでAIが自分でスクショを撮る
Claude Codeはコードは書けても、そのままでは「実際にどう表示されているか」を見られません。そこで、ヘッドレスEdge(msedge --headless --screenshot)で実描画のスクリーンショットを撮らせ、AIが自分の変更結果を画像で確認してから報告するループを組みました。
UIの配置崩れや、3D描画の「剣が消えている」といった問題は、コードレビューでは絶対に見つかりません。実際にレンダリングした画像を見るのが一番確実です。
あわせて、タイトル画面をスキップして戦闘状態に直行する検証用URLパラメータを用意し、「戦闘中の画面」をヘッドレスで再現できるようにしました。
ヘッドレス検証にはいくつか罠がある
実際にやってみると、ヘッドレス検証固有の罠をいくつか踏みました。
- ウィンドウ幅の最小値が約500px: スマホ想定で幅390pxを指定しても500pxで描画され、「右側が見切れた画像」を見たAIが存在しない表示崩れを直そうとする
- Windowsの表示スケール(125%等)が乗る: 指定サイズと出力画像のピクセル数がずれる
-
--virtual-time-budgetでは非同期読み込みを待てない: モデルのデコードなど実時間がかかる処理は、仮想時間の早送りでは終わらない
AIのミスをAIのテストで検出した話
象徴的だったのが閉じ括弧1個事件です。容量削減のために埋め込み生成スクリプトを書き直した際、AIが出力コードの末尾の }; を1個書き忘れ、せっかくの32MB→3.7MBの成果物が構文エラーで全ロード失敗しました。
このときは、埋め込みJSだけを読み込む使い捨てのテストページを作って切り分け、「ゲーム側の問題ではなく埋め込みファイル自体の構文エラー」と特定してから修正しました。AIは堂々と間違えるので、「動いた」の判定を人間の目視だけに頼らず、機械的に確認できる仕掛けを作っておくことが重要だと感じます。
AI協働開発で学んだこと
AIの失敗は「silent fail」する
一番怖かったのは剣が消えた事件です。Three.jsのGLTFLoaderは、ノード名から : や . を除去する仕様があります(mixamorig:RightHand → mixamorigRightHand)。武器をボーンに装着する処理が名前の完全一致で探していたため、全キャラ分の武器装着がエラーも出さずに失敗していました。
例外が出るバグは気づけますが、この手の「何も起きないだけ」のバグは、実際に画面を見ない限り発見できません。前述のスクリーンショット検証体制は、こうしたsilent failの検出にも効いています。
人間の役割は「手触りの言語化」と「判断」
今回、コードのほぼすべてはAIが書いています。では人間は何をしていたかというと、
- 感覚の言語化: 「攻撃がスカスカする」「剣に見えない」「かわいすぎる」を、AIが仮説を立てられる粒度で伝える
- 素材と権利の判断: Mixamoの利用規約の確認、再配布不可素材の公開方針の決定
- 方向性の決定: 「file://で動くこと」のような、後の技術選定すべてを縛る制約の設定
- 最終確認: AIの「できました」を鵜呑みにしない
でした。特に制約と方針を最初に明確化しておくと、AIの提案の精度が目に見えて上がります。逆に曖昧なまま進めると、角材の剣や矢印の剣が出てきます。
今後のステップ
ボスの追加を予定しています。本作はデータ駆動設計(HP・AI・攻撃判定・使用アニメクリップをすべて boss-data.js に記述し、配列にオブジェクトを1つ足せばボスが増える)のため、追加コストは低く抑えられているはずです。
// boss-data.js のイメージ: この配列に足せばボスが増える
const BOSSES = [
{ id: 'gram', name: '骸ノ騎士 グラム', hp: 1000, ai: {/* 距離帯×重み */}, attacks: [/* ... */] },
{ id: 'volga', name: '雷ノ王 ヴォルガ', hp: 1200, ai: {/* ... */}, attacks: [/* ... */] },
];
最後に数値のまとめです。
| 項目 | 数値 |
|---|---|
| コード | index.html 約2,100行 + boss-data.js 約270行 |
| 3Dモデル | 3キャラクター、アニメーション計25本超 |
| モデル容量 | 32MB → 3.7MB(89%削減) |
| 対応環境 | iOS 16.4+ / モダンブラウザ(非対応時はプロシージャルキャラへ自動フォールバック) |
ゲーム本体とソースコード(リポジトリ)はこちらです。興味があれば触ってみてください。



