1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code でソウルライク風3Dボス戦ゲームを作ってみた(Three.js / Mixamo / サーバー不要)

1
Last updated at Posted at 2026-07-12

はじめに

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 として今もリポジトリに残してあります。

04-classic.png

第2段階: カプセル体版(Three.js導入)

線だけでは限界があるので Three.js を導入し、円柱と球でキャラクターに「肉付け」しました。アニメーションは引き続き手続き生成(コードで関節角度を計算する方式)です。

この方式は今も生きていて、モデル読み込みに失敗した環境では、このカプセル体キャラに自動フォールバックします。

第3段階: CC0モデル版 — 「かわいすぎる」問題

次に、無料で使えるCC0の3Dモデル(KayKit → Quaternius)を導入しました。ここで直面したのが「かわいすぎる」問題です。

KayKitのモデルはデフォルメが強く、ソウルライクの重厚な雰囲気にはどうしても合いません。Quaterniusに替えても物足りず、「見た目の雰囲気」は素材選びの段階でほぼ決まってしまうことを痛感しました。

ここで挟んでおきたいのが武器生成の失敗談です。モデルに持たせる剣はコードで生成する方針だったのですが、

  1. AIが生成した剣が「厚み8cmの角材」にしか見えない
  2. 薄くして先端に円錐を付けるよう指示したら、今度は「矢印」になった
  3. 菱形断面の一体型ジオメトリにして、ようやく剣に見えた

という三段オチでした。「剣らしさ」のような感覚的な要件を言葉でAIに伝える難しさが出た場面です。

第4段階: Mixamo版(最終形)

最終的に採用したのが、Adobeの Mixamo です。リアル頭身のキャラクターモデルと、プロが作ったモーションキャプチャ由来のアニメーション約25本を組み込んだ結果、一気に「それらしい」見た目になりました。

02-battle-gram.png

ここでの失敗談がマネキン事件です。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 = falseoutputEncoding = sRGBEncoding を設定して新仕様に合わせることで解消しました。

「file://で動くこと」という制約を最初にAIへ明確に伝えたことで、以降の技術選定(r147採用、埋め込み方式)をAI側が一貫して制約込みで提案してくれたのが良かった点です。

壁2: アニメーションと当たり判定の同期 — 「剣が当たる前にダメージ」問題

このゲームは当たり判定の設計が先で、アニメーションは後から載せています。ボスの攻撃は「予告円の表示 → 一定時間後に判定発生」という時間窓で管理していたため、Mixamoのアニメを単純に再生すると、剣が振り下ろされる前にダメージが発生するという間抜けな状態になりました。

解決策は、アニメーションクリップの「命中の瞬間」(クリップの55%地点と仮定)が判定開始時刻に重なるよう、再生開始位置を逆算することです。判定側の設計を変えずに、アニメ側を時間軸上でスライドさせる発想です。

このほか、見た目の「スカスカ感」への対策として、

  • 足滑り防止(移動速度と歩幅アニメの同期)
  • クリップを引き伸ばして尺を合わせるのではなく、予備動作をスキップして自然な速度で再生
  • 被弾時のヒットストップ

を入れています。この辺りは「なんかスカスカする」「手応えがない」といった感覚的なフィードバックを投げ、AIに原因の仮説を出させて潰していく進め方でした。

壁3: 通信容量 32MB → 3.7MB(89%削減)

Mixamo素材をそのまま埋め込むと、モデルデータだけで32MBになりました。スマホでの読み込みを考えると論外のサイズです。

まずAIに内訳を分析させたところ、容量の8割がテクスチャPNGで、しかもPNGはすでに圧縮済みのためgzipではほぼ縮まないことが分かりました。対策は次の2段構えです。

  1. テクスチャを1024pxのJPEGに変換: 24MB → 5.1MB。ゲーム中の表示サイズでは画質差は視認できませんでした
  2. gzip + base64埋め込み: ブラウザ側では DecompressionStream API(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:RightHandmixamorigRightHand)。武器をボーンに装着する処理が名前の完全一致で探していたため、全キャラ分の武器装着がエラーも出さずに失敗していました。

例外が出るバグは気づけますが、この手の「何も起きないだけ」のバグは、実際に画面を見ない限り発見できません。前述のスクリーンショット検証体制は、こうした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+ / モダンブラウザ(非対応時はプロシージャルキャラへ自動フォールバック)

ゲーム本体とソースコード(リポジトリ)はこちらです。興味があれば触ってみてください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?