この roadmap は実装手順ではなく、各段階で達成したいことを並べる。
具体的な API shape や実装方法は、その phase に入ってから code と sample で決める。
完了した phase・項目は本文から削る(経緯は git log と docs/log/ にある)。
- Phase 0: PoC 脱却
- Phase 1: NGS
lub 開発の駆動力は、自作ゲーム 4 本の完全移植と配信に置く。 どの phase も完了の定義は「win / web のビルド済みパッケージが配信されている」。 移植の流儀(理想設計で書き、忠実に写すのは gameplay rule だけ)は CLAUDE.md のとおり。
対象と原典(いずれも ghq に clone 済み):
| ゲーム | 原典 | 内容 |
|---|---|---|
| ngs | neguse/ngs (2004, C + SDL, 復元ソース) | 2D シューティング。samples/ngs に移植済み |
| hakonotaiatari | neguse/hakonotaiatari (2012, C++ + DirectX, 約 2,700 行) | マウス操作のアクション。効果音あり |
| SuperJumpAndDashMan | neguse/SuperJumpAndDashMan (LÖVE, Lua 約 2,900 行) | 2D プラットフォーマー。Tiled マップと効果音あり |
| ld44 | neguse/ld44 (2019, Go + Ebiten, 約 1,000 行) | パズル cut'n'align。BGM と効果音あり |
順序は出荷基盤を最短で通せる ngs を先頭にする。以降の順序は原典の規模と runtime 需要を見て入れ替えてよい。
達成したいこと:
- native release パッケージ: Haxe compiler / watch / dev server に依存しない 単体実行物(exe + コンパイル済み Lua + assets)を 1 コマンドで作れる。
- web release パッケージ: dev server 無しで静的ホスティングに置くだけで動く バンドルを 1 コマンドで作れる。
- 配信先(第一候補 itch.io)の実環境で WebGPU が動くことを最初に検証する。
- ngs を製品品質にする: hiscore 永続化、fullscreen、起動・終了・エラー時の挙動。
- 配信するゲーム本体を lub リポジトリに置き続けるか、別リポジトリへ分けるかを
決める(
docs/serve.mdの外部リポ運用が下地)。
完了の目安:
- ngs の win / web パッケージが配信されている。
- パッケージ生成が CI で再現できる。
達成したいこと:
- 3D game の最小構成を lub と lubx の上で自然に書ける(lubx は使ってよい)。
- depth、render target、camera、mesh-like draw、per-object parameter を一貫して扱える。
- マウス操作(カーソル表示の制御を含む)と効果音を gameplay loop の中で扱える。
完了の目安:
- hakonotaiatari の win / web パッケージが配信されている。
達成したいこと:
- action game の loop を lub と lubx の上で自然に書ける。
- physics、contact、action input、camera、audio event を runtime に game 固有の機能を足さずに扱える。
- gameplay reload 時にも runtime resource lifetime が壊れない。
- Tiled のような外部マップデータを asset として扱える。
完了の目安:
- SuperJumpAndDashMan の win / web パッケージが配信されている。
達成したいこと:
- パズルゲームを BGM 込みで扱える。
- スマホブラウザ対応を必須とする。対象は WebGPU が動く端末のみでよい
(
docs/design.mdの配布対象と整合)。タッチ操作を扱える。
完了の目安:
- ld44 の win / web パッケージが配信されている。
このへんは今後扱う予定の領域として持っておく。各項目は上の 4 本のどれかが 引くときに着手する。どのゲームも引かない項目は、需要が出るまで作らない。
- Save 永続化(ngs の hiscore が引く)。前提となる web の実態: Safari は
「7日間未訪問で script-writable storage 全削除」が現役で
persist()が 免除になるか Apple 未回答、itch.io 埋め込みは更新のたびに配信 origin が 変わり旧セーブに到達不能。よって「ブラウザ保存はキャッシュ、恒久保証は ユーザー手元のファイル」と割り切る。core API は KV:save_write(key, bytes)/save_read(key) -> status, bytes(status は request_file と同型の pending/ready/missing/error)/save_delete(key)/save_keys()。 native は save dir(ゲーム id ごと)に 1 key = 1 file で即 flush。 web は IndexedDB に 1 record で書き込み毎に即 commit、起動時にnavigator.storage.persist()を要求し全 key を先読み(実用上は起動直後に ready)。localStorage は採らない(5MiB 制限で eviction 耐性は IndexedDB と 大差なく二重管理になるだけ)。export/import は core API にしない (native は save dir を直接触れる。web の blob download / file picker は app/JS 側の責務)。core の契約は「永続を保証する」ではなく 「プラットフォームの storage に書く」に留める。 - Window 制御(ngs の fullscreen と hakonotaiatari のカーソル制御が引く)。
title / fullscreen / cursor(表示・グラブ)を実行中に動的に
変えられること(カーソルキャプチャは FPS カメラ系で必須)。API は宣言型
window({title, fullscreen, cursor})を毎フレーム宣言し、runtime が実状態を 収束させる(voice と同型、hot reload 後もコードが真)。ただし web の fullscreen / Pointer Lock はユーザージェスチャ必須なので「宣言 = 要求」とし、 runtime は入力のあったフレームまで適用を保留、実状態はwindow_info()snapshot(focused, fullscreen, width/height, dpi)で読む2層契約にする。 cursor は visible / hidden / grabbed(= relative mode / Pointer Lock)の3値。 native は SDL3 直、web は document.title / Fullscreen API / CSS cursor / Pointer Lock に張る。vsync は native のみ config で(web は rAF 固定)。 初期 width/height/backend は従来どおりconfig(onInit 専用)に残す。 - Gamepad 入力(SuperJumpAndDashMan が引く候補)。core の「外部状態の
snapshot」として polling API を持つ:
pads()(接続 slot 一覧)、pad_down/pressed/released(slot, button)、pad_axis(slot, axis)、pad_info(slot) -> {connected, mapping, name}。 button/axis 名は standard layout で正規化(a/b/x/y,dpad_*,lb/rb,lx/ly/rx/ry/lt/rt)。native は SDL3 SDL_Gamepad(mapping DB が XInput/DInput/HIDAPI を吸収)、web は Gamepad APImapping="standard"。 slot は接続順に採番し、切断→再接続で同じ slot を維持する(hot reload 耐性)。 web は仕様上、接続済みでも最初のボタン入力までパッドが見えない (fingerprinting 対策)ので、「ボタンを押して」誘導は app 側の責務とし core はpads()で観測可能にするだけ。mapping != "standard"の機器 (Switch Pro 等の一部ブラウザ×OS)はリマップせずpad_infoで観測のみ。 振動は宣言型pad_rumble(slot, {low, high})(voice と同型: 毎フレーム宣言、 途切れたら停止)。native=SDL_RumbleGamepad、web=vibrationActuator "dual-rumble" を feature-detect し Firefox/iOS では no-op。 trigger-rumble・navigator.vibrate(iOS 不可で移植性なし)・raw joystick は作らない。 - Audio の将来の扉: レジスタ式の固定機能チップシンセ(audio callback 内 オンデマンド生成)、timestamp 付きイベントキューによるサンプル精度 シーケンス。4 本の効果音・BGM はまず既存の audio 経路で賄い、 チップシンセは需要が出るまで作らない。
- Ozz Animation による animation(引くゲームが今のところ無い)。
- TTF フォントの残り(引くゲームが今のところ無い。基盤の
lub.Font/lubx.Text/lubx.MeshTextは実装済み):- 行レイアウトの折返し・禁則: UAX #14 サブセットの禁則(日中)+ 単語折返し(FIGS/韓)。スコープの最大境界は「対応言語 = 表引き (cmap) で 正しく出る言語」で、シェーピングが要る Arabic/Indic 等は非対応と明言する (中途半端に出すと壊れた文字列になる)。
- fallback チェーン: web はシステムフォント列挙不可なのでフォント同梱前提。
- 絵文字画像シート: カラーフォント (CBDT/COLR) は読まず、画像シート (Twemoji / Noto Emoji の PNG、ファイル名 = codepoint 列)を既存の 画像→atlas 経路に貼る。ZWJ 合字・肌色・旗はシート索引への最長一致で 解決し、未知の列は構成要素に分解して個別表示にフォールバック。
- 未解決課題: 動的 CCJK 前提だと同梱フォントが重い(Noto CJK 級)。
native は許容できるが web 配信では言語別分割ロードや使用頻度サブセット
- フォールバックの工夫が要る。