TL;DR
- Claude Codeに「ファミコン縦スクロールアクションゲームの仕様書を作って」と依頼
- 仕様内容を確認(自分で見て確認)
- 「この仕様書に基づいてファミコン向けSTGを作って」と依頼
- CC65(C言語)+ 6502アセンブリのハイブリッド構成で約1,500行
- NES固有のバグ(スタックポインタ未初期化、スプライト0ヒット不発、VBlank時間超過)をAIと対話しながら解決
- AIによるコードレビューで5件のバグを追加発見
- 所要時間:約3時間(環境構築からプレイ可能なROMまで)
はじめに
「ファミコンのゲームを作りたい」── そう思ったことがある開発者は少なくないはずです。しかし現実には、6502アセンブリ、PPUレジスタ、VBlankタイミング、スプライト0ヒットなど、レトロゲーム開発特有の厄介な制約が立ちはだかります。
本記事では、Claude Code(Vibe Coding) を使って、ファミコン向け縦スクロールSTG「STAR ASCEND」をゼロから開発した過程を紹介します。要求仕様書を渡すところから、ビルド・デバッグ・レビュー・コミットまで、ほぼ全てをAIとの対話で進めた記録です。
完成したもの
STAR ASCEND ── 1面構成の縦スクロールSTG
- スプライト0ヒットによるラスター分割(固定ステータスバー+スクロールゲーム画面)
- 3種の敵(スカウト / ファイター / ボマー)+ドクロ型ボス戦艦
- パワーアップ(3段階)、ボム(画面フラッシュ+全弾消去)
- スコア / ハイスコア / ライフ表示
- ROM容量:40,976バイト(PRG 32KB + CHR 8KB + iNESヘッダ 16B)
開発環境
| ツール | 用途 | インストール |
|---|---|---|
| CC65 | 6502向けCコンパイラ | brew install cc65 |
| Mesen | NESエミュレータ | GitHub Releasesから.appをDL |
| Claude Code | AI開発アシスタント | 公式サイト |
Mesenのインストールでハマった点
MesenのmacOSのIntel版を入れてしまい。Apple Silicon版に入れ直しました。
# ビルド後の起動
open -a Mesen star_ascend.nes
開発の流れ
1. 要求仕様書を渡す
まずゲームの仕様書(famicom_vscroll_stg_spec.md)を書いてClaude Codeに読み込ませました。
Claude への指示:「この仕様書に基づいてファミコン向けSTGを作って」
仕様書に含めた内容:
- ゲームの基本仕様(縦スクロールSTG、1面構成)
- メモリマップ(iNESヘッダ、PRG/CHR配置)
- PPU制御の方針(スプライト0ヒットでステータスバー分割)
- 敵の出現テーブル定義
2. アセンブリからCへの移行
最初は6502アセンブリで全体が生成されましたが、1,000行を超えた時点でメンテナンスが困難に。CC65のC言語に切り替えました。
star_ascend/
├── star_ascend.c # ゲームロジック(約1,270行 / C言語)
├── crt0.s # スタートアップ、NMI、コントローラ(約270行 / 6502 asm)
├── chr_data.s # タイルデータ BG 256 + SPR 256(6502 asm)
├── nes.cfg # リンカ設定
└── Makefile
ポイント: NMIハンドラ・スプライト0待ち・コントローラ読取りはアセンブリのまま残し、ゲームロジックだけCで書くハイブリッド構成です。VBlankのタイミングがシビアな処理はCのオーバーヘッドで破綻するリスクがあるため、Claude自身がこの構成を提案しました。
3. ビルド → 実行
make # cc65 → ca65 → ld65 → star_ascend.nes (40,976 bytes)
make run # Mesenで起動
ビルドは数秒。修正→ビルド→エミュレータ確認のサイクルが速いので、Vibe Codingとの相性が良いです。
デバッグの記録 ── NES固有のバグとの戦い
Vibe Codingでファミコンゲームを作ると、AIが生成したコードにNES固有のハードウェアバグが潜みます。修正過程を3件紹介します。
Bug 1: グレースクリーン(何も表示されない)
症状: ROMを起動しても画面が灰色のまま。
原因: CC65のソフトウェアスタックポインタspが未初期化。
crt0.sのリセットハンドラでRAMを$00でクリアした後、ゼロページにあるspもゼロに。CC65のC関数は呼び出しごとにspを操作するため、全てのC関数でゼロページが破壊されていました。
; 修正: RAMクリア後にspをRAM末尾に初期化
lda #<(__RAM_START__ + __RAM_SIZE__)
sta sp
lda #>(__RAM_START__ + __RAM_SIZE__)
sta sp+1
jsr copydata ; ROM → RAM 初期値コピー
jsr initlib ; CC65ランタイム初期化
jmp _main
CC65 + NESの必須知識:
spの初期化を忘れると何も動きません。リンカ設定(nes.cfg)でdefine = yesを指定し、__RAM_START__/__RAM_SIZE__シンボルを公開する必要もあります。
Bug 2: STARTボタンでフリーズ
症状: タイトル画面は正常。STARTを押すとスコアと星が見えるが、完全フリーズ。
原因: スプライト0ヒットが永久に発生しない。問題は2つ。
問題①: ステージ準備中(STATE_STAGING)でoam_clear()が全スプライトを隠し、スプライト0が再配置されなかった。STATE_PLAYに遷移した瞬間、スプライト0ヒット待ちが無限ループに。
問題②: スプライト0タイルのピクセル位置とBGタイルのピクセル位置が重ならなかった。スプライト0ヒットは「両方の不透明ピクセルが同じ座標で重なる」ことが条件です。
// 修正: ステージ準備中もスプライト0を必ず配置
static void update_staging(void)
{
if (--stage_timer == 0) {
game_state = STATE_PLAY;
}
oam_clear();
oam_spr(248, 15, TILE_SPR0, 0x20); // ← これが無いとフリーズ
}
さらにCHR-ROMに専用のBGマーカータイル(左上1ピクセルだけ描画)を追加し、スプライト0と同じ画面座標に配置しました。
Bug 3: ボム使用後にスコア表示が崩壊
症状: ボムを使った後しばらくすると、SCOREの文字が化ける。
原因: NMIハンドラ内でunsigned longの除算を7回実行していた。
NESのVBlank時間は約2,273サイクルしかありません。CC65の32ビット除算は1回で数百サイクル消費するため、7回 × 除算 = VBlank時間を大幅超過。PPUアドレス設定の途中で描画期間に突入し、スコア表示が壊れていました。
// 修正: 計算をメインループ、PPU転送をNMIに分離
// メインループ側(重い処理はここ)
static void prepare_score_display(void) {
score_to_tiles(score_val, score_tiles); // 32bit除算 × 7
score_to_tiles(hiscore_val, hiscore_tiles);
score_dirty = 1;
}
// NMI側(PPU転送だけ = 軽い)
static void update_score_display(void) {
if (!score_dirty) return;
score_dirty = 0;
ppu_addr(0x2007);
for (i = 0; i < 7; ++i) PPUDATA = score_tiles[i];
}
NES開発の鉄則: NMI内では「PPUレジスタへの書き込み」以外の処理を極力避ける。重い計算は事前にメインループで済ませておく。
コードレビュー ── AIが自分のコードをレビューする
ゲームが動くようになった段階で、Claude Codeに「レビューして」と依頼しました。全ソースを読み直し、以下の結果が返ってきました。
発見されたバグ(5件)
| # | 重要度 | 内容 |
|---|---|---|
| 1 | 高 | ボスをボムで倒してもSTATE_CLEARに遷移しない → 空のステージに閉じ込められる |
| 2 | 中 | ハイスコアのPPUアドレスが1バイトずれ($2012 → $2013) |
| 3 | 低 | コピーライト年が "2027" 表示(タイルID $22 → $21) |
| 4 | 低 | プレイヤー移動時の境界クランプ漏れ(速度4で最大3px超過) |
| 5 | 低 | ライフ表示がゲーム中に更新されない |
発見された潜在的問題(2件)
- NMIハンドラがC関数を呼ぶため、フレーム落ち時にゼロページの一時変数が破壊される可能性
- SFX優先度がなく、長い効果音が短い効果音で上書きされる
レビューの精度について
バグ #1(ボスをボムで倒すとハマる)は秀逸でした。 check_collisions()でのボス撃破ではSTATE_CLEARを設定しているのに、use_bomb()では忘れている。同じ処理が2箇所にあって片方で漏れているパターンを、コード全体を俯瞰して検出しています。人間のレビューでも見落としやすいバグです。
一方、バグ #3(コピーライト年) は人間なら画面を見た瞬間に気づきます。AIは実行画面を見られないので、タイルIDテーブルを逆引きして数値的に検証しています。網羅的ですが、「目で見て確認する」を代替するものではありません。
レビュー結果を「y」で承認すると、5件全てを即座に修正 → ビルド → コミットまで対話だけで完了しました。
ボスキャラクターの制作
「ボスのキャラクターを極悪人の宇宙船をイメージしたものにして」と依頼。
Claudeはドクロモチーフの戦艦「Skull Battlecruiser」を提案。6タイル(8×8 × 6)を左右反転で3×3に配置する構成です。
[翼TL] [スカルTC] [翼TL反転] ← ドクロの顔(目と歯がオレンジに光る)
[翼ML] [コアMC] [翼ML反転] ← 船体(リアクターが赤く光る)
[翼BL] [エンジンBC] [翼BL反転] ← 武器ポッド+エンジン排気
NESの制約(1パレット4色、8×8タイル単位)の中で、ダークレッドの船体にオレンジに光るドクロの目という配色を実現。CHRデータの2ビットプレーン形式を直接エンコードしてくれるので、ドット絵エディタなしでタイルが作れます。
所要時間
| フェーズ | 時間 |
|---|---|
| 環境構築(CC65 + Mesen) | 15分 |
| 仕様書作成 | 20分 |
| 初期実装(アセンブリ版) | 30分 |
| C言語への移植 | 20分 |
| グレースクリーンのデバッグ | 15分 |
| フリーズバグのデバッグ | 20分 |
| スコア表示バグのデバッグ | 10分 |
| 速度調整・ボスデザイン | 15分 |
| レビュー+バグ修正 | 15分 |
| 仕様書更新・記事下書き | 20分 |
| 合計 | 約3時間 |
1面構成のシンプルなゲームですが、ラスター分割、敵出現テーブル、ボム演出、スコアシステムなど、STGの基本要素は一通り入っています。全て手作業ならNES開発経験者でも数日、未経験者なら数週間かかる内容です。
感想
Vibe Codingの強み:知識の壁を越える
NES開発で最もつらいのは、PPUの動作仕様やVBlankタイミングなど 「知らなければ絶対に解決できない」系の問題 です。Claude Codeはこれらの知識を持っていて、バグの症状から原因を推定し、適切な修正を提示できます。
CC65のsp初期化問題は「CC65ランタイムの内部挙動」を知らないとたどり着けません。従来ならドキュメントを読み漁って数時間かかるところを、コードレビューの中で指摘してもらえました。
Vibe Codingの限界:目と手は人間の仕事
AIはエミュレータの画面を見られません。 スクリーンショットを渡せば状況は理解できますが、「操作してみて違和感がある」レベルのフィードバックは人間の役割です。実際、「動きが遅い」という感覚から速度を2倍にする判断は、プレイした自分にしかできませんでした。
スコア表示の崩壊のようなタイミング依存のバグも、コード上は正しく見えます。エミュレータのデバッグ機能(PPU Viewer等)と組み合わせて初めて追い詰められる類のものです。
総評
ファミコン開発は「古い」が「単純」ではありません。むしろ、極端なリソース制約の中で工夫する面白さがあります。Vibe Codingはその工夫の試行回数を劇的に増やしてくれるツールです。
2026年に、1983年のハードウェアのためのコードをAIと書く。これはこれで一つの未来の形かもしれません。
リポジトリ
git clone https://github.com/SamAkada/star_ascend.git
cd star_ascend
make # star_ascend.nes をビルド
make run # Mesenで起動
参考リンク
- CC65 Users Guide
- NESdev Wiki ── NES開発の総合情報源
- Mesen Emulator ── デバッグ機能付きNESエミュレータ
- Claude Code ── Anthropic公式CLI
