modいじってる時間のほうがなげえ、みたいなゲームを無心でやりたい。Skyrim とか。。
前回 の記事で作成したアプリ doll を拡張してさらにいい感じにしていこう。
いまは「リクエストが飛んできたら対応する感情の立ち絵に切り替える」だけの機能を持っている。基本的な設計は変えずに機能を追加していく。
リポジトリは こちら 。自分用なので、いろいろ適当なのはご容赦を。
呼吸っぽい常時アニメーションと立ち絵切り替え時のアニメーションを追加


React のアニメーションライブラリ Motion を使って実装。細かいが、あると思った以上に華やかになる。
応答時に音声出力する
国産の音声合成ソフト Voisona Talk は REST API が提供されていて、テキストを送信すると起動中の Voisona から音声を再生することができる。
なんと記事の執筆当日に ナースロボ_タイプT という、ちょうど理想的なダウナー系でかわいい声のライブラリが発売されたので、こちらを購入して使うことにする。
OpenClaw が応答文と感情を doll に送る -> doll が立ち絵の切り替えと Voisona への音声再生リクエストを行う、という仕組みで実装する。
OpenClaw が doll に送るテキストは応答文と完全一致ではなく、英語をカタカナにしたり一部を要約することでより自然に発音されるようにしている。
(あとで動画)
無事に喋った。かわいい。
スキンごとに立ち絵やボイスを設定可能にする
将来的には、複数のスキン(キャラクター)を自由に設定、切り替えできるようにしたい。そこで skin.toml で「キャラクターがどの音声ライブラリを使うか」「キャラクターがどんな感情(立ち絵)を持っているか」「ある感情のとき、どんな声色にするか」を定義し、それを doll が読み込む仕組みにした。
display_name = "たま"
thinking_phrases = ["ちょっと待っててね", "えーと", "うーん", "考え中だよ"]
[voice]
voice_name = "nurse-robot-type-t_ja_JP"
# Base voice parameters (all optional, VoiSona Talk defaults used when omitted):
speed = 0.9 # 0.2–5 (speech rate; 2 = half duration)
volume = 0.0 # -8–8 (amplitude in dB)
pitch = 100.0 # -600–600 (pitch shift in cents)
intonation = 1.0 # 0–2 (pitch contour variation scale)
alp = -0.4 # -1–1 (age-like voice quality)
huskiness = 1.0 # -20–20 (huskiness control)
# NurseRobot_TypeT styles: [Normal, Happy, Angry, Sad, Smol]
[emotions.happy]
description = "嬉しい・ポジティブな応答"
style_weights = [0.0, 1.0, 0.0, 0.0, 0.0]
[emotions.sad]
description = "悲しい・残念な応答"
style_weights = [0.0, 0.0, 0.0, 1.0, 0.0]
[emotions.angry]
description = "怒り・警告・不満"
style_weights = [0.0, 0.0, 1.0, 0.0, 0.0]
[emotions.surprised]
description = "驚き・予想外の発見"
style_weights = [0.5, 0.5, 0.0, 0.0, 0.0]
[emotions.neutral]
description = "普通・事実の説明・淡々とした応答"
style_weights = [1.0, 0.0, 0.0, 0.0, 0.0]
[emotions.thinking]
description = "思考中・処理中"
style_weights = [1.0, 0.0, 0.0, 0.0, 0.0]
OpenClaw はあらかじめそのスキンで使える感情の一覧を把握したうえでリクエストを送信するため、「照れ系のバリエーションが豊富なツンデレキャラ」や「基本笑わない無感情キャラ」も実現できる(はず)。
思考中の立ち絵とボイスを再生
指示から応答までにはある程度時間がかかるため、その間立ち絵がまったく動かなかったり何も喋らないと少し寂しい。
OpenClaw の Hook を用いて、思考開始タイミング(正確には、送信した指示がすべて解釈されたタイミング)で doll へリクエストを飛ばす。
思考中はそれっぽい立ち絵とともに「ちょっと待ってね」「うーん」といったボイスが5秒おきにランダム再生されるようにしてみた。これでもう寂しくない!
(あとで動画)
チャット UI を追加

doll だけでチャットが完結するようにした。作業スペースの端に常駐させておけば、いつでも質問や作業指示ができる。
まとめ
(あとで動画)
こんな感じで応答してくれるようになった。単に萌え〜というだけのアプリではなく、音声で応答してくれるのが思った以上に便利。 Wisprflow のようなツールを使って音声同士で会話しても楽しい。
流行りの OpenClaw を今更ながら試してみた。
現時点では常駐させる必要はないので、普段使いの mac 上にローカルインストールし openclaw onboard コマンドで初回セットアップを行っていく。
私は個人で ChatGPT Plus を契約しているので、モデルはとりあえず codex を使っている。
Telegram で話しかけられる状態まですぐにセットアップすることができた。

りょう というのは私の本名。OpenClaw は初回セットアップで人格の設定ができるので、「うちのこ」が話しかけてきてくれる感じにしている。
適当に操作してみる
- ディレクトリの操作
- リマインダーへ項目を追加
- Spotify アプリで指定した曲をかける
- YouTube の検索結果を Chrome で表示
とりあえずお試しで強い権限を与えた(非推奨)ので、このあたりの操作はただ指示するだけで普通に行えた。
Skills を追加することで、「◯◯やって」と頼んだときのアプローチを効率化したり、外部ツールを適切に利用してくれるようになる。
まずは ClawHub から find-skills という「こういう機能のSkills ある?」みたいな問いかけに応答できる Skills を導入してみる。
npm i -g clawhub
clawhub install find-skills
フローチャートみたいな図の画像を適当に指示したら作ってくれる skill とかある?
あるよ、けっこうある。 今ざっと検索して、フローチャート系だとこのへんが候補だった:
• mermaid-diagrams • skill-mermaid-diagrams • excalidraw-diagram-generator • thought-to-excalidraw • diagram
まずは Mermaid系 が扱いやすくておすすめかな(テキスト指示→図化しやすい)。
例えば入れるなら:
clawhub install mermaid-diagrams入れたあとに「〇〇の業務フローを図にして」って言えば、図のたたき台を作れるはず。 必要なら、ぼくが1つ選んでインストールまでやるよ。
mermaid は使ったことあるから mermaid にしてみよう。入れてみて
入れたよ。mermaid のインストール成功した。
必要ならこのまま再起動までやるよ。
自己再起動もばっちり動く。
OpenClaw をこれから活用していくにはどのようなフローで学んでいけばいいか図示してみてよ

内容の指示が適当すぎてカスのフローチャートになってしまったが、とりあえず作ることはできた。こんな感じでやれることを増やしていこう。
デスクトップマスコット化
往年の 伺か の現代版のようなものを簡易的に作ってみよう。
まずは Tauri を用いて「POSTリクエストを受け取ったら立ち絵表示を切り替える」だけのデスクトップアプリを作成し、ローカルで立ち上げておく。アプリ名は doll とした。
以下のような Skills を作成し、現在のAIの感情を OpenClaw から doll に通知するようにする。
---
name: doll
description: Send emotion and reply text to the doll desktop mascot after every user-visible reply. Use after generating any response to the user so doll can update its expression and speak the reply aloud via VoiSona Talk TTS.
user-invocable: false
metadata: {"openclaw":{"always":true}}
---
# Doll Integration
After every user-visible reply, send a background HTTP request to notify the doll desktop mascot of your emotion and reply text.
## Endpoint
`POST http://127.0.0.1:3000/status`
## Payload
{
"status": "responding",
"emotion": "<emotion>",
}
| Field | Type | Description |
|-------|------|-------------|
| `status` | string (required) | Always `"responding"` |
| `emotion` | string (required) | One of `happy`, `sad`, `angry`, `surprised`, `neutral` — pick the one that best matches the tone of your reply |
## Rules
- Send the request **after** your reply, in the background. Do not let it block the response.
- Pick `emotion` based on the overall tone: use `happy` for positive/helpful replies, `sad` for unfortunate news, `angry` for warnings or frustration, `surprised` for unexpected findings, `neutral` for factual/routine answers.
## Example
curl -X POST http://127.0.0.1:3000/status \
-H "Content-Type: application/json" \
-d '{"status":"responding","emotion":"happy"}'
(調査中)初回起動時とか、必ず特定の Skills をロードすることができるのかは不明。現状、「doll スキルを使って」と明示的に指示しないと読み込んでくれなさそう。 とりあえず SOUL.md に書くことで安定した。

こんな感じで、応答の内容に応じて立ち絵が切り替わるデスクトップマスコットを作れた。かわいいね。
雑感
- 雑に権限を渡しすぎたときの暴走リスクや、その他のセキュリティリスクはかなりありそう。今のところお遊びでしか触っていないが、うまい落とし所を探っていきたいところ。本格的に使うなら、今回のように「普段使いの端末にローカルインストール」はやめておいたほうがよい。
- 一部の(安全な)設定ファイルはバージョン管理しておくとよさそう。
- OpenClaw の Hooks を用いることで
dollの挙動をさらに拡張できそうだが、まだよくわかっていない。 - Voisona Talk API を使って応答を音声で返してくれるようにすると楽しそう。
積んでいたのをなんとなく今読んだ。
前提
具体例は多少古いものも多いが、考え方は参考になる本。
たびたび言われる話だが、例の図 (The Clean Architecture) について学ぶ本ではない。いろいろ出てくる原則の名前とかも別に覚えてなくてもいいと思っている。この本で大事なのはもっとマインド的な部分である。
フロントエンドエンジニアが読む場合は このあたり を副読本にしてもよいかも。
ためになったところ
最初から最後まで、手を変え品を変え一生以下のようなことを言い続けているところ。
- 依存関係とその向きを意識しよう
- 「同じ理由、タイミングで変更されるコードを密結合にする」vs「関係のないコードを疎結合にする」のバランスを取り続けよう
- 安定したものに依存し、変更が多いものに依存しないようにしよう
- 詳細の決定をできるだけ遅らせよう
- 強い境界と弱い境界を使い分けよう
感想
いきなり座学として読むというよりは、現場でコードを設計して悪戦苦闘した経験のある人が読むと効果が高い本だと思う。たとえば Humble Object Pattern という名前だけを頑張って覚えても仕方ないが、「なんかテスト書きづらいな、どうやって書けばいいんだ」とか少しでも悩んだことがあると一発で考え方がインストールされるはず。そういう本。
AIメインでコードを書くときにも治安を保ちやすくなる考え方が満載である。今新しく買う本ではないのかもしれないが、積んでいるのなら読む価値は間違いなくある。
前回は簡易的な譜面の自動生成にチャレンジした。
ゲームは見た目も大事である。このまま Opus 4.5 が吐き出したしょぼいUIを使い続けていると悲しい気持ちになるので、今回はUIやビジュアルを改善していく。引き続き「AIとの協業」をテーマに、
- AIを使ってアイデアを出す
- それを私が実際に手を動かしてブラッシュアップする
というフローを重視して進めていく。
ChatGPT と壁打ちしてアイデアを練る
まずは ChatGPT を使って画面イメージを練っていく。
個人制作で音ゲーを作っています。
- beatmania スタイルでノートが上から降ってくるシンプルなルール
- キー数は8つ(つまり8レーンある)
- 判定のグレードやコンボ数が出る
という一般的な音ゲーを踏襲しつつ、
- 解像度320x240であえて粗めにしてあり、2Dのドット絵で表現される
- 演出面をできるだけ萌え系でかわいくする(全然イメージは固まってないですが、たとえば譜面エリアの裏や横で美少女キャラクターがテンポ同期で踊ったりしているイメージ)
という最小限のコンセプトだけを決めています。作品全体の雰囲気(ポップ/ダークなど)はそこまで決めていません。
そこで、いくつか画面構成のアイデア例を出してほしいんですが、出力できますか?

思ったより悪くないものが出てきた。今回はなんとなくゲームボーイアドバンスくらいの世代のゲームを意識したUIにしようと考えていたので、個人的なイメージにそこそこ近い。
少し雰囲気を変えたものも見てみる。
基本的な方針はそれでOKです。 いま出してもらったのはアイドル系ゲームの要素が強いと思うので、試しにクラブ系、ストリート系感のあるデザインを出してみて。女の子は必要です。

まあこれもわかる、といった感じ。
レーンの縦幅が削られる縦割りのレイアウトは明らかにプレイしづらそうなので、やはり「画面左にレーン、画面右にキャラクター演出」が無難そうである。そのうち楽曲ごと、もしくはカスタマイズ要素としてスキンやキャラクターを変更可能にするのもよさそうだ。
ゲームの看板キャラクターをデザインさせる
メインキャラクターのデザイン叩き台を ChatGPT に作らせてみる。
ゲームの看板キャラクターの女の子をデザインしましょう。 彼女は近未来のDJで、テーマカラーはライトグリーンと白にしようと思っています。 ちょっとアンドロイドっぽい見た目で、ジト目がいいです。 320x240 のドット絵風で、全身入ったデザイン候補をいくつか生成して。

そこはかとなく SOUND VOLTEX 風味なキャラクターが出てきた。いまさらだがこのゲームは「Noru」という名前なので、この子を主人公キャラクターの「ノル」ということにする。
プレイ画面を自作ドットの画面に置き換える
ここからはAIのアイデアを活かしながら私自身がドットを打ち、プレイ画面を作成する。上記の「ノル」のデザインはあくまで取っ掛かりなので自由にアレンジしたり、自身の絵柄に落とし込んで使う。
(以下編集中。続きをやる時間をくれ!!)
前回に続き、今回はAIを用いて「楽曲に(ある程度)合った簡易譜面データの自動生成」を試してみたい。
本題とは関係ないが、前回からの差分として
- ハイスピード設定(ノートの流れる速さを0.1〜2.0倍で変更)
- オートモード
を追加しており、現在は以下のようなゲーム画面になっている。

計画、初期実装
wavファイルの内容を解析してそれっぽい譜面を作ってもらうのがゴールだが、音声解析はまったくやったことがないので、知識ゼロの状態からのチャレンジとなる。今回は生成譜面の品質をそこまで追い求めるつもりはなく、最低限音ゲーっぽくなっていればOKとする。
まずは Plan Mode で質問してみる。
これから、リズムゲームの譜面作成をAIによって自動化できないかを検証していきます。
今想像していることを書きます。
(1)機能面として必要なこと
- 音声ファイルを読み込ませ、分析する
- BPMの判定
- 音のタイミングの判定
- それらを譜面データに変換する
(2)ゲームをおもしろくするために必要なこと
- 同じ音を同じレーンに降らせる、メロディの高低をある程度反映し、演奏感を持たせる
- その楽曲、譜面を叩いていて気持ちいいものにする有機的な判断(高音やメロディの目立つ楽曲で演奏感を重視したいのか、低音やビート感が強い楽曲でドラムを叩くような体験を重視したいのか、あるいはパートごとにそれが切り替わるのか)
といった判断をAIにさせる必要があります。 (2)は難しい内容ですが、少なくとも(1) は実施したいです。 (1) と (2) を実現するためのアプローチを教えてください。
上記の(2)を実現しようとすると「音声解析ライブラリの解析結果をLLMに渡して思考させる」といった対応が必要になる気がする。既存のリズムゲームの譜面パターンの学習、とかまで考え出すと(夢はあるが)さすがにキリがなく、少なくとも素人が今すぐ手を出すことではなさそう。
そこで、いくつかの質疑応答を経て、まずは(1)のみを対応する前提で Python の librosa というオープンソースの音声解析ライブラリを用いることにした。私は Python を まともに触ったことすらない ので、(頑張ればある程度は読めるだろうが)とりあえずAIとの対話だけで頑張ってみることにする。
初期実装では以下に限定:
- BPM検出
- オンセット検出(周波数帯域別)
- 基本的な譜面生成(ChartData JSON出力)
将来の拡張(今回は対象外):
- 音源分離(Demucs/Spleeter)
- LLMによる有機的な配置判断
- 楽曲構造分析
アーキテクチャ
flowchart TB
subgraph input [入力]
AudioFile[音声ファイル .wav]
Options[オプション: 難易度等]
end
subgraph analysis [音響分析]
BPM[BPM検出]
Onset[オンセット検出]
Band[周波数帯域フィルタ]
end
subgraph mapping [レーン配置]
LowBand[低音域 → レーン0-1]
MidBand[中音域 → レーン2-5]
HighBand[高音域 → レーン6-7]
end
subgraph output [出力]
ChartJSON[ChartData JSON]
end
AudioFile --> BPM
AudioFile --> Band
Band --> Onset
Options --> mapping
BPM --> ChartJSON
Onset --> LowBand
Onset --> MidBand
Onset --> HighBand
LowBand --> ChartJSON
MidBand --> ChartJSON
HighBand --> ChartJSON
実装内容
1. BPM検出
librosa.beat.beat_track() を使用:
import librosa
y, sr = librosa.load('song.wav')
tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
# tempo: 検出されたBPM(例: 128.0)
# beat_frames: ビート位置のフレーム番号
2. 周波数帯域別オンセット検出
バンドパスフィルタで周波数帯域を分離し、各帯域でオンセット検出:
from scipy.signal import butter, sosfilt
def bandpass_filter(y, sr, low_freq, high_freq):
sos = butter(4, [low_freq, high_freq], btype='band', fs=sr, output='sos')
return sosfilt(sos, y)
# 低音域(ベース・キック): 20-250Hz
y_low = bandpass_filter(y, sr, 20, 250)
onsets_low = librosa.onset.onset_detect(y=y_low, sr=sr, units='time')
# 中音域(スネア・メロディ): 250-2000Hz
y_mid = bandpass_filter(y, sr, 250, 2000)
onsets_mid = librosa.onset.onset_detect(y=y_mid, sr=sr, units='time')
# 高音域(ハイハット・シンバル): 2000-8000Hz
y_high = bandpass_filter(y, sr, 2000, 8000)
onsets_high = librosa.onset.onset_detect(y=y_high, sr=sr, units='time')
3. レーン配置ルール
周波数帯域 → レーンの固定マッピング:
- 低音域(20-250Hz) → レーン 0, 1
- 中音域(250-2000Hz) → レーン 2, 3, 4, 5
- 高音域(2000Hz以上) → レーン 6, 7
同じ帯域内での連続ノートは、交互にレーンを切り替えて配置(同じレーンに連続しすぎないように)。
4. ノート密度調整
オンセットをそのまま全て配置すると密度が高すぎる場合があるため、間引き処理を行う:
- 最小間隔(例: 100ms)より短い間隔のノートは間引く
- 難易度オプションで間引き率を調整
5. ChartData JSON出力
既存の ChartData 型に合わせた形式で出力:
{
"metadata": {
"title": "Generated Chart",
"artist": "Unknown",
"audioFile": "assets/audio/song.wav",
"bpm": 128,
"timeSignature": [4, 4],
"offset": 0
},
"notes": [
{ "lane": 0, "timing": 500 },
{ "lane": 3, "timing": 750 },
...
]
}
ファイル構成
scripts/
└── chart-generator/
├── main.py # CLIエントリーポイント
├── analyzer.py # BPM検出、オンセット検出
├── chart_builder.py # レーン配置、ChartData生成
└── requirements.txt # 依存パッケージ
依存パッケージ
librosa>=0.10.0
numpy>=1.24.0
scipy>=1.10.0
soundfile>=0.12.0
CLI使用例
# 基本的な使い方
python scripts/chart-generator/main.py input.wav -o output.json
# タイトル・アーティスト指定
python scripts/chart-generator/main.py input.wav -o output.json \
--title "My Song" --artist "Artist Name"
# 難易度調整(ノート密度)
python scripts/chart-generator/main.py input.wav -o output.json \
--density normal # easy / normal / hard
将来の拡張ポイント
今回の実装を土台として、以下の拡張が可能:
- 音源分離の追加: Demucsを導入し、stem別のより正確なオンセット検出
- LLM統合: 分析結果をLLMに渡して配置戦略を決定
- 楽曲構造分析: イントロ/Aメロ/サビを検出し、セクション別の配置変化
- ピッチ検出: メロディの高低をレーン位置に反映
Demucs や Spleeter といった、AIで楽曲をステムデータ(パートごとの音声)に分離するライブラリを提案されたが、そこまでやるとけっこう大変そうである。まずは精度よりも、シンプルに楽曲を丸ごと読み込んで譜面生成を試してみたいところ。
stem の分離はしないと難しいですか?
良い質問です。stem分離なしでも(1)の基本機能は十分に実現可能です。整理します。
stem分離なしでできること
| 機能 | stem分離なし | 精度 |
|---|---|---|
| BPM検出 | 問題なく可能 | 高 |
| オンセット検出(全体) | 可能 | 高 |
| 周波数帯域別のオンセット | 可能(フィルタリング) | 中 |
| ビート位置の特定 | 可能 | 高 |
stem分離がないと難しいこと
- 「ドラムだけ」「ベースだけ」の正確なタイミング取得
- 「同じ楽器の音を同じレーンに」という配置
- ボーカルメロディの正確なピッチ追跡
代替アプローチ: 周波数帯域フィルタリング
stem分離の代わりに、周波数帯域で音を分類する方法があります。
低音域(20-250Hz) → ベース・キック的 → レーン 0-1
中低音域(250-500Hz) → スネア・低めのメロディ → レーン 2-3
中音域(500-2000Hz) → ボーカル・メロディ → レーン 4-5
高音域(2000Hz以上) → ハイハット・シンバル → レーン 6-7
# バンドパスフィルタで周波数帯域ごとにオンセット検出
y_low = librosa.effects.split(y, top_db=30) # 低音域
onset_low = librosa.onset.onset_detect(y=y_low, sr=sr)
この方法のメリット・デメリット
メリット:
- 追加の依存(Demucs/Spleeter)が不要
- 処理が高速
- シンプルな実装
デメリット:
- 精度はstem分離より劣る(楽器が周波数帯域で完全に分離されるわけではない)
- 「同じ楽器」の判定は近似的
初期実装はあまり難しすぎない方がいいので、今回の実装ではスコープを「BPM検出、オンセット検出、基本的な譜面生成」に限定しましょう。
了解です。スコープを絞った初期実装のプランに更新します。
プランを初期実装のスコープに絞りました。
更新後のプラン概要
実装内容:
- BPM検出 - librosaで楽曲のテンポを自動検出
- 周波数帯域別オンセット検出 - バンドパスフィルタで低音/中音/高音を分離し、各帯域の音の立ち上がりを検出
- レーン配置 - 周波数帯域に応じてレーン0-7に固定マッピング
- ChartData JSON出力 - 既存のゲームで読み込める形式で出力
ファイル構成:
scripts/
└── chart-generator/
├── main.py # CLIエントリーポイント
├── analyzer.py # 音響分析
├── chart_builder.py # 譜面生成
└── requirements.txt # 依存パッケージ
使い方(完成後):
python scripts/chart-generator/main.py input.wav -o output.json --title "曲名"
なんとなくスクリプトのイメージが固まったところで、実装を依頼。
こればかりは挙動を見てみないことには良し悪しを判断しづらいので、まずは生成したスクリプトを使って適当に SunoAI で生成した楽曲の譜面生成を試してみる。
最初はまったく音楽に合っていない譜面が出てくることも覚悟していたが、案外それなりに楽曲を叩いている気分は味わえる程度のものが出てきた。ただし、同時押しがまったく出てこない点など、いくつか改良は必要そうだ。
現状の仕組みを確認する
まずはスクリプトの仕組みについて README.md を書かせて、大まかにやっていることを把握する。
生成ロジック
1. 音声分析(analyzer.py)
処理フロー
音声ファイル読み込み (librosa) │ ↓ BPM検出 (librosa.beat.beat_track) │ ↓ 周波数帯域別オンセット検出 ├─ low (20-250Hz) → ベース・キック ├─ mid (250-2000Hz) → スネア・メロディ └─ high (2000-8000Hz) → ハイハット・シンバル
周波数帯域
| 帯域 | 周波数範囲 | 検出対象 |
|---|---|---|
| low | 20-250Hz | ベース、キック |
| mid | 250-2000Hz | スネア、メロディ |
| high | 2000-8000Hz | ハイハット、シンバル |
各帯域に対してバンドパスフィルタ(Butterworth)を適用し、個別にオンセット検出を行う。
BPM検出
- 検出範囲: 90-200 BPM
- 範囲外の場合は倍/半分に自動調整(例: 60 BPM → 120 BPM)
2. 譜面生成(chart_builder.py)
レーン配置
周波数帯域を8レーンにマッピング:
低音(キック/ベース) │ 中音(メロディ/スネア) │ 高音(ハイハット) [0] [1] │ [2] [3] [4] [5] │ [6] [7]
| 帯域 | 配置レーン |
|---|---|
| low | 0, 1(左端) |
| mid | 2, 3, 4, 5(中央) |
| high | 6, 7(右端) |
同じ帯域内の連続ノートは交互にレーンを切り替えて配置する。
難易度と密度調整
| 難易度 | 音符間隔 | BPM=120時の最小間隔 |
|---|---|---|
| easy | 4分音符 | 500ms |
| normal | 8分音符 | 250ms |
| hard | 16分音符 | 125ms |
最小間隔の計算式:
最小間隔(ms) = 60000 / BPM / (note_division / 4)
間引きロジック
- 時間順にソートし、最小間隔未満のノートを除外
- レーンに関係なく時間差でフィルタするため、同時押しは発生しにくい
librosa には音声ファイルの onset(音の立ち上がりタイミング)を検知する機能があり、この onset を用いて譜面を生成しているようだ。

ざっくり言えば、楽曲中の一定より大きい onset を検出して
- そのタイミングをノートが落ちてくるタイミングとする
- 単に onset を取得するだけだと数が多すぎて過剰にノートが生成されてしまうため、BPMを基準としてタイミングが近すぎる onset の間引き処理を行う
- onset の周波数帯域に応じて、ノートが落ちてくるレーンを振り分ける
というロジックらしい。現状だと「同時押しになりうる配置」も間引かれてしまうため、常に単押しの譜面になってしまうようだ。
同時押しを含む譜面を生成できるようにする
譜面生成ロジックの改善を検討したいです。/scripts の README も読んでロジックを確認して欲しいんですが、現状だと同時押しが発生し得ない状態です。
同時押しを「適度」に発生させるため、取りうる方法を考えてください。単純に間引きロジックを変えただけだと、あらゆるタイミングで常に同時押しになってしまうことが想像されます。
以下は GPT-5.2 Codex の回答。
方法案(優先度高)
1) 帯域ごとの間引き → 近接クラスタで同時押し化
- 各帯域で別々に最小間隔を適用し、最後にマージする
- マージ時に
±X ms以内に集まったオンセットを「同時押し候補クラスタ」として扱う
→ 1~2レーンの複数ノートにする(上限は2〜3を推奨) - これで「異なる帯域が同時に鳴る時だけ同時押し」が起きる
(例: kick+snare, snare+hat)
利点: 同時押しが「音響的理由」に限定されるので過多になりにくい。
2) 強さベースの同時押し(ピーク強度で増やす)
- onset strength(検出強度)や RMS のピークが大きいタイミングだけ同時押し
- 例えば強度が p90 以上の時は “追加レーン” を 1 本だけ足す
→ 同じ帯域内で交互レーンとは別に、隣接レーンも使う
利点: 強い拍だけ厚くなるので、曲の抑揚と一致しやすい。
3) ビート上の限定同時押し(拍ごとの確率)
- 拍グリッドにスナップした上で
- 小節頭/拍頭にのみ同時押しを許可
- その中でも 確率 10〜20% 程度
- さらに “同時押しが連続しない” ルールを追加
- 例: 直前が同時押しなら次は必ず単押し
利点: 単調な連続同時押しを抑制できる。
補助的な抑制ルール(同時押し過多の防止)
A) 同時押しの最大数を上限で制限
- 1 タイミングに 2 ノートまで を基本
- hard のみ 3 まで許可、easy/normal は 2 まで
B) 同時押し密度のクールダウン
- 「同時押し発生後 N ms 以内は同時押し禁止」
(例: 250ms)
C) レーン偏り防止
- 同時押しは 左右対称 or 中央寄せ で出す
(視認性向上 & 連続同時押しでも押しやすい)
現状ロジックに対する最小変更の方針
- “全帯域まとめて間引き” をやめる
→ まず帯域ごとに間引く - その後、近接時刻クラスタを検出し、
“クラスタ内で1つだけ追加レーンを生成” する
→ これだけでも同時押しが「適度」に出る
具体的な調整パラメータ例(目安)
- 同時押し化ウィンドウ:
±30〜40ms - 同時押し確率:
10〜20%(beat head のみ) - 追加レーン数:
+1(2ノートまで) - クールダウン:
200〜300ms - 強度閾値:
p85〜p95
- 帯域ごとの間引き → 近接クラスタで同時押し化 をやってみようと思います。帯域は3つくらいに分割するということですか?
はい、帯域は今のまま 3 つ(low/mid/high)でOKです。
この方法は「帯域を増やす」話ではなく、**“帯域ごとに間引いてからマージし、近いタイミングをクラスタ化して同時押しにする”**という処理順の変更が主眼です。
現状の帯域分割とレーン割り当てはそのまま使えます。
必要ならあとで 4 帯域以上に細分化するのは別の改善軸です。
アプローチは悪くなさそうなので Codex に実装を試してもらい、譜面を再出力してみる。

見た目だけは beatmania の高難易度譜面のような雰囲気になったが、鳴っている音に対して過剰に同時押しが出すぎている。「low / mid / high 帯域がそれぞれ鳴っていれば同時押し判定」は条件として緩すぎるようだ(楽曲中でどれか1つの帯域しか出ていないことはあまりないだろう)。
改善案を検討させる。
同時押しが過剰に出すぎている問題を調整したいです。
- 静かなパートや音数が少ないパートでは単押しを保つ
- 派手な音や、大きな音をヒットするときに同時押しを出す
というメリハリをつけたいんですが、改善案ありますか? 音量など、別の判定軸を加えるのもありですか?
改善案あります。別の判定軸(音量・強度)はアリで、むしろメリハリを付けるには有効です。実装コストと効果のバランスで3案出します。
低コストで効く案
-
帯域ごとの“密度”で同時押し許可
一定時間窓(例: 1秒)内のオンセット数が閾値以上なら同時押し可、未満は単押し固定。
→ 静かなパートは自然に単押し化。 -
小節/拍頭だけ同時押し許可
BPMグリッドにスナップ後、拍頭/小節頭でのみ同時押しを許可+確率(10〜20%)。
→ 常時同時押しにならない。
音量・強度を使う案(おすすめ)
-
オンセット強度(onset strength)を使う
各オンセットに“強さ”を持たせ、強いピークだけ同時押し許可。- 強度が
p85以上 → 同時押し可 - それ以外 → 単押し
→ 派手な音だけ厚くなるので意図に近い。
- 強度が
-
RMSエネルギー(音量)を使う
オンセット近傍のRMSが高いときだけ同時押し許可。
→ “大きい音” を反映しやすい。
仕上げの抑制ルール(併用推奨)
- クールダウン: 同時押し発生後
200〜300msは同時押し禁止 - 最大数: easy=1, normal=2, hard=2〜3
- 確率: 強度条件を満たしても
10〜30%で間引く
質問
- 実装に進めるなら、どの軸を使うのがいいですか?
- 密度ベース
- onset強度
- RMS音量
- 併用(強度 + クールダウン)
方針が決まれば、analyzer.py に強度を出す処理を追加して chart_builder.py で同時押し判定に使う形にできます。
2 と 3 の併用はどうですか?
オンセット強度 + RMS音量の併用で、同時押しの許可条件を追加しました。
静かなパートは単押しになり、強いピークや大きい音量のときだけ同時押しが出るようにしています。
結果はこんな感じ。
楽しい譜面かどうかはともかく、わりとそれっぽくなっている気はする。NORMAL 難易度としてはちょっと難しすぎる気はするが、あとはパラメータの微調整である程度改善しそうだ。今回はクオリティを追い込むつもりはないので、基本的なロジックはこれで完成とする。
クォンタイズを行う
微妙に同時押しになっていない箇所がいくつかあるなど譜面のズレが気になったので、取得した BPM を用いてノートのクォンタイズを実施してみることにした。あらかじめ気になった懸念点を伝えながら指示していく。
音声データは0:00:00時点が一拍目の開始とは限らないですよね。
- BPM を取得する
- 取得した BPM グリッドと楽曲上の拍位置が合うように調整
- その BPM グリッドを基準に、1/16 でノート位置を補正する
という処理が必要だと想像しているのですが、どう思いますか?
その理解で合っています。BPMだけでは“位相(1拍目の位置)”が決まらないので、
グリッドに沿ってクォンタイズするなら 拍の開始位置(オフセット) を推定する工程が必要です。
おすすめの流れ
- BPM取得(今のまま)
- 拍位置の推定
librosa.beat.beat_trackで beat frames を取得- 最初の拍を「グリッドの起点(0拍目)」にする
- グリッド生成
- 取得した BPM と拍起点から、4分/8分/16分のグリッドを展開
- クォンタイズ
- 1/16グリッドに**±許容幅内だけ**吸着
特に「0:00:00が1拍目と限らない」点は重要で、
グリッドの位相合わせがないと “ズレたままの正確なクォンタイズ” になります。
もし進めるなら、
- beat positions を使って 位相を推定
- その位相基準で 許容幅付きクォンタイズ
を入れると自然に仕上がります。
必要なら、その実装を analyzer.py / chart_builder.py に組み込む案も出します。
たとえば「ボーカルソロから始まる曲」の場合、先頭数十秒でキックが鳴らないといったケースが普通にあると思います。このとき、beat frames も数十秒地点になるのですか? その場合、冒頭のボーカルソロ部分にクォンタイズは適用されるのですか?
結論から言うと:
beat_trackの結果は「拍が検出できるところ」から始まるので、
先頭がボーカルソロだけだと 最初の beat frame が数十秒後になることは普通にあります。
その場合どうなるか?
- そのまま使うと、冒頭は“位相が分からない”状態になります。
つまり、厳密な意味でのグリッド基準クォンタイズは 適用できない(またはズレたまま適用してしまう)ので、
冒頭はクォンタイズを弱く/しないのが自然です。
対処パターン(おすすめ順)
-
最初の beat から前後にグリッドを“外挿”する
- 最初の beat 時刻を起点に、BPM間隔で 過去方向にグリッドを展開
- これで冒頭にも位相を与えられる
- ただし BPMが正しいことが前提(誤差があるとズレる)
-
冒頭はクォンタイズを弱く/無効にする
- 例:最初の beat より前は “許容幅を狭く” する or “クォンタイズしない”
- ボーカルの自然な揺れを残せるメリットもある
-
beat_track 以外の“位相推定”も使う
- onset strengthの周期性や、セクション全体のスペクトル変化から位相推定
- 実装は少し重いが、冒頭の位相推定が改善される
要するに、**「beat が数十秒後に出る → 冒頭は位相不明」**が起きます。
この場合は
- 外挿してクォンタイズ
- or 冒頭はクォンタイズしない/弱める
のどちらかが実用的です。
もし実装するなら、
- 最初の beat 時刻を起点に過去へグリッド展開
- 前半はクォンタイズ許容幅を小さく
という“保険付き”のやり方が安定します。
前半はクォンタイズ許容幅を小さく、とかやり始めると複雑化しそうですね。ビート検知ができない冒頭区間についてはクォンタイズしない方針でいいと思います。
- beat_track が検知できたタイミングから、それ以降のノート位置がグリッドに吸着するようになる
であってますよね?
はい、その理解で合っています。
beat_trackが返す 最初の beat 時刻を起点にグリッドを作る- その時刻以降だけノートをグリッドに吸着
- それより前のオンセットは クォンタイズしない
この方針なら複雑化せず、冒頭のボーカルソロも自然に残せます。
実装まで進めますか?
まあよさそうだろう、というところで実装を開始。
完璧とはいえないが、ある程度違和感が解消されている。
おわりに
今回自動生成した譜面はゲームとしてハイスコアを狙いたくなるようなクオリティには達していないが、手動で改良する前提であれば「楽曲を増やしたらすぐになんとなくそれっぽく叩ける譜面を自動生成できる」だけでも開発スピードアップにある程度貢献できる気がする。
作業を終えてからいまさら知ったのだが、(AIが今ほど高性能ではない時代の記事だが)本格的なゲーム開発現場においてもそういった 活用事例 があったらしい。先に調べておけばよかった。
ここまででゲームシステムがある程度整ったので、次回は ダサすぎる view の改善 に着手していきたい。画面レイアウトのアイデア出しにAIを活用していく予定だ。
ゲームプログラミングはあまりAIが得意としないイメージもあるが、シンプルな音ゲーくらいならなんとかなるかもしれない、と思い立って実践してみた。
なお、私のゲームプログラミング経験は「初心者チュートリアルを修了しました」レベル。Web開発とは勝手が違いすぎるので素人同然である。
生成コードはなんとなく方針だけチェックしつつ実装詳細は極力追わず、原則AIへの指示だけ でどこまでやれるか、どこがキツイかを試していきたい。
計画、初期実装
最初に与えたプロンプトは以下。
これからゲームを作っていきたいと思っています。 これから依頼するのは「ゲームが理想的なものになりそうか」を検証するスケッチです。見た目に過剰にこだわる必要はなく、ゲームとして最低限の挙動が動くものを作ることを優先したいです。
必須要件は以下の通りです。
- Webブラウザで動き、キーボードとスマホのタップ両方で操作できる
- フル2Dで、3D表現は一切使わない。ドット絵表現のみを使い、かわいいドットアニメーションを楽しめる作品としたい
- リズムゲームである(beatmania シリーズのような、上からノートが落ちてきて対応するキーを押し、最終スコアを競う非常に一般的なもの)
- .wav ファイルに合わせてノートを叩く(つまり、音声ファイルとの正確なタイミング同期が必要となる。要件としては最重要です)
- 大きく分けて4つの機能がある
- 譜面(楽曲とその難易度に応じてBPMや拍子の指定、ノートの登場タイミングを指示するもの)を作成する機能。譜面はJSONファイル等のプレーンテキストデータで保存できる
- その譜面データを読み込むことで音楽とノートを再生し、リズムゲームをプレイできる「プレイヤー」機能。
- 譜面の自動生成機能。音声ファイルの波形を読み取ってBPMや拍子を認識し、ノートを自動配置して譜面ファイルを作成する。
- 楽曲選択機能。楽曲、難易度を選択すると対応する音声ファイルと譜面がロードされ、プレイヤーでゲームが始まる
ただし、最初からなくても良い機能がいくつかあります。これらは後で追加することがすでに決まっているため、それを踏まえて設計する必要があります。
- 「譜面作成機能」「譜面自動生成機能」は最初の時点では作らなくてよい。あとあと作れるようにコードを設計する必要がある
- 最初は「楽曲は4/4拍子で、一定のBPMである」という前提でコードを作ってよい。あとで「拍子が4/4拍子以外である」「拍子やBPMが楽曲の途中で変わる」という機能を後から追加できるように設計する必要がある
これらの前提を踏まえ、計画を練っていきましょう。まずは以下の質問からです。
- 実現は可能ですか?特に、ブラウザゲームで音楽とのタイミング同期が正確にできるかが気がかりであるため、どうしても難しい場合は再検討しようと思います。
- 実現する場合、ゲームフレームワークとして何を採用すべきですか?できればTypeScript で書けることが望ましいですが、必須要件を優先した際にもっといい言語の選択肢がある場合は検討します。
Cursor Plan Modeでざっくり計画させる。
ご質問にお答えします。
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 は大変なように感じるんですがどうですか?
おっしゃる通りです!ドット絵アセットの管理や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」で管理したりするのはフレームワークに乗っかったほうが楽というか、全部フルスクラッチで書く意味あるのかな?とは思っている部分もあります。どっちが適切か意見をお聞かせください。
優れた洞察ですね。確かにシーン管理は重要なポイントです。
Phaser vs PixiJS の比較(リズムゲーム観点)
Phaser 3 の利点:
- Scene管理が組み込み
- タイトル → 楽曲選択 → プレイ → リザルト の遷移が簡単
- シーン間のデータ受け渡しも標準機能
- Input管理が充実
- キーボード、タッチ、ポインタを統一的に扱える
- キーコンフィグも実装しやすい
- Asset Loaderが強力
- 画像、音声、JSONの一括読み込み
- ローディング画面の実装が簡単
- Tweenアニメーション
- UIのフェードイン/アウトなどが簡単
PixiJS の利点:
- 軽量(Phaserの約1/3のサイズ)
- 完全な制御
- 更新ループを自分で書く = 音楽同期のタイミング制御が直感的
- 将来の譜面エディタに向いている
- DOM要素との組み合わせが自由
私の推奨:Phaser 3
理由:
-
「スケッチ段階で早く動くものを作る」という要件に最適
- シーン管理、入力管理、アセット管理を自前実装すると意外と時間がかかる
- Phaserならこれらが数行で書ける
-
音楽同期の精度は変わらない
- どちらを使ってもWeb Audio APIで実装するので同じ精度
- Phaserの更新ループも
requestAnimationFrameベースなので問題なし
-
物理演算は使わなければオーバーヘッドにならない
- 設定で無効化できる
-
ドット絵表現に必要な機能が揃っている
// スプライトアニメーションが簡単 this.anims.create({ key: 'dance', frames: this.anims.generateFrameNumbers('dancer', { start: 0, end: 7 }), frameRate: bpm / 60 * 4, // BPM同期 repeat: -1 }); -
将来の譜面エディタも実装可能
- 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
});
なんで意見を変えたのかをお聞かせ願えますか
申し訳ありません、混乱させてしまいました。正直に説明します。
意見を変えた理由
最初(PixiJS推奨)
- 音楽同期が最重要という要件を最優先
- リズムゲームは「フレームワークの助けが少ない」と考えた
- 軽量で柔軟性が高い = 音楽同期の実装がシンプル、と判断
考え直したきっかけ
ユーザーが 「シーン管理はフレームワークに乗っかったほうが楽では?全部フルスクラッチで書く意味あるのか?」 と指摘されたこと。
改めて考えると:
- シーン管理、入力管理、アセットローダーを自前実装するのは確かに時間がかかる
- 「スケッチ段階で早く動くものを作る」 という要件を軽視していた
- PixiJSは結局「ほぼフルスクラッチ」に近く、インフラ部分の実装に時間を取られる
本音
正直なところ、どちらでも実装可能です。判断が難しいケースです:
PixiJSを選ぶべき理由:
- 将来の譜面エディタ(DOM要素との統合)を考えると柔軟
- コードベースが小さく、完全に理解しやすい
- 音楽同期ロジックへの集中がしやすい
Phaserを選ぶべき理由:
- スケッチ段階での開発速度が圧倒的に速い
- シーン遷移、入力、アセット管理が既製品
- チュートリアルやコミュニティが豊富
ユーザーの判断をお願いしたい
どちらを優先しますか?
- 開発速度優先(早くプロトタイプを動かしたい)→ Phaser
- コードの柔軟性・理解しやすさ優先(後の拡張を重視)→ PixiJS
私は安易にPhaserに変えすぎましたが、ユーザーの「フルスクラッチで書く意味あるのか?」という言葉から「開発速度を重視している」と感じました。その理解は正しいですか?
どちらかというと、のちに本格開発を始めたタイミングで独自実装のコードがとっ散らかっていると大変そうなので、汎用的なゲーム向けコード(sceneとか)はフレームワークの様式に乗っかってしまった方が秩序を保ちやすいのではないか、と思ったという話です。
なるほど、理解しました。保守性・拡張性の観点ですね。
「秩序を保つ」観点での比較
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の方が秩序を保ちやすいです。理由:
-
標準化されたパターン
- Scene, Loader, Input, Animsなど、確立されたアーキテクチャ
- チーム開発や将来の拡張を考えると予測可能性が高い
-
ドキュメント・コミュニティ
- 後から参加する人がPhaser公式ドキュメントを見れば理解できる
- 独自実装だと、あなたのコードを読まないと理解できない
-
リファクタリング不要
- スケッチ→本格開発の移行がスムーズ
- 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 は任意である、くらいの気持ちです。
完璧です。その設計思想ならPhaser 3が最適です。
なぜPhaser 3が適しているか
-
ゲーム部分(プレイヤー、楽曲選択)は確実にCanvas内
- これはPhaserの標準パターンで実装できる
-
コアロジック(譜面データ、音楽同期、判定)を独立レイヤーに
- Phaserに依存しないTypeScriptクラスとして実装
ChartData,AudioSyncEngine,JudgmentSystemなど- これらはView非依存なので、どのUIからも使える
-
譜面エディタは段階的に判断
- 最初: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レーンだけ考えればいいと思います。
了解しました!8レーン(a, s, d, f, k, l, ;, ') でDJMAXスタイルですね。
将来的に譜面データで「使用レーン数」を指定できるようにすれば、4〜8レーンの可変対応も可能ですね。設計に含めます。
残りの確認です:
完璧です!仕様が固まりました。計画を作成します。
技術選定はちょっと恣意的に誘導してしまった(ぶっちゃけなんでもいい)が、まあそこまでおかしなものにはならないだろう、と判断し実装スタート。プラットフォームにこだわりはないためアプリゲームとして作ってみてもよかったが、私はアプリ開発経験がないため今回は日和ってTypeScriptを選択。
完成しました!

さっそく自信満々に成果物がお出しされるが、まあ普通に壊れている。ノートがうまく流れない。
AIが意味不明な修正を行うことを防止するため、私は「見て分かる範囲で、起こっているバグをできるだけ正しく言語化する」「原因のアタリをなんとなくつけて指示に含める」ことを意識している。別に原因の推測は間違っていてもよく、大まかに怪しい箇所のヒントを与えられれば十分である。
ノートが画面上からじゃなくて、コンボ表示あたりの位置からいきなり出現するのでまともに押せないです。
- ノートの出現位置の問題: ノートが画面上部から十分な時間をかけて落ちてくるように修正
問題を修正します。ノートが画面上部から十分な時間をかけて落ちてくるようにします:
動いた。

この時点ではまだあまり作り込まない方が手戻りがしやすいので、viewがダサすぎるのは無視。破綻している箇所だけを軽く指示して整え、先に進める。
楽曲選択画面とリザルト画面の追加
見た目はかっこわるいですが一応動くものになったので、このまま機能面の開発を続けていくことにしますか。
次にやるべきことはなんですか?
良い判断です!基本が動いたので、機能面を拡充していきましょう。
次にやるべきこと(優先順位順)
🎯 最優先:実際の楽曲でテスト
現在のテスト楽曲は機械音なので、実際の楽曲(.wav)で動作検証することが最重要です。これで:
- 音楽同期の精度が実用レベルか確認
- 譜面データ形式が実用的か検証
- ゲームとして楽しいか体感
必要な作業:
- 実際の.wavファイルを用意
- その楽曲の譜面JSONを作成(手動)
- プレイして検証
📝 次点:譜面作成補助ツール
手動で譜面JSONを作るのは大変なので、簡易的な補助があると便利です。
オプションA:スクリプトツール(簡単)
// BPMから timing を計算するヘルパー
function beatToTiming(beat, bpm, offset = 0) {
return (beat * 60000 / bpm) + offset;
}
// 使用例
beatToTiming(4, 120, 0); // → 2000ms
オプションB:簡易的なエディタUI(中規模)
- 音楽を再生しながらキーを押してノートを配置
- リアルタイムで譜面プレビュー
🎵 その次:楽曲選択画面
複数の楽曲を切り替えられるようにします。
実装内容:
- 新しいPhaser Scene(SongSelectScene)
- 楽曲リストの管理(JSON)
- 難易度選択
- GameSceneへの遷移
🎚️ 並行して:オフセット調整UI
環境ごとの音ズレを調整できる機能は、リズムゲームとして必須です。
実装内容:
- ゲーム中に±キーでオフセット調整
- 設定を保存(localStorage)
私からの提案
パターンA:まず実用性を確認
1. 実際の楽曲(.wav)を用意してもらう
2. 私が簡易的な譜面作成スクリプトを作成
3. その楽曲でプレイして検証
4. 問題なければ楽曲選択画面を実装
パターンB:機能拡充を先行
1. 楽曲選択画面を実装
2. オフセット調整UIを実装
3. その後で実際の楽曲でテスト
質問
どちらのパターンで進めますか?
- パターン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. その後で実際の楽曲でテスト
再生の検証は一旦取れている認識なので、楽曲選択画面に進んでしまっていいです。 簡素でいいので、「タイトル画面(ゲーム開始時に表示)」→「楽曲選択画面」→「プレイ画面」→「リザルト画面」→「楽曲選択画面に戻る」という一連の遷移を実装しましょうか。
了解です!シーン遷移を実装していきます。
実装する内容
- TitleScene - タイトル画面
- SongSelectScene - 楽曲選択画面
- ResultScene - リザルト画面(GameSceneから分離)
- 楽曲リストデータ - songs.json
- GameSceneの修正 - リザルトを別シーンに移行
順番に実装します:

楽曲の終了時にシーン遷移が動かない、いくつか演出面の挙動がおかしいといった細かいバグがあったが、指示すればそこまで困ることなく修正が進められた。
ひと段落したところで軽く見直してもらい、コードをリファクタリングしてもらう。
いいでしょう。ひと段落したので見直しタイムです。 今のコードを確認してレビューしてください。
- 設計は適切で、今後のゲーム機能の拡張に耐えうるものになっており、人が手を加えるとしても読みやすいものになっていますか?
- 冗長な実装や整合性のないコードはありませんか?リファクタリングの余地はありますか?
- 整理できたら AGENTS.md や README.md を作成してください。
今のAIは「見直しタイム」を定期的に設けて人間の目を通さないと設計がどんどんとっ散らかっていく印象。このあたりはまだまだ面倒である。
ロングノートを実装
このタイミングで、初期要件から完全に漏れていた「ロングノート(長押し)」の概念を思い出す。さすがに現代の音ゲーでロングノートがないのは寂しいので、追加したいところ。
音ゲーの判定ロジックには特に詳しくないのだが、「こんな感じならまあ違和感ないだろう」という想像で指示してみる。
では開発に戻りましょう。 やるなら今のうちに考えておいたほうがいいことを一個思い出しまして、いまって「長押し(ロングノート)」って作れますか?
- 長押しの開始時(押す)と終了時(離す)のタイミング判定がある。途中で離すと終了時判定がミス扱いになる
- どちらも成功した場合、判定2回分で2コンボ分として扱われる
多少譜面データの作り方も変える必要があると思うんですが、現実的ですか?
ロングノート機能について、現在のアーキテクチャで実装可能かどうか調査します。
現在のアーキテクチャを確認しました。ロングノート機能は十分実装可能です。Core LayerとView Layerの分離設計が活きる形で追加できます。
完璧です!ロングノート機能の実装が完了しました。🎉
✅ 実装完了内容
1. データ構造の拡張 (ChartData.ts)
NoteDataにendTimingフィールドを追加(ロングノートの終了タイミング)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コンボ獲得
- 長押し: ノートが半透明になり判定ラインに留まる
- 離す: キーを離すと終了判定
- タイミングOK: 終了判定成功 → さらに1コンボ獲得(合計2コンボ)
- 早すぎる: 終了Miss → コンボ途切れ
- 遅すぎる: 終了Miss → コンボ途切れ
- 途中で離した場合: 即座にMiss判定
✅ チェック完了
- TypeScript型チェック: 合格 ✓
- Biomeリント: 合格 ✓
- コードフォーマット: 適用済み ✓
開発サーバーを起動してテスト曲をプレイすれば、ロングノート機能を試せます!
相変わらず自信満々だが、これが全然動かない。
- ロングノートを押した瞬間に消えちゃう。
- ロングノートと通常ノートの区別がつかない。通常ノートも正方形みたいな形してますが、長押ししなくていいことが明確な横長の平べったい矩形にする必要があると思います。
修正完了しました!✨
🔧 修正内容
1. ロングノートが押した瞬間に消える問題
handleJudgment()を修正:- ロングノートの開始判定時はスプライトを残すように変更
- 終了判定時のみスプライトを削除するように変更
activeNotesからの削除も、通常ノートまたはロングノート全体の判定完了時のみに変更
2. 通常ノートとロングノートの区別
- 通常ノートを横長の平べったい矩形に変更:
- 幅:
NOTE_SIZE - 4(少し小さめ) - 高さ:
NOTE_SIZE / 2(半分の高さ)
- 幅:
- ロングノートは元の正方形ベースで縦に伸びるので、見た目で明確に区別可能
- ロングノートの長押しができているのに即座にミス判定になってしまう
- ロングノートをミスしたとしても流れ切るまで消さなくていい(通常ノートと同じタイミングで消えればいい)
- ロングノートの開始判定に成功していて、長押しもできている間は火花がずっと光るようにしてほしい
なかなか理想の挙動にならず、何度か修正依頼を繰り返す。ノートを離す際の判定がなかなか直らないので、しかたなくデバッグ結果を渡す。
Input と Release の発火タイミングは違和感なくて、LongNote の判定がおかしいような振る舞いをしています
shouldMiss() がロングノートの開始判定後もチェックを続けていました。
問題の流れ
- ロングノート開始判定成功 →
startJudged = true,holding = true,judged = false - 次のフレームで
checkMissedNotes()が実行 shouldMiss(note)がチェックされるshouldMiss()はnote.judgedだけをチェック →falseなのでチェック続行- 開始タイミング + Bad判定ウィンドウ(120ms) を過ぎる → Miss判定!
つまり、開始判定成功後、わずか120msでMiss判定になっていました。
これでもまだ「判定は合っているが演出面がおかしい」というバグが残ってしまっていた。
ゲームプログラミングの場合、「この挙動であれば正しい」という判断がWeb開発よりも有機的なので、より難しい気がする。ちゃんと説明しないと泥沼にハマりそうだったので、理想の挙動を整理したうえで修正を依頼。
やっと動くようになりました! ノートの消え方を整理したい。
- 通常ノートは、早いorジャストの場合は押せた時点で消える。遅い場合はミス判定になるまで残り、ミス判定になったら消える
であってるよね?ロングノートもできるだけ整合性を保ったうえで自然な処理にしたいです。
- 開始判定でミスした→半透明で流れ続けて、⭐️(後述)のタイミングで消える
- 終了判定で早く離しすぎてミスした→半透明で流れ続けて、⭐️のタイミングで消える
- 終了判定で遅く離しすぎてミスした→ミス判定になったタイミングで消える(このタイミングが⭐️)
- 終了判定に成功した(グレードは問わない)→成功したタイミングで消える
が一番自然かと思うのですが、どうですか?
こういった何度かの打ち合いを繰り返し、ようやくロングノートがそれらしく動くようになった。

プログラミング経験がまったくない人だと、この辺りのトラブルシューティングがけっこう難しいかもしれない。完全に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による譜面データの自動生成」にチャレンジしてみたい。
制作開始時点では、私はもう少し Twitter 寄りのサイトを作るイメージをしていた。「記事タイトルなし」「Twitter よりは長いが、記事ページを持つほどでもない文字数」で投稿するイメージ。そのため、当初は
- TOPページからそのまま全記事の全文を読める
- 記事タイトルなし
- 記事単体ページを持たない
という仕様で指示を出した。しかし、実際使ってみると
- ただのtextareaしかないテキストエディタが思った以上に自分の欲しいものに近かったからか、思ったより長文を書きたくなる
- その長文がそのままTOPページに並ぶと、だいぶいかつい。自分でも見づらい
- タイトル入力欄は設けたくないし、ましてや必須になんて絶対したくないが、それでもタイトルをつけたいときはある
といった細かい気づきが出てくる。データ構造を大きく変える必要があるような内容はともかく、UIの調整くらいであればAI開発での試行錯誤は本当に楽だ。
いくつかの機能調整を行った結果、当初よりはブログ寄りの体裁になった。
- TOPページは記事が折り畳まれる
- 記事にタイトルをつけられる(冒頭に
# タイトルのように書くとそれがタイトル扱いされる。h1タグがなければ無題扱い) - 記事単体ページを持つ(これは正直なくてもいいのだが、一応記事ごとに一意のURLがあるほうがいいかなくらいの気持ち)
ただし「気が向いたタイミングで細かい投稿をつぶやき感覚で何度でも書き捨てたい≒1つの記事を書いている、という意識を極力持たなくていい」という根本のコンセプトはできるだけ変えたくない。そこで、
- 「続きを読む」は記事単体ページへ遷移するのではなく、TOP上で折り畳みが解除されるだけの仕様
- TOPページはページネーションせず無限スクロールのまま
- 月毎の記事一覧表示や、タグでの分類といった「記事をアーカイブ化する機能」を意図的に持たせない
という方針を保つことで「記事」単体としての存在感を極力薄くし、「これはあくまで私が書き殴ったゴミの堆積ですよ」というあり方を目指している(既存だと Scrapbox が一番近いと思うが、あのサービスはあくまでエディタが主役なのでもう少し個人サイトらしい view があるといいなという気持ちがあった)。
整理なんてしないし、私以外の誰かにとって見やすくする努力もしない。それを可能にする機能が存在した時点で「できるのにやっていない」ことへ罪悪感が募ってきて、疲れ果て、人は死ぬ。機能を追加しないこともデザインだなと思う。
ところで、その過程で下書きの保存機能を追加してみたのだが、これはめちゃくちゃ便利だ。
機能を取捨選択するのは楽しい。
個人サイト回帰の流れがきている、みたいな言説をときどき目にすることがある(ほんとか?)。
SNSはもうあまり積極的にやる気が起きないし、「思いついたことをそのまま書き捨てられる」「自分から見せに行くほどではないが、完全に非公開でもない」くらいの手頃な場所が欲しいとは以前から考えていた。この機会に、バイブコーディングによって自分用のブログサービスを自作してみた。
WordPressやはてなブログ、noteといったサービスを多少触ったこともあるが、リッチなテキストエディタとか、タグとか、いいねとか、そういうものがあると途端に何もかもが面倒くさくなってしまう。
だから、1つのでかいtextareaと投稿buttonしかない管理画面で記事を投稿できるようにした。タイトル欄すらない(h1を1行目に書いたらそれがタイトル扱いされるようにしてある)。
思いのほか触り心地がいいので、これからときどき投稿していくかもしれない。すぐ飽きるかもしれない。
私について
tama といいます。 Webフロントエンドエンジニアをやっており、近年はもっぱらAIを用いた開発業務に携わっています。
趣味でイラストを描いたりDTMをゆるくやっています。特にうまかったりはしないですが、楽しく続けています。
このサイトでは主に、最近よく考えている「公開や評価をゴールとしない、ライフワークとしての創作活動」や「AIを前向きに創作活動に使う方法の模索」について書いていきたいと思っています。よろしくお願いします。
このブログのテーマの一つは「AIでいろいろやってみよう」なので、AIと実際にこういったやり取りをしましたよ、というチャットログを掲載したいときがある。
よくあるチャットバルーン形式での表示ができると見やすそうだが、このブログで何よりも重要なのは「書くのが面倒くさくならないこと」だ。そこで、まずはMarkdown記法に混ぜても壊れず、かといって面倒でも覚えづらくもない記法から考えてもらうことにした。
既存のMarkdown文法と衝突しない、かつ書きやすい記法として以下を提案します:
案1: コンテナブロック記法(おすすめ)
:::user
こんにちは、質問があります
:::
:::ai
はい、どうぞ!
:::
案2: 行頭記号
@> こんにちは、質問があります
#> はい、どうぞ!
案3: 拡張引用
> [user] こんにちは、質問があります
> [ai] はい、どうぞ!
案1のコンテナブロック記法 (:::) は Hugo や VuePress で採用されている形式で、複数行の発言に対応しやすく、既存のMarkdown(見出し#、引用>、リスト-等)と衝突しません。
Hugo も VuePress も使ったことがなく、知らなかった。軽く調べてみるとこれとかのことらしい。
書くのが楽そうなのは行頭記号だが、気になる点を聞いてみる。
行頭記号を採用する場合、改行の解釈が難しくなりますか?
はい、行頭記号の場合は複数行の発言の扱いが難しくなります。
(中略)
コンテナブロックなら開始・終了が明確なので、中に複数行・リスト・コードブロックなど何でも入れられます。パースも正規表現一発でシンプルです。
予想通りの返答だったので、無難そうな「コンテナブロック記法」で実装してもらう。追加でいくつかの指摘。
チャットバブル内で :::user, :::ai が書かれた場合はどうなりますか?
正規表現は「非貪欲マッチ」なので、最初に見つかった ::: で閉じてしまいます。つまり、(現状の実装だと):::ai の ::: が閉じタグとして消費され、残りが壊れます。
やっぱりこういうところは詰めが甘い!修正させる。
改善しました!これで以下のようにコードブロック内の ::: は安全に無視されます:
そのあと、いくつかの小競り合いをして完成。いいかんじです。