Schemeで作ったゲームをどうやって配布するか考える

追記: Gaucheのパス挙動部分をstrike。see comment。
かなり気の早い話しだけど、どうやってSchemeで書いたゲームを配布するかどうかを考える。実はpygameとかDXRuby、Love( https://love2d.org/ )のような既存のフレームワークでもこの部分をあまりリッチに考察しているとは言えないので、それなりに新しいアイデアが必要になる。
yuniは既に10を超える( https://github.com/okuoku/yuni#implementations )Scheme処理系で(ゲームを書くのに必要な)FFI抽象化層を持っているので、原理的にはこれらの処理系全部で動作させる可能性を検討できる。

やりたいこと

  • ゲーム本体を(スクリプトエンジンとしての)Schemeで開発し、
  • GitHubからチェックアウトしてきたリポジトリのファイルをダブルクリックするだけで遊ばせたい

意外とこのポイントにフォーカスしたゲームプロジェクトは少く、だいたい一度は明示的なビルドを挟む必要がある。既存の同様なゲームフレームワークは.exeのような実行可能形式の配布はよく考察しているものの、ソースコード形態での実行は良い考察がない。(これはElectronとかnw.jsのような最近流行のデスクトップJavaScriptでも同様と言える気がする)
処理系と"ランチャ"、つまり処理系のフロントエンドとなるGUIアプリはインストーラで配布しても良いことにする。 ...実は最近のWindowsにはpowershellが付いてくるし、macOSにはRubyが有るので、原理的には依存関係ゼロで諸々を実装することも不可能ではないが。。

むずかしいこと

チャレンジとなりそうなのは、PygameとかDXRubyのような単一処理系を想定したシステムと違って多数の処理系を同時にサポートする点。ChezやSagittariusのようにロードパスさえ指定すれば高速に動作する処理系もあれば、LarcenyやRacketのように明示的にバイトコードコンパイルしないとロードが遅い処理系もある。
逆に言えば、動作する処理系の数では世界記録を狙えるかもしれない(!)

アプリケーションの構造

現状の想定では、ゲームはローカルなSchemeライブラリ(R6RSフォーマット)の集合として書かれ、必要に応じて処理系ネイティブのフォーマットに変換した後に実行されることになる。

+ <root>
 + lib/          -- ゲームを構成するR6RS形式ライブラリ
   - gamescene-actionstage.sls -- アクションシーンの描画ループ実装
   - gamescene-stageselect.sls -- ステージセレクト画面の描画ループ実装
       :
 + assets/       -- アセットデータ
   - texture.png
       :
 - game.yuniapp -- 起動用のメタデータ(S式)
 - main.sps     -- エントリポイント

ランチャは.yuniappを読み、ライブラリの依存関係を必要に応じてセットアップしてからゲーム本体を起動する。
... 要するにこのランチャにどのような機能が必要なのかが問題の中心になる。処理系毎にライブラリの置き方や中間コード/ネイティブコードへのコンパイル手法等が丸ごと異なるため。
ゲーム本体をR6RSライブラリで書くのは:

  • ChezSchemeやLarcenyのような有力な処理系が直接サポートしている
  • R7RSライブラリは処理系の方言がきつく、アプリケーション配布に向かない
  • R6RSライブラリをR7RSライブラリにexpandさせるのは比較的容易だが逆は難しい

といった理由に因る。yuniでは既にライブラリの記法はR6RS形式で開発し、語彙はR7RS相当にするという手法で処理系間の互換性を実現しているので、同じ考察が流用できる。

処理系の分析

とりあえず以下の処理系をTier1として、これらの処理系での動作を中心に考える。
(nmoshはそもそもアプリケーション組込みが最初からサポートされているので追加の考察はしない。)

これらの処理系はWindowsインストーラが有る(Chezには現状無いが、商用時代には存在したのと、現在開発ブランチは存在するのでそのうちサポートされると仮定)。mit-schemeが選から漏れているのは、まだmit-schemeでyuniのFFIを実現する方法を思いついていないため(non-movableなオブジェクトがmallocでしか作れないので自前のGCが必要)。。
まず、SagittariusとChezSchemeはR6RSライブラリを直接ロードできるので単にライブラリパスを適切に指定して処理系本体を起動すれば良い。これは非常にシンプルなケースで、ランチャは処理系のパス(と、ライブラリパスの指定方法)さえ知っていれば良いことになる。
Racketはライブラリの先頭に #!r6rs を付与し、かつ、r6rs-libsがインストールされていないとR6RSライブラリをロードできない。通常のインストーラを使用すればr6rs-libsはインストールされているはずなので#!r6rsの付与だけが問題になる。ファイルを直接置換すべきかは何とも言えない。そもそもRacketはracoを使用して.exeの生成まで行えるため、可能な限りこのシステムに乗るように構成を考えた方が良いのかもしれない。racoのバイトコード(.zo)生成APIもプログラムから使用できる( https://docs.racket-lang.org/raco/API_for_Simple_Bytecode_Creation.html )。
GaucheはR7RSライブラリのみの対応でR6RSのライブラリは読み取れないため、ランチャは中間ライブラリを生成する必要がある。中間ライブラリから元のR6RSライブラリをincludeし、syntax-rulesでフォーマットの変換を行う。更に、Gaucheはincludeの相対パスがCのようなソース相対でない(See comment)( https://github.com/okuoku/yuni/blob/master/doc/sibr/SIBR0005-C-STYLE-INCLUDEPATH.md )ため、アプリの物理的位置が変わるたびにライブラリを再生成する必要がある。Gaucheには現状ではバイトコードキャッシュが無いので、単にloadするだけにすべきかもしれない。
Gambitでは先日の方法( http://d.hatena.ne.jp/mjt/20160825/p1 )を使用して完全にexpandしてコンパイルするしか無い。行番号情報を保持するために伝統的マクロとincludeを駆使するとか、マクロをエクスポートしないライブラリだけでも分割コンパイルできるようにするとか色々と考察すべきことは有るが。。
まとめると、ランチャに求められるのは:

  • システムにインストールされている処理系を検出し、ユーザに選択させる機能
  • アプリに含まれるR6RSライブラリを読み取り、各種処理系に対応したライブラリを出力する機能
  • 適切なコマンドラインオプションで処理系を起動する機能
  • GaucheのようにyuniのFFIにネイティブ拡張が必要な場合はそのネイティブ拡張
  • FFIで呼ばれるゲーム用のランタイムライブラリバインディング - 要するにSDL2のDLL

のパッケージということになる。
例えばゲームパッドの設定とかセーブデータ管理のようなゲームの共通インフラはランタイムの機能ということでランチャと一緒に配布する方が扱い易い気はする。

次の一手

とりあえず、パフォーマンスは度外視で一旦全部loadする方向で考える。つまりRacketのようなR6RSでは、repl環境でライブラリをevalする方向ということになる。...これも可能なのかどうかはやってみないと解らない(R6RSの規格にはrepl環境は規定が無い)けど、不可能だったらGambit同様全部連結で行けるはず。