プラットフォーム抽象化 — 同じ作品が Switch まで動く設計
作成日: 2026-06-10 / 更新日: 2026-06-10
kaedevn は「一度書けば、どこでも動く」を最初の設計目標に据えています。同じ作品(スクリプト+アセット)が、Web ブラウザ・ネイティブ(macOS / Windows / iOS / Android)・そして将来の Nintendo Switch で、作り直しなく動きます。これを支えるのが、プラットフォーム差を抽象境界の裏に閉じ込める設計です(Godot と同型の考え方)。
抽象の4本柱
ゲーム実行に必要な要素を、少数のインターフェースに分離しています。ロジック層(スクリプトエンジン)は、これらのインターフェースだけを通して世界と関わります。
| 抽象 | 役割 | Web 実装 | ネイティブ実装 |
|---|---|---|---|
| IInput | すべての入力を論理アクションに変換 | PixiJS のポインタ / キーボード | SDL_Event(Joy-Con / タッチ / キーボード) |
| IAudio | BGM / SE / VOICE をカテゴリ別に再生・音量管理 | Web Audio API | SDL_mixer |
| IStorage | セーブ / ロードの抽象化 | IndexedDB | ファイルシステム(JSON) |
| IOpHandler | コンパイル済み命令(Op)の実行 | WebOpHandler(PixiJS) | SDL2Engine + GLRenderer(OpenGL) |
IInput / IAudio / IStorage は packages/core に定義された必須3抽象、IOpHandler は Op 命令の実行を担う4本目です。KSC(コード記法)の VM はこれに加えて HostAPI(packages/ksc-compiler)を通じて描画・音声に委譲します。
物理ボタンの配置や音声 API の違いは、すべてこの境界の裏側に隠れます。作者は「OK が押されたらテキストを進める」というロジックだけを書けば、Web でも Switch でも正しく動きます。
Op[] / IR — プラットフォーム非依存の共通言語
抽象化のもう一つの柱が、プラットフォームに依存しない中間表現です。
- KS(タグ記法) は事前に Op[](命令配列)へコンパイルされ、OpRunner が 1 命令ずつ
IOpHandlerに委譲して実行します。 - KSC(コード記法) は型チェックを経て IR(バイトコード) にコンパイルされ、KscVM(Web は TypeScript、ネイティブは C++ 移植
ksc-vm-cpp)が同一の IR を実行します。
どちらも最終的に IOpHandler / HostAPI に収束します。同じ命令列が、プラットフォームを問わず共通です。だからこそ、描画・音声・入力の実装を差し替えるだけで、同じ作品が別環境で動きます。
KS → Op[] → OpRunner ┐
├→ IOpHandler 実装(Web: PixiJS / ネイティブ: SDL2+OpenGL)
KSC → IR → KscVM ────┘ (KSC は HostAPI 経由)
設計判断1: async と sync の分離
IOpHandler のメソッドは、完了を待つ必要があるもの(async) と 撃ちっぱなしでよいもの(sync) に分けています。
- async(
Promiseを返す):textAppend(クリック待ち)、choice(選択結果を返す)、bgSet/chSet(トランジション完了を待つ)、waitMs/waitClick/waitVoiceEnd(待機が目的)、battleStart(勝敗を返す)など。 - sync(
voidを返す):bgmPlay、sePlay、voicePlay、bgmStopなど、再生を開始すれば十分で完了を待たない処理。
この分離で、エンジン側の実装が素直になります。await が必要なのは「待つ意味がある処理」だけです。
await handler.textAppend("赤音", "おはよう!"); // クリック待ち
await handler.bgSet("school_day"); // フェード完了を待つ
handler.bgmPlay("morning_theme"); // 即座に次へ(await 不要)
handler.sePlay("door_open");
設計判断2: 最小実装でも動く
IOpHandler は多くのメソッドを持ちますが、全部を真面目に実装する必要はありません。テキスト表示・選択肢など中核メソッドさえ実装すればスクリプトは動き、立ち絵・エフェクト・カメラなどの拡張メソッドは任意(未実装なら no-op)です。
- 移植の参入障壁を下げる: 新プラットフォームへは、まず中核だけ実装すれば作品が動く。
- 段階的に積める: 背景 → 立ち絵 → 音声 → 演出、の順に実装を進められる。
- テストが容易: 本物の描画がなくても、検証用のハンドラを注入すれば、画面なしでロジック全体を CI で検証できる。
Switch の現在地
Switch はノベルゲーム路線の本命ターゲットで、設計は最初から Switch 移植を見据えています。
- 抽象境界は整備済み:
IInput/IAudio/IStorage/IOpHandler、課金抽象IBilling、そして同一 IR を実行するksc-vm-cpp。Switch 固有実装を差し込むだけで動く状態を目指しています。 - ネイティブエンジンは実機ビルド済み: macOS / Windows / Android は SDL2 + OpenGL で動作確認済み(iOS はシミュレータ)。Switch も同じ抽象の裏に同じエンジンを置く構造です。
- 公開ビルドは不可: Nintendo SDK は NDA 配下のため、公開リポジトリにツールチェーンや実装は含めません。移植は承認された開発環境で、コンソール移植サービスとして対応します。
これを成立させるため、以下は凍結(frozen)仕様です。
- 論理解像度 1280×720 + セーフエリア 5%
- セーブスキーマ v1(参照 ID のみ、画像・音声は埋め込まない)
- 入力アクション 10 種(OK / Back / Menu / SkipToggle / AutoToggle / Log / QuickSave / QuickLoad / Screenshot / HideWindow)
- Core コマンド集合(text / choice / jump / set / if / show / hide / move / fade / playBgm / playSe / playVoice / wait / overlay)。メインシナリオは Core のみで書け、Web 限定コマンド(openUrl / share / analytics 等)はメニュー・ナビゲーション限定で分離。
なぜここまで徹底するのか
- 全書き直しの回避 — 従来のノベル系エンジンは、プラットフォームごとにエンジンを書き直す悲劇が多発していました。抽象境界があれば、書き直すのは境界の裏だけです。
- AI 実装との相性 — 抽象が明確だと、AI エージェントはプラットフォームの細部を気にせず純粋なロジックに集中でき、バグが減ります。
- テストの容易性 — 本物の描画・物理ボタンがなくても、検証用ハンドラを注入するだけで CI 上で全ロジックをテストできます。
関連ドキュメント
- エンジンの仕組み — コンパイラ / VM / IOpHandler の流れ
- マルチプラットフォーム — 各プラットフォームの実装対比
- 対応プラットフォーム — 対応状況の一覧
- ソースコード説明書 06(packages/core) — 抽象インターフェースの実体
- ソースコード説明書 25(Switch 準備状況) — Switch の整備状況