作成日: 2026-05-15
このドキュメントは、PVNMで作成したビジュアルノベルゲームを Android / Web / Linuxハンドヘルドで動かすために実施した主な対応内容をまとめたもので、設計判断と実装範囲を共有するための要約です。
PVNMとは
PVNMは「Pyxel Visual Novel Maker」という仮名称であり、その名の通りビジュアルノベルを作るために特化したフレームワークです。
正規名称は「Palette Visual Novel Maker」になる予定です。
- 主な機能
- デジカメで撮影した写真やAIが生成した画像をそのまま取り込める
- 取り込んだ画像は自動的に256色へと減色される
- PVNM用のマークダウン形式で書かれた原稿をインポートできる
- 背景画像、キャラクター画像3枚を同時に表示可能
- セーブロード、オートプレイ、スキップ、ログ、条件分岐、コントローラーなど基本的なプレイ機能は実装済み
- エンディング画面では音楽の再生時間と画像の表示タイミングを同期できる
目的
- PVNMで作ったビジュアルノベルゲームを Android / Web で実行できるようにする
- PVNM自体の配布ではなく、あくまでも成果物であるゲームを配布することに重点を置いています
- 基本方針は
pyxel package->pyxel app2html-> Capacitor Android化 - Android / Webでは
pygameを使わず、JavaScript側の音声バックエンドでBGMを再生する - 音楽同期演出はPyxelのフレーム数ではなく、必ず音楽の
currentTimeを親時計にする - 画像の256色減色は、可能な限り実行時ではなくexport/build時のプリビルド処理へ寄せる
- Linux/plumOSなどのハンドヘルド機では、
.pyxapp実行時の/tmp展開容量問題を避ける
全体アーキテクチャ
PVNMで作成されたビジュアルノベルゲームの実行環境を大きく2系統に整理した。
特に44.1kHzの再生レートを維持するために、複雑な構造となっている。
- PC / Linux Python実行系
- Pyxel + Pythonランタイムで実行
- BGMは主に
pygame.mixer、macOSでは必要に応じてAVFoundationバックエンドを利用 - 保存先は共通ランタイム保存先へ集約
- Web / Android実行系
-
pyxel app2htmlでPyxel Web / Pyodideアプリ化 - CapacitorでHTML一式をAndroid WebViewへ同梱
- 音声、保存、Android Back、診断ログなどはJavaScriptブリッジ経由で扱う
- BGMの再生位置はJavaScript側Audio APIの
currentTimeをPython側へ返す
-
Export対応
PVNMでは各OSでゲームを動作させるためのエクスポート機能を実装している。
SETTINGS画面にExportメニューを追加し、ショートカットキーだけに依存しない運用へ移行した。
-
.pyxappexport -
.pyxapp + assetsexport - Web HTML export
- Capacitor Android project export
- Android debug APK export
- Android release APK export
- macOS
.appexport
エクスポート中は進捗表示を出し、同時に複数exportを押せないようにした。
未保存のプロジェクトファイル .pvnm をexportしようとした場合は警告を出す。
Android / Capacitor対応
Capacitorは、HTML、CSS、JavaScriptを使用して、iOS、Android、Webでネイティブに動作するアプリを構築するためのクロスプラットフォーム・ランタイムです。
CapacitorプロジェクトをPVNMから生成。
生成物の主な内容:
www/index.htmlwww/assets/app/index.pyxappwww/assets/audio/www/assets/image_cache/www/pvnm_diagnostics.htmlwww/pvnm_debug.htmlbuild_android.shserve_web.shandroid_log.shpvnm_configure_android.pyREADME_PVNM_CAPACITOR.mdPVNM_ANDROID_TEST_CHECKLIST.mdPVNM_ANDROID_LOGS.mdPVNM_EXPORT_REPORT.md
Androidネイティブ側では、生成後に pvnm_configure_android.py を自動適用する。
主な設定:
- 横画面固定
- fullscreen / immersive sticky
- WebView背景を黒に固定
- hardware acceleration有効
- APK更新後のWebView HTTP cacheクリア
- Android Gradle Pluginを8.8.0へ固定
- Capacitor / AndroidX / Kotlin依存のバージョン衝突回避
Web / Android音声対応
PC版の pygame 依存を分離し、AudioBackend 方式に整理した。
- PC版
PygameAudioBackend- 44.1kHz / 16bit / stereo を維持
- plumOSでのSDL_mixer読み込み順問題に対応するため、Pyxel初期化前にpygame音声を初期化
- macOS補助
AVFoundationAudioBackend- PyInstaller配布時にpygameなしでも44.1kHzステレオ再生できる経路を追加
- Web / Android版
WebAudioBackend-
web/pvnm_audio_bridge.jsのPVNM_AUDIOへ委譲 - WebAudio / HTMLAudioでBGM再生
- Android WebViewの自動再生制限に備え、ユーザー操作でAudioContextをunlock
- BGM再生位置を
get_bgm_pos_sec()経由でPython側へ返す
重要な設計判断:
- Android / Webの同期演出はPyxelフレーム数を信用しない
- 常に音楽の
currentTimeを親時計にする - フレーム落ちしても、次の描画で正しい画像・表情・演出状態へ追いつける
画像キャッシュ / アセット対応
開発時に生成された画像キャッシュは維持したうえで、エクスポート時に元画像がなくても実行できる「プリビルド画像アセット」を追加した。
元画像を含めると配布ファイルサイズが肥大化する可能性がある一方、元画像を配布物に含めておくことにもメリットはある。
それは、キャッシュ側に何らかの問題が発生した場合でも、ゲームプレイ中にリアルタイムでキャッシュを再生成できるため、「画像が表示されない」という致命的なトラブルを回避できる点だ。
実際にどのようなケースでキャッシュ問題が起きるかは現時点で想定できていないが、あくまでフォールバック(安全対策)の一環として、元画像を含める選択肢を残した。
主な内容:
-
image_manifest.jsonを生成 - パレット別に
.npyキャッシュを生成 - 同じ元画像でも
palette_hashが異なれば別キャッシュとして扱う - 元画像が存在する場合は従来通り読み込み・キャッシュ生成
- 元画像が存在しない場合はmanifest経由で
.npyを読み、pyxel.Imageに復元 - Web / Androidでは画像cacheを外部アセット化し、必要に応じて読み込む
- 実行時に不要な原寸cacheを減らすため、PVNMの表示解像度に合わせたサイズを優先
これにより、Android/WebではPillow/NumPyによる実行時減色依存を減らし、配布物はプリビルド済みcache中心にできる。
Linux / plumOS対応
通常の .pyxapp は pyxel play 実行時に /tmp へ展開されるため、大量画像・音声アセットを同梱すると /tmp が枯渇する可能性がある。
そのため、Linuxハンドヘルド向けに .pyxapp + 外部assets exportを追加した。
-
.pyxappはEmulationStationなどから従来通り起動可能 - 画像cache、音声、大きなアセットは隣接フォルダへ外出し
-
/roms/pyxel/のような環境依存パスでも、.pyxappから相対的にassetsを見つける - 必要に応じて
PVNM_ASSET_ROOTでも外部assets位置を指定できる
保存先
プレイ中に生成されるデータを、実行環境ごとに保存できるように整理した。
保存対象:
- セーブデータ
- コントローラー設定
- プレイヤー設定
- テキストサイズなどのプレイ中設定
保存先:
- macOS / Linux / plumOS
- 実行ユーザーの
~/.pvnm/
- 実行ユーザーの
- Web / Android
- WebView / browser の
localStorage - namespaceはゲームごとに分離
- WebView / browser の
コントローラー対応
plumOS系ハンドヘルドやAndroidゲーム機で遊べるよう、プレイヤー操作をコントローラー対応した。
現在の標準割り当て:
- A: 選択肢決定 / 次ページ
- B: 一つ前へ戻る
- X: AUTO切り替え
- Y: OPTを開く
- START: SAVEを開く
- BACK: ダイアログ非表示
- L1: LOGを開く
- R1長押し: SKIP
- BACK + START: PVNM終了
OPTにはHELPを追加し、現在の操作説明を大きめの文字で表示するようにした。
コントローラー設定は、ABXYの物理配置が機種ごとに異なる問題に対応するため、ウィザード形式にした。
ボタンごとの個別変更UIは混乱しやすいため簡略化している。
小型画面向けUI調整
4インチ前後のLinuxハンドヘルドでは、通常UI文字が小さすぎるため、プレイヤー側の文字サイズを重点的に調整した。
対応内容:
- SAVE画面の文字を大型化
- OPT画面の文字を大型化
- コントローラー設定画面の文字を大型化
- HELP画面を追加
- 入力待機マークを
vから下向き三角相当の表示へ変更 - 入力待機マークをダイアログ右下付近に表示し、14pxで点滅
- BDFフォントの17〜23pxを追加生成
- OPTからプレイ中にテキストサイズを変更可能にした
- 17〜23pxでは本文文字間隔を追加し、文字同士が詰まりすぎないようにした
BDF / テキスト描画の前提
PVNM全体の文字描画は元々TTFフォントを利用して描写するようにしていたが、UIデザインが崩れたり、折り返し判定が曖昧になるなどの問題があったため、最終的にBDFへ統一済みとなる。
- エディタプレビューと実プレイで同じ文字幅・同じ折り返しになる
- word-wrapは共通処理を使用
- スピーカーカラーやプレイ用カラーロールがexport後ランタイムにも適用されるよう修正
- 追加BDFサイズとして17〜23pxも利用可能
診断 / ログ
Android実機やWebViewでの問題切り分け用に、診断導線を追加した。
これはAndroid実機側での試験を行うにあたって必要な機能でり、特にオーディオ周りの処理や起動時間が重要となるので、最重要項目として位置している機能の一つ。
人間の目で各状況を判断できるHUDを搭載しつつ、ログとしても保存しておき、のちに分析するために利用している。
-
pvnm_diagnostics.html- Storage bridge
- Audio bridge
- AudioContext state
- BGM currentTime
- Input bridge
- Android Back listener
- pointer/touch/click counts
-
pvnm_debug.html- 実ゲームをdebug HUD付きで起動
- HUD内容をlocalStorageへ保存し、テキストログとして取得可能
-
android_log.shpvnmwebviewaudiobackcrash
-
PVNM_STARTUPログ- 起動時間、初回描画、BGM開始などを比較できるようにした。
実機検証メモ
テストデータで作成されたビジュアルノベルゲームで動作確認したので、具体的な数値の表記は避けている。
端末同士の数値から見て相対的にどうだったか? という視点で記録しています。
確認済み端末:
- GAMEMT EX8 / G99
- 起動時間はおおむね許容範囲
- AYN Thor / QCS8550
- 起動が比較的速い
- MANGMI AIR X / SM6115
- 体感起動はやや遅め
- GAMEMT E5 Ultra / T620
- 初回にWebView/Trichrome更新の影響でアプリがkillされた
- 再実行後は正常動作
- WebView更新中のkillはPVNM本体のクラッシュではなく、Android側のWebView更新処理によるものと判断(要はアプデのタイミングとゲーム起動のタイミングが奇跡的に重なったことが原因)
- XU20 V30 / Allwinner A133 / Android 10 / WebView 74
-
pyxel.jsのoptional chainingを古いWebViewが解釈できず起動不可 - 黒画面の代わりに非対応メッセージを表示するガードを追加済み
- 無理に対応しとすると複雑化してバグりやすいので、ある程度切り捨てる覚悟
-
WebView互換性対応
古いAndroid WebViewでは、Pyxel Webランタイム自体をparseできない場合がある。
対応内容:
-
web/pvnm_webview_compat.jsを追加。 -
pyxel/pyxel.js読み込み前にWebView互換性を検査。 -
Promise,fetch,WebAssembly,TextDecoder, modern JavaScript syntaxを確認。 - 非対応ならゲームを起動せず、Android System WebView / Chrome更新を促す画面を表示。
-
launchPyxel()をstub化し、後続エラーで画面が壊れないようにした。
現時点の推奨環境:
- Android 11以上推奨
- ただし本質的にはAndroid OSバージョンではなく、Android System WebView / Chromeの新しさが重要
- WebView 74は非対応とする
配布形式ごとの考え方
- macOS
.app- 基本的にApple Silicon向け
- Intel Mac対応には別途x86_64またはuniversal2ビルドが必要
- Windows
.exe- Windows側でPyInstallerビルドする必要がある
- macOSからWindows
.exeは原則作らない
- Linux / plumOS
-
.pyxapp + assetsが現実的 - EmulationStationから
.pyxappを選ぶ運用を維持できる -
/tmp枯渇回避のため、大きなassetsは外部フォルダへ置く
-
- Android
.apk- Capacitorプロジェクトからdebug/release APKを生成
- 動作確認段階はdebug APKで十分
- GitHub等で配布するなら、将来的にはrelease署名ワークフローを整える
- Web HTML
-
serve_web.shでローカル確認可能 - 実配布ではHTTPサーバー上に置く形が自然
-
重要な設計判断
- Android/WebのBGM同期は
currentTimeが正 - Pyxelフレーム数は同期基準にしない
- 画像減色はexport/build時に寄せる
- ランタイムでは可能な限りPyxel本体と軽いロジックだけで動かす
- WebViewの古さはOSバージョンだけでは判断しない
- Linuxハンドヘルドでは
.pyxapp単体同梱より、.pyxapp + assetsが安全 - 小型画面では本文以外のUI文字も大きくする必要がある
残っているリスク / 今後の確認項目
- 完成版データで画像・音声が数百件になった場合の起動時間計測
- 低スペックAndroid WebViewでのPyodide/Pyxel初期化時間
- 大量assets時のWebViewメモリ使用量
- Android release署名の正式運用
- GitHub公開時のREADME整備
- Windows版exportのWindows実機検証
- iPhone/iOS対応は優先度低
- 技術的にはCapacitor/iOSの可能性はあるが、Pyxel Web/Pyodide/Audio制限を別途検証する必要がある
現時点の到達点
「PVNMで作ったゲームをAndroidでプレイできるようにする」ための主要な土台は実装済み。
現時点で確認できていること:
- Android実機で起動する
- 画像が表示される
- BGMが44.1kHz相当で再生される
- 音楽再生位置を親時計にできる
- タッチで進行できる
- コントローラーで基本操作できる
- SAVE / LOG / OPTが使える
- セーブや設定が保存される
- 古いWebViewでは黒画面ではなく非対応メッセージを出せる
- Linux/plumOS向けには
.pyxapp + assetsで/tmp枯渇リスクを下げられる
次の大きな作業は、完成版に近い実データを入れて、起動時間・メモリ・長時間プレイ・セーブ/ロード・終了/再起動を再検証すること。