momochitama

momochitama.com

自分のための創作と、AIについて

AIで音ゲーを作ってみる(1): 基本動作

ゲームプログラミングはあまりAIが得意としないイメージもあるが、シンプルな音ゲーくらいならなんとかなるかもしれない、と思い立って実践してみた。

なお、私のゲームプログラミング経験は「初心者チュートリアルを修了しました」レベル。Web開発とは勝手が違いすぎるので素人同然である。

生成コードはなんとなく方針だけチェックしつつ実装詳細は極力追わず、原則AIへの指示だけ でどこまでやれるか、どこがキツイかを試していきたい。

計画、初期実装

最初に与えたプロンプトは以下。

これからゲームを作っていきたいと思っています。 これから依頼するのは「ゲームが理想的なものになりそうか」を検証するスケッチです。見た目に過剰にこだわる必要はなく、ゲームとして最低限の挙動が動くものを作ることを優先したいです。

必須要件は以下の通りです。

  • Webブラウザで動き、キーボードとスマホのタップ両方で操作できる
  • フル2Dで、3D表現は一切使わない。ドット絵表現のみを使い、かわいいドットアニメーションを楽しめる作品としたい
  • リズムゲームである(beatmania シリーズのような、上からノートが落ちてきて対応するキーを押し、最終スコアを競う非常に一般的なもの)
  • .wav ファイルに合わせてノートを叩く(つまり、音声ファイルとの正確なタイミング同期が必要となる。要件としては最重要です)
  • 大きく分けて4つの機能がある
    • 譜面(楽曲とその難易度に応じてBPMや拍子の指定、ノートの登場タイミングを指示するもの)を作成する機能。譜面はJSONファイル等のプレーンテキストデータで保存できる
    • その譜面データを読み込むことで音楽とノートを再生し、リズムゲームをプレイできる「プレイヤー」機能。
    • 譜面の自動生成機能。音声ファイルの波形を読み取ってBPMや拍子を認識し、ノートを自動配置して譜面ファイルを作成する。
    • 楽曲選択機能。楽曲、難易度を選択すると対応する音声ファイルと譜面がロードされ、プレイヤーでゲームが始まる

ただし、最初からなくても良い機能がいくつかあります。これらは後で追加することがすでに決まっているため、それを踏まえて設計する必要があります。

- 「譜面作成機能」「譜面自動生成機能」は最初の時点では作らなくてよい。あとあと作れるようにコードを設計する必要がある
- 最初は「楽曲は4/4拍子で、一定のBPMである」という前提でコードを作ってよい。あとで「拍子が4/4拍子以外である」「拍子やBPMが楽曲の途中で変わる」という機能を後から追加できるように設計する必要がある

これらの前提を踏まえ、計画を練っていきましょう。まずは以下の質問からです。

  • 実現は可能ですか?特に、ブラウザゲームで音楽とのタイミング同期が正確にできるかが気がかりであるため、どうしても難しい場合は再検討しようと思います。
  • 実現する場合、ゲームフレームワークとして何を採用すべきですか?できればTypeScript で書けることが望ましいですが、必須要件を優先した際にもっといい言語の選択肢がある場合は検討します。

Cursor Plan Modeでざっくり計画させる。

AI

ご質問にお答えします。

1. 実現可能性について

Yes、完全に実現可能です。 特に音楽とのタイミング同期については以下の理由で問題ありません:

  • Web Audio APIを使用することで、非常に高精度(ミリ秒単位以下)のタイミング制御が可能です
  • Web Audio APIは、オーディオ専用の高優先度スレッドで動作するため、JavaScriptのメインスレッドの影響を受けません
  • 現代的なリズムゲーム(例:osu!web版、Taiko Web、など)も同じ技術で実装されています

注意点

  • モバイルブラウザでは、ユーザーの最初のタッチ操作でAudioContextを起動する必要があります(ブラウザの自動再生ポリシー)
  • オーディオ再生と描画タイミングの同期にはrequestAnimationFrameとWeb Audio APIのcurrentTimeを組み合わせます

2. フレームワークの選択

TypeScriptで書ける選択肢をいくつか提示します:

オプションA: Phaser 3

  • メリット:完全なゲームフレームワーク、スプライトアニメーション、入力管理が充実
  • デメリット:やや重量級、リズムゲーム特有の機能は自前実装

オプションB: PixiJS + 自前ゲームロジック

  • メリット:軽量で高速な2Dレンダリング、ドット絵に最適、柔軟性が高い
  • デメリット:ゲームロジックは自分で実装する必要がある

オプションC: Pure TypeScript(フレームワーク無し)

  • メリット:最も軽量、リズムゲームに特化した実装が可能、Web Audio APIと完全に統合
  • デメリット:描画やアニメーション管理を全て自前実装

私の推奨はピュアTypeScriptまたはPixiJSです。理由:

  • リズムゲームは音楽同期が最重要で、その部分はフレームワークの助けが少ない
  • ドット絵アニメーションは比較的シンプルな実装で十分
  • 将来の譜面エディタなどの機能拡張を考えると、軽量な基盤の方が柔軟

最終的には、ドット絵で描いたアセットファイルを読み込んで canvas で描画することになりますよね?(2Dではありますが、たとえばドット絵で描かれた女の子がBPM同期でダンスする、といったリッチな表現が入る可能性があります。) Pure TypeScript は大変なように感じるんですがどうですか?

AI

おっしゃる通りです!ドット絵アセットの管理やBPM同期アニメーション(スプライトアニメーション)を考えると、Pure TypeScriptは確かに大変すぎます

スプライトシートの管理、アニメーションフレームの切り替え、複数のキャラクターレイヤー、パフォーマンス最適化など、全て自前実装することになります。

推奨:PixiJS

ユーザーの要件(ドット絵キャラクターのBPM同期ダンスアニメーションなど)を考えると、PixiJSが最適です:

PixiJSのメリット:

  • スプライトシート対応:複数フレームのドット絵アニメーションを簡単に管理
  • AnimatedSprite:フレームアニメーションが簡単(BPMに合わせて制御可能)
  • 軽量:Phaserより軽く、リズムゲームの音楽同期を邪魔しない
  • 高パフォーマンス:WebGL/Canvasの自動切り替え、大量のスプライト描画に最適
  • ピクセルパーフェクト設定:ドット絵をぼやけずに描画する設定が簡単
  • TypeScript完全対応:型定義が充実
  • 柔軟性:Web Audio APIと自由に組み合わせられる

実装イメージ:

// スプライトシートからアニメーション作成
const dancerFrames = /* スプライトシート読み込み */;
const dancer = new PIXI.AnimatedSprite(dancerFrames);
dancer.animationSpeed = bpm / 60 / 4; // BPMに同期

// リズムゲームのノート描画
const note = PIXI.Sprite.from('note.png');
note.y = audioTime * scrollSpeed; // 音楽時間に同期
// スプライトシートからアニメーション作成
const dancerFrames = /* スプライトシート読み込み */;
const dancer = new PIXI.AnimatedSprite(dancerFrames);
dancer.animationSpeed = bpm / 60 / 4; // BPMに同期

// リズムゲームのノート描画
const note = PIXI.Sprite.from('note.png');
note.y = audioTime * scrollSpeed; // 音楽時間に同期

じゃあ phaser vs pixi になりそうですよね。 今回は物理演算系のエンジンはいらないということで、確かに phaser はそっちを使いたい人向けのイメージはあるんですが、とはいえ、たとえばゲーム画面ごとに「scene」で管理したりするのはフレームワークに乗っかったほうが楽というか、全部フルスクラッチで書く意味あるのかな?とは思っている部分もあります。どっちが適切か意見をお聞かせください。

AI

優れた洞察ですね。確かにシーン管理は重要なポイントです。

Phaser vs PixiJS の比較(リズムゲーム観点)

Phaser 3 の利点:

  1. Scene管理が組み込み
    • タイトル → 楽曲選択 → プレイ → リザルト の遷移が簡単
    • シーン間のデータ受け渡しも標準機能
  2. Input管理が充実
    • キーボード、タッチ、ポインタを統一的に扱える
    • キーコンフィグも実装しやすい
  3. Asset Loaderが強力
    • 画像、音声、JSONの一括読み込み
    • ローディング画面の実装が簡単
  4. Tweenアニメーション
    • UIのフェードイン/アウトなどが簡単

PixiJS の利点:

  1. 軽量(Phaserの約1/3のサイズ)
  2. 完全な制御
    • 更新ループを自分で書く = 音楽同期のタイミング制御が直感的
  3. 将来の譜面エディタに向いている
    • DOM要素との組み合わせが自由

私の推奨:Phaser 3

理由:

  1. 「スケッチ段階で早く動くものを作る」という要件に最適

    • シーン管理、入力管理、アセット管理を自前実装すると意外と時間がかかる
    • Phaserならこれらが数行で書ける
  2. 音楽同期の精度は変わらない

    • どちらを使ってもWeb Audio APIで実装するので同じ精度
    • Phaserの更新ループもrequestAnimationFrameベースなので問題なし
  3. 物理演算は使わなければオーバーヘッドにならない

    • 設定で無効化できる
  4. ドット絵表現に必要な機能が揃っている

    // スプライトアニメーションが簡単
    this.anims.create({
      key: 'dance',
      frames: this.anims.generateFrameNumbers('dancer', { start: 0, end: 7 }),
      frameRate: bpm / 60 * 4, // BPM同期
      repeat: -1
    });
    
  5. 将来の譜面エディタも実装可能

    • HTML要素とPhaser Canvasを組み合わせることは可能
    • 最悪、エディタ部分だけ別フレームワーク(Reactなど)にもできる

結論

今回のプロジェクトではPhaser 3が最適です。フルスクラッチで書く部分(音楽同期ロジック、判定システム、スコア計算)に集中でき、インフラ部分はフレームワークに任せられます。

   // スプライトアニメーションが簡単
   this.anims.create({
     key: 'dance',
     frames: this.anims.generateFrameNumbers('dancer', { start: 0, end: 7 }),
     frameRate: bpm / 60 * 4, // BPM同期
     repeat: -1
   });

なんで意見を変えたのかをお聞かせ願えますか

AI

申し訳ありません、混乱させてしまいました。正直に説明します。

意見を変えた理由

最初(PixiJS推奨)

  • 音楽同期が最重要という要件を最優先
  • リズムゲームは「フレームワークの助けが少ない」と考えた
  • 軽量で柔軟性が高い = 音楽同期の実装がシンプル、と判断

考え直したきっかけ

ユーザーが 「シーン管理はフレームワークに乗っかったほうが楽では?全部フルスクラッチで書く意味あるのか?」 と指摘されたこと。

改めて考えると:

  • シーン管理、入力管理、アセットローダーを自前実装するのは確かに時間がかかる
  • 「スケッチ段階で早く動くものを作る」 という要件を軽視していた
  • PixiJSは結局「ほぼフルスクラッチ」に近く、インフラ部分の実装に時間を取られる

本音

正直なところ、どちらでも実装可能です。判断が難しいケースです:

PixiJSを選ぶべき理由:

  • 将来の譜面エディタ(DOM要素との統合)を考えると柔軟
  • コードベースが小さく、完全に理解しやすい
  • 音楽同期ロジックへの集中がしやすい

Phaserを選ぶべき理由:

  • スケッチ段階での開発速度が圧倒的に速い
  • シーン遷移、入力、アセット管理が既製品
  • チュートリアルやコミュニティが豊富

ユーザーの判断をお願いしたい

どちらを優先しますか?

  • 開発速度優先(早くプロトタイプを動かしたい)→ Phaser
  • コードの柔軟性・理解しやすさ優先(後の拡張を重視)→ PixiJS

私は安易にPhaserに変えすぎましたが、ユーザーの「フルスクラッチで書く意味あるのか?」という言葉から「開発速度を重視している」と感じました。その理解は正しいですか?

どちらかというと、のちに本格開発を始めたタイミングで独自実装のコードがとっ散らかっていると大変そうなので、汎用的なゲーム向けコード(sceneとか)はフレームワークの様式に乗っかってしまった方が秩序を保ちやすいのではないか、と思ったという話です。

AI

なるほど、理解しました。保守性・拡張性の観点ですね。

「秩序を保つ」観点での比較

PixiJSの場合(自前実装)

// 独自のシーン管理を実装することになる
class SceneManager {
  private currentScene: BaseScene;
  private scenes: Map<string, BaseScene> = new Map();
  
  switchScene(name: string) { /* 独自実装 */ }
  update(delta: number) { /* 独自実装 */ }
}

// 問題点:
// - パターンが独自になる
// - 後から参加する人が理解コストを払う
// - 将来的にリファクタリングが必要になる可能性

Phaserの場合(標準パターン)

// 標準的なPhaser Scene
class GameScene extends Phaser.Scene {
  constructor() {
    super({ key: 'GameScene' });
  }
  
  preload() { /* アセット読み込み */ }
  create() { /* 初期化 */ }
  update(time: number, delta: number) { /* 更新 */ }
}

// 利点:
// - Phaserの標準パターン
// - 公式ドキュメント・サンプルが使える
// - 後から見ても理解しやすい

ユーザーの懸念に対する答え

「本格開発を始めたタイミングで独自実装のコードがとっ散らかっていると大変」Phaserの方が秩序を保ちやすいです。理由:

  1. 標準化されたパターン

    • Scene, Loader, Input, Animsなど、確立されたアーキテクチャ
    • チーム開発や将来の拡張を考えると予測可能性が高い
  2. ドキュメント・コミュニティ

    • 後から参加する人がPhaser公式ドキュメントを見れば理解できる
    • 独自実装だと、あなたのコードを読まないと理解できない
  3. リファクタリング不要

    • スケッチ→本格開発の移行がスムーズ
    • PixiJSだと「自前シーン管理」を後で書き直す可能性がある

結論:Phaser 3を推奨

ユーザーの「秩序を保つ」という目的に最も適しています。PixiJSは柔軟ですが、その柔軟性が逆に「独自実装のとっ散らかり」を生む可能性があります。

ただし、1点だけ確認させてください: 譜面エディタ機能(UI heavy)は将来追加予定ですが、これもPhaser内で実装予定ですか?それとも別のUIフレームワーク(React等)と組み合わせることを想定していますか?

// 独自のシーン管理を実装することになる
class SceneManager {
  private currentScene: BaseScene;
  private scenes: Map<string, BaseScene> = new Map();
  
  switchScene(name: string) { /* 独自実装 */ }
  update(delta: number) { /* 独自実装 */ }
}

// 問題点:
// - パターンが独自になる
// - 後から参加する人が理解コストを払う
// - 将来的にリファクタリングが必要になる可能性
// 標準的なPhaser Scene
class GameScene extends Phaser.Scene {
  constructor() {
    super({ key: 'GameScene' });
  }
  
  preload() { /* アセット読み込み */ }
  create() { /* 初期化 */ }
  update(time: number, delta: number) { /* 更新 */ }
}

// 利点:
// - Phaserの標準パターン
// - 公式ドキュメント・サンプルが使える
// - 後から見ても理解しやすい

譜面エディタもゲームの機能のひとつとして持たせられるとゲームをひとつのパッケージとして捉えられるし、web ui じゃなくて canvas 内でできたほうが没入感はあるかなとは思ってます。ただ、UIとしての柔軟性が削がれる/UXが悪くなるならReactで別実装でもなんでもいいですね。よく設計されたインターフェースが露出していて、その view は任意である、くらいの気持ちです。

AI

完璧です。その設計思想ならPhaser 3が最適です。

なぜPhaser 3が適しているか

  1. ゲーム部分(プレイヤー、楽曲選択)は確実にCanvas内

    • これはPhaserの標準パターンで実装できる
  2. コアロジック(譜面データ、音楽同期、判定)を独立レイヤーに

    • Phaserに依存しないTypeScriptクラスとして実装
    • ChartData, AudioSyncEngine, JudgmentSystemなど
    • これらはView非依存なので、どのUIからも使える
  3. 譜面エディタは段階的に判断

    • 最初:Phaser内でシンプルなもの(Canvas + DOM overlay)
    • UXが厳しい場合:React + コアロジックで実装
    • コアロジックが独立していれば、View層の変更は容易

アーキテクチャイメージ

Core Layer (View非依存)
  ├─ ChartData (譜面データ構造)
  ├─ AudioSyncEngine (音楽同期)
  ├─ JudgmentSystem (判定ロジック)
  └─ ChartLoader (譜面読み込み)

View Layer (交換可能)
  ├─ Phaser Scenes (ゲーム本体)
  │   ├─ TitleScene
  │   ├─ SongSelectScene
  │   ├─ GameScene
  │   └─ ResultScene
  └─ Editor (将来:Phaser or React)

では、仕様の詳細を確認して計画を立てます:

Core Layer (View非依存)
  ├─ ChartData (譜面データ構造)
  ├─ AudioSyncEngine (音楽同期)
  ├─ JudgmentSystem (判定ロジック)
  └─ ChartLoader (譜面読み込み)

View Layer (交換可能)
  ├─ Phaser Scenes (ゲーム本体)
  │   ├─ TitleScene
  │   ├─ SongSelectScene
  │   ├─ GameScene
  │   └─ ResultScene
  └─ Editor (将来:Phaser or React)

ゲーム仕様もいくつか詳細を詰めていく。

現代はリズムゲームの種類も多くプレイヤーのスキルも上がっているので、レーンはキーボードでいうとa, s, d, f, k, l, ;, ' の8レーンとかでいいと思ってます。(DJMAX というゲームがそんな感じだったと思います。)初心者向けの譜面データではそこからいくつかのレーンが間引かれて6〜4レーンになるという機能はあっていいですが、現時点では8レーンだけ考えればいいと思います。

AI

了解しました!8レーン(a, s, d, f, k, l, ;, ') でDJMAXスタイルですね。

将来的に譜面データで「使用レーン数」を指定できるようにすれば、4〜8レーンの可変対応も可能ですね。設計に含めます。

残りの確認です:

完璧です!仕様が固まりました。計画を作成します。

技術選定はちょっと恣意的に誘導してしまった(ぶっちゃけなんでもいい)が、まあそこまでおかしなものにはならないだろう、と判断し実装スタート。プラットフォームにこだわりはないためアプリゲームとして作ってみてもよかったが、私はアプリ開発経験がないため今回は日和ってTypeScriptを選択。

AI

完成しました!

ゲームの初期画面

さっそく自信満々に成果物がお出しされるが、まあ普通に壊れている。ノートがうまく流れない。

AIが意味不明な修正を行うことを防止するため、私は「見て分かる範囲で、起こっているバグをできるだけ正しく言語化する」「原因のアタリをなんとなくつけて指示に含める」ことを意識している。別に原因の推測は間違っていてもよく、大まかに怪しい箇所のヒントを与えられれば十分である。

ノートが画面上からじゃなくて、コンボ表示あたりの位置からいきなり出現するのでまともに押せないです。

AI
  1. ノートの出現位置の問題: ノートが画面上部から十分な時間をかけて落ちてくるように修正

問題を修正します。ノートが画面上部から十分な時間をかけて落ちてくるようにします:

動いた。

ゲームのプレイ画面

この時点ではまだあまり作り込まない方が手戻りがしやすいので、viewがダサすぎるのは無視。破綻している箇所だけを軽く指示して整え、先に進める。

楽曲選択画面とリザルト画面の追加

見た目はかっこわるいですが一応動くものになったので、このまま機能面の開発を続けていくことにしますか。

次にやるべきことはなんですか?

AI

良い判断です!基本が動いたので、機能面を拡充していきましょう。

次にやるべきこと(優先順位順)

🎯 最優先:実際の楽曲でテスト

現在のテスト楽曲は機械音なので、実際の楽曲(.wav)で動作検証することが最重要です。これで:

  • 音楽同期の精度が実用レベルか確認
  • 譜面データ形式が実用的か検証
  • ゲームとして楽しいか体感

必要な作業:

  1. 実際の.wavファイルを用意
  2. その楽曲の譜面JSONを作成(手動)
  3. プレイして検証

📝 次点:譜面作成補助ツール

手動で譜面JSONを作るのは大変なので、簡易的な補助があると便利です。

オプションA:スクリプトツール(簡単)

// BPMから timing を計算するヘルパー
function beatToTiming(beat, bpm, offset = 0) {
  return (beat * 60000 / bpm) + offset;
}

// 使用例
beatToTiming(4, 120, 0); // → 2000ms

オプションB:簡易的なエディタUI(中規模)

  • 音楽を再生しながらキーを押してノートを配置
  • リアルタイムで譜面プレビュー

🎵 その次:楽曲選択画面

複数の楽曲を切り替えられるようにします。

実装内容:

  1. 新しいPhaser Scene(SongSelectScene)
  2. 楽曲リストの管理(JSON)
  3. 難易度選択
  4. GameSceneへの遷移

🎚️ 並行して:オフセット調整UI

環境ごとの音ズレを調整できる機能は、リズムゲームとして必須です。

実装内容:

  • ゲーム中に±キーでオフセット調整
  • 設定を保存(localStorage)

私からの提案

パターンA:まず実用性を確認

1. 実際の楽曲(.wav)を用意してもらう
2. 私が簡易的な譜面作成スクリプトを作成
3. その楽曲でプレイして検証
4. 問題なければ楽曲選択画面を実装

パターンB:機能拡充を先行

1. 楽曲選択画面を実装
2. オフセット調整UIを実装
3. その後で実際の楽曲でテスト
AI

質問

どちらのパターンで進めますか?

  • パターンA推奨: 実際の楽曲で早めに検証し、問題があれば早期に発見
  • パターンB: 見た目も含めた全体像を先に作る

または、具体的に「次はこれをやりたい」という希望があれば教えてください!

// BPMから timing を計算するヘルパー
function beatToTiming(beat, bpm, offset = 0) {
  return (beat * 60000 / bpm) + offset;
}

// 使用例
beatToTiming(4, 120, 0); // → 2000ms
1. 実際の楽曲(.wav)を用意してもらう
2. 私が簡易的な譜面作成スクリプトを作成
3. その楽曲でプレイして検証
4. 問題なければ楽曲選択画面を実装
1. 楽曲選択画面を実装
2. オフセット調整UIを実装
3. その後で実際の楽曲でテスト

再生の検証は一旦取れている認識なので、楽曲選択画面に進んでしまっていいです。 簡素でいいので、「タイトル画面(ゲーム開始時に表示)」→「楽曲選択画面」→「プレイ画面」→「リザルト画面」→「楽曲選択画面に戻る」という一連の遷移を実装しましょうか。

AI

了解です!シーン遷移を実装していきます。

実装する内容

  1. TitleScene - タイトル画面
  2. SongSelectScene - 楽曲選択画面
  3. ResultScene - リザルト画面(GameSceneから分離)
  4. 楽曲リストデータ - songs.json
  5. GameSceneの修正 - リザルトを別シーンに移行

順番に実装します:

楽曲選択画面

楽曲の終了時にシーン遷移が動かない、いくつか演出面の挙動がおかしいといった細かいバグがあったが、指示すればそこまで困ることなく修正が進められた。

ひと段落したところで軽く見直してもらい、コードをリファクタリングしてもらう。

いいでしょう。ひと段落したので見直しタイムです。 今のコードを確認してレビューしてください。

  • 設計は適切で、今後のゲーム機能の拡張に耐えうるものになっており、人が手を加えるとしても読みやすいものになっていますか?
  • 冗長な実装や整合性のないコードはありませんか?リファクタリングの余地はありますか?
  • 整理できたら AGENTS.md や README.md を作成してください。

今のAIは「見直しタイム」を定期的に設けて人間の目を通さないと設計がどんどんとっ散らかっていく印象。このあたりはまだまだ面倒である。

ロングノートを実装

このタイミングで、初期要件から完全に漏れていた「ロングノート(長押し)」の概念を思い出す。さすがに現代の音ゲーでロングノートがないのは寂しいので、追加したいところ。

音ゲーの判定ロジックには特に詳しくないのだが、「こんな感じならまあ違和感ないだろう」という想像で指示してみる。

では開発に戻りましょう。 やるなら今のうちに考えておいたほうがいいことを一個思い出しまして、いまって「長押し(ロングノート)」って作れますか?

  • 長押しの開始時(押す)と終了時(離す)のタイミング判定がある。途中で離すと終了時判定がミス扱いになる
  • どちらも成功した場合、判定2回分で2コンボ分として扱われる

多少譜面データの作り方も変える必要があると思うんですが、現実的ですか?

AI

ロングノート機能について、現在のアーキテクチャで実装可能かどうか調査します。

現在のアーキテクチャを確認しました。ロングノート機能は十分実装可能です。Core LayerとView Layerの分離設計が活きる形で追加できます。

AI

完璧です!ロングノート機能の実装が完了しました。🎉

✅ 実装完了内容

1. データ構造の拡張 (ChartData.ts)

  • NoteDataendTimingフィールドを追加(ロングノートの終了タイミング)
  • ActiveNoteにロングノート用のフィールドを追加:
    • startJudged, endJudged, holding(状態管理)
    • startJudgment, endJudgment(判定結果)
  • isLongNote()ヘルパー関数を追加

2. 判定システムの拡張 (JudgmentSystem.ts)

  • judgeStart(): ロングノート開始判定
  • judgeEnd(): ロングノート終了判定
  • shouldMissEnd(): 終了タイミングのMissチェック
  • missEnd(): 終了Miss判定の処理
  • releaseEarly(): 途中離しの処理(即座にMiss)

3. スコア計算の調整 (ScoreCalculator.ts)

  • コンストラクタをnotes: NoteData[]を受け取るように変更
  • ロングノートは2カウント(開始+終了)として計算

4. ゲームシーンの実装 (GameScene.ts)

  • キー状態管理: pressedKeys: Set<number>で現在押されているキーを追跡
  • キーリリースイベント: keyupイベントでロングノート終了判定
  • 判定処理:
    • キーダウン時:まずロングノート開始判定、次に通常ノート判定
    • キーリリース時:ロングノート終了判定
  • 描画処理:
    • ロングノートは縦長の矩形として描画
    • 押下中は半透明表示で判定ラインに固定
    • 押下前は通常の落下アニメーション
  • 途中離しチェック: checkLongNoteHolding()で毎フレームチェック

5. テスト譜面の追加 (test-song-hard.json)

テスト用に6個のロングノートを追加:

  • レーン1: 2500ms → 3500ms(1秒間)
  • レーン4: 4000ms → 5000ms(1秒間)
  • レーン0: 8000ms → 8500ms(0.5秒間)
  • レーン2: 8500ms → 9000ms(0.5秒間)
  • レーン0: 12000ms → 13000ms(1秒間)
  • レーン3: 12500ms → 13500ms(1秒間)

🎮 動作仕様

  1. 押す: キーを押すと開始判定 → 1コンボ獲得
  2. 長押し: ノートが半透明になり判定ラインに留まる
  3. 離す: キーを離すと終了判定
    • タイミングOK: 終了判定成功 → さらに1コンボ獲得(合計2コンボ)
    • 早すぎる: 終了Miss → コンボ途切れ
    • 遅すぎる: 終了Miss → コンボ途切れ
  4. 途中で離した場合: 即座にMiss判定

✅ チェック完了

  • TypeScript型チェック: 合格
  • Biomeリント: 合格
  • コードフォーマット: 適用済み

開発サーバーを起動してテスト曲をプレイすれば、ロングノート機能を試せます!

相変わらず自信満々だが、これが全然動かない。

  • ロングノートを押した瞬間に消えちゃう。
  • ロングノートと通常ノートの区別がつかない。通常ノートも正方形みたいな形してますが、長押ししなくていいことが明確な横長の平べったい矩形にする必要があると思います。
AI

修正完了しました!✨

🔧 修正内容

1. ロングノートが押した瞬間に消える問題

  • handleJudgment()を修正:
    • ロングノートの開始判定時はスプライトを残すように変更
    • 終了判定時のみスプライトを削除するように変更
    • activeNotesからの削除も、通常ノートまたはロングノート全体の判定完了時のみに変更

2. 通常ノートとロングノートの区別

  • 通常ノートを横長の平べったい矩形に変更:
    • 幅: NOTE_SIZE - 4(少し小さめ)
    • 高さ: NOTE_SIZE / 2(半分の高さ)
  • ロングノートは元の正方形ベースで縦に伸びるので、見た目で明確に区別可能
  • ロングノートの長押しができているのに即座にミス判定になってしまう
  • ロングノートをミスしたとしても流れ切るまで消さなくていい(通常ノートと同じタイミングで消えればいい)
  • ロングノートの開始判定に成功していて、長押しもできている間は火花がずっと光るようにしてほしい

なかなか理想の挙動にならず、何度か修正依頼を繰り返す。ノートを離す際の判定がなかなか直らないので、しかたなくデバッグ結果を渡す。

Input と Release の発火タイミングは違和感なくて、LongNote の判定がおかしいような振る舞いをしています

AI

shouldMiss() がロングノートの開始判定後もチェックを続けていました。

問題の流れ

  1. ロングノート開始判定成功 → startJudged = true, holding = true, judged = false
  2. 次のフレームで checkMissedNotes() が実行
  3. shouldMiss(note) がチェックされる
  4. shouldMiss()note.judged だけをチェック → false なのでチェック続行
  5. 開始タイミング + Bad判定ウィンドウ(120ms) を過ぎる → Miss判定!

つまり、開始判定成功後、わずか120msでMiss判定になっていました。

これでもまだ「判定は合っているが演出面がおかしい」というバグが残ってしまっていた。

ゲームプログラミングの場合、「この挙動であれば正しい」という判断がWeb開発よりも有機的なので、より難しい気がする。ちゃんと説明しないと泥沼にハマりそうだったので、理想の挙動を整理したうえで修正を依頼。

やっと動くようになりました! ノートの消え方を整理したい。

  • 通常ノートは、早いorジャストの場合は押せた時点で消える。遅い場合はミス判定になるまで残り、ミス判定になったら消える

であってるよね?ロングノートもできるだけ整合性を保ったうえで自然な処理にしたいです。

  • 開始判定でミスした→半透明で流れ続けて、⭐️(後述)のタイミングで消える
  • 終了判定で早く離しすぎてミスした→半透明で流れ続けて、⭐️のタイミングで消える
  • 終了判定で遅く離しすぎてミスした→ミス判定になったタイミングで消える(このタイミングが⭐️)
  • 終了判定に成功した(グレードは問わない)→成功したタイミングで消える

が一番自然かと思うのですが、どうですか?

こういった何度かの打ち合いを繰り返し、ようやくロングノートがそれらしく動くようになった。

ロングノートの表示されたゲーム画面

プログラミング経験がまったくない人だと、この辺りのトラブルシューティングがけっこう難しいかもしれない。完全にAI任せとはなかなかいかない。

仕上げ

AIが修正に手間取ったときは大抵コードがとっ散らかった状態になっており、今後のメンテナンス性に影響が出る可能性が高い。「ロングノートが動く今の状態を正しい挙動として、コードを本来あるべき姿に寄せてもらう」という作業を依頼する。

できました。多分理想の挙動になりましたね。 めっちゃ手間かかったので、反省会とリファクタリングしましょう。

AI

コードを見直した結果、以下の問題点とリファクタリングすべき箇所が見つかりました:

🔴 重大な問題:実行順序への依存

// Miss判定チェック(ノート削除前に実行することが重要)
this.checkMissedNotes(currentTime);
...
// ノートを更新・削除(判定処理後に実行)
this.updateNotes(currentTime);

問題点

  • updateNotes()endTiming + 120msを超えたロングノートを削除する
  • checkMissedNotes()updateNotes()より前に実行されないと、ミス判定が行われない
  • この順序依存はコメントでしか表現されていないため、将来誰かが順序を変えるとバグになる

今回のバグもこれが原因でした

🟡 中程度の問題

1. ノートの削除ロジックが分散している

  • handleJudgment() - 通常ノートと終了判定完了したロングノートを削除
  • updateNotes() - 画面外に流れたロングノートを削除
  • checkOffscreenNotes()のような専用メソッドがない

2. ロングノートの状態管理が複雑

interface ActiveNote {
  judged: boolean;        // 全体の判定完了?
  startJudged?: boolean;  // 開始判定完了?
  endJudged?: boolean;    // 終了判定完了?
  holding?: boolean;      // 現在押している?
  startJudgment?: JudgmentType;
  endJudgment?: JudgmentType;
  judgmentType?: JudgmentType;
}

4つの状態フラグと3つの判定結果フィールドがあり、相互依存が複雑です。

3. handleJudgment()が長く複雑

50行以上あり、ロングノート/通常ノートの分岐、エフェクト管理、削除処理が混在しています。

💡 リファクタリング案

案1: ノート削除を専用メソッドに分離(推奨)

update() {
  this.spawnNotes(currentTime);
  this.checkMissedNotes(currentTime);  // 判定
  this.checkLongNoteHolding();
  this.updateHoldingEffects();
  this.updateNotes(currentTime);       // 描画のみ
  this.removeFinishedNotes(currentTime); // 削除(新規)
}

private removeFinishedNotes(currentTime: number) {
  // 通常ノート:判定済み(handleJudgment()で削除済みなのでここには来ない)
  // ロングノート:終了判定完了 or endTiming + 120ms超過
  const notesToRemove = this.activeNotes.filter(note => {
    if (!isLongNote(note)) return false;
    return note.endJudged || 
           (note.endTiming && currentTime > note.endTiming + DEFAULT_JUDGMENT_WINDOW.bad);
  });
  // ... 削除処理
}

メリット

  • updateNotes()は純粋に描画のみ担当
  • 削除ロジックが1箇所に集約
  • 実行順序への依存が減る(checkMissedNotes()removeFinishedNotes()が独立)

案2: ロングノートの状態をenumで管理

enum LongNoteState {
  NOT_STARTED,     // まだ押されていない
  HOLDING,         // 押している(成功)
  START_MISSED,    // 開始判定ミス
  RELEASED_EARLY,  // 早く離した
  COMPLETED,       // 終了判定完了
}

holding, startJudged, endJudgedの組み合わせを1つのenumで表現できます。

今回はできるだけ「作業中のコード詳細を深追いせずに指示だけで進める」よう意識しているが、AIは裏で怪しいコードを書きまくっているので、指摘すると明らかに壊れやすそうなコードがゴロゴロ出てくる。いくつかのやりとりをしながら修正を進める。

こうしたやり取りを経て、「JSON譜面データを読み込める8レーンの音ゲー」の基本機能が完成した。

せっかくAIがテーマなので、次回は「AIによる譜面データの自動生成」にチャレンジしてみたい。