5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PyxelAdvent Calendar 2024

Day 22

カスタムタグを使った Web 版 Pyxel の動作を理解する(2)

Last updated at Posted at 2024-12-21

Web 版 Pyxel の仕組みについて理解する下記の記事の続きとなります。

本記事では、 pyxel-edit について調べてみます。

なぜこのような調査をしようと思ったのか

サムライアプスという団体名で、Pyxel を使った写経スタイルのプログラミング教育ウェブサービス「Code & Magic」(コードアンドマジック)を開発しています。

本記事は、このサービスの開発に際して調査したことのまとめとなります。

Pyxel Editor とは

まず、Pyxel Editor についておさらいします。

Pyxel には、スプライト画像や音楽、効果音のデータをまとめたPyxel リソースファイルというデータ保存形式が用意されています。
このリソースファイルを編集するために用意されているアプリが「Pyxel Editor」です。

通常のターミナルで実行する場合はこんな感じになります。

pyxel edit resource.pyxres

PyxelEditor.png

面白いのは、この Pyxel Editor も Pyxel で作られたアプリケーションであるという点です。
ですので、エディタだからといって特別なことはなく、ゲームなどを作る場合と同じメカニズムで動作していると考えればよいでしょう。

そういえば、昔ベーマガにスプライト編集アプリを投稿したことがあったことを思い出しました。(遠い目)

Web 版の pyxel-edit

さて、この Pyxel Editor も Web ブラウザ上で実行できます。

<html>
<body>
    <script src="https://cdn.jsdelivr.net/gh/kitao/pyxel/wasm/pyxel.js"></script>
    <pyxel-edit name="resource.pyxres"></pyxel-edit>
</body>
</html>

立ち上げるには、前回記事同様サーバーからホストされる必要があります。

ローカルのターミナルなら、下記のようにして、

python3 -m http.server

そして、ブラウザで、下記の URL で開けば OK です。

http://localhost:8000/

このあたりの手順は pyxel-runpyxel-play と同じです。 Pyxel Editor も Pyxel アプリケーションの1つであることを考えれば、同じメカニズムで動くということは自然に理解できます。

Web 版のデータ保存の仕組み

さて、 Web 版 Pyxel Editor で気になるのは、データ保存についてです。
Pyxel Editor はリソースの編集をするアプリですので、データ保存ができなければなりません。

一般的に、Web ブラウザで動作するアプリはユーザーのPCのファイルシステムにアクセスすることはできません。これは、悪意あるサイトがユーザーのファイルに自由にアクセスできてしまうことを防ぐためです。

まずは挙動を調べてみましょう。

デフォルト挙動はダウンロード

Pyxel Editor のセーブアイコンをクリック、または Ctrl + S を入力すると、編集結果がダウンロードされます。そう、 Web サイトを見ているときのダウンロードと同じです。

image.png

どのように実現されているのか、深堀してみます。

Pyxel Editor アプリの保存ボタンをハンドリングしているのは App.__on_save_button_press() です。

    def __on_save_button_press(self):
        pyxel.save(self._resource_file)

pyxel.save() というメソッドを呼んでいるだけですね。
このメソッドは公式にはドキュメントされていない、上級者向け APIのひとつとなっています。ですので、ここからはソースコードを読み解いていきましょう。

pyxel.save()メソッドの実体を探す

pyxel.save() の実体を探してみましょう。

Rust 言語で書かれた、pyxel/rust/pyxel-engine/src/resource.rs にありました。

    pub fn save(
        &mut self,
        filename: &str,
        exclude_images: Option<bool>,
        exclude_tilemaps: Option<bool>,
        exclude_sounds: Option<bool>,
        exclude_musics: Option<bool>,
        include_colors: Option<bool>,
        include_channels: Option<bool>,
        include_tones: Option<bool>,
    ) {
       ...
    }

実は Python のライブラリは、C/C++ や Rust で主要な処理を実装して、Python から呼び出すためのバインディングと組み合わせて作ることができます。目的は、主に高速処理で、Python の標準ライブラリや多くの著名なライブラリもこういった方式で作られています。

Pyxel には、さらにゲームエンジンとしての高速動作だけでなく、グラフィックデバイス、サウンドデバイスといった、OSの機能にアクセスする目的もありそうです。

このメソッドの引数リストから次のようなことが推測されます。

  • 最初の引数 filename に、保存先ファイル名を指定する。省略不可。
  • それ以降は、リソースファイル内の画像、タイルマップ、SE、BGMなどそれぞれを含むかどうかのスイッチで、省略できる。

さらに動作を深堀してみましょう。

emscripten を通じて JavaScript を実行

引数リストの後ろはこんな感じです。

    pub fn save(
        ...
    ) {
        let toml_text = ResourceData2::from_runtime(self).to_toml(
            exclude_images.unwrap_or(false),
            exclude_tilemaps.unwrap_or(false),
            exclude_sounds.unwrap_or(false),
            exclude_musics.unwrap_or(false),
            include_colors.unwrap_or(false),
            include_channels.unwrap_or(false),
            include_tones.unwrap_or(false),
        );
        let path = std::path::Path::new(&filename);
        let file = std::fs::File::create(path)
            .unwrap_or_else(|_| panic!("Failed to open file '{filename}'"));
        let mut zip = ZipWriter::new(file);
        zip.start_file(RESOURCE_ARCHIVE_NAME, SimpleFileOptions::default())
            .unwrap();
        zip.write_all(toml_text.as_bytes()).unwrap();
        zip.finish().unwrap();
        #[cfg(target_os = "emscripten")]
        pyxel_platform::emscripten::save_file(filename);
    }

ほとんどは、リソースファイルのフォーマットである TOML の操作と、それを Zip する処理ですね。

注目は最後の行です。

        #[cfg(target_os = "emscripten")]
        pyxel_platform::emscripten::save_file(filename);

またどこかの save_file() というメソッドを呼んでいます。

/rust/pyxel-platform/src/emscripten.rs#L63 にありました。
_savePyxelFile('{filename}'); という文字列を渡して、スクリプトを実行するのでしょうか。

pub fn save_file(filename: &str) {
    run_script(&format!("_savePyxelFile('{filename}');"));
}

下請けの run_script()も同じファイルにあります。
さらに emscripten_run_script() というメソッドを呼び出しています。

pub fn run_script(script: &str) {
    let script = CString::new(script).unwrap();
    unsafe {
        emscripten_run_script(script.as_ptr());
    }
}

emscripten_run_script は、emscripten というツールが用意しているメソッドで、説明は下記にあります。

emscripten とは、 C/C++ のソースコードから Web Assembly (WASM) のバイナリを生成するツールです。 Pyodide は CPython がベースなので、 emscripten が使われているということでしょう。一方、 Pyxel は Rust を使っています。元の言語が違うのに WASM を通じて相互運用できるというのは、よくできていて、面白いですね。

少々引用します。

Emscripten provides two main approaches for calling JavaScript from C/C++: running the script using emscripten_run_script() or writing “inline JavaScript”.

(ChatGPT による翻訳)

Emscripten は、C/C++ から JavaScript を呼び出すための主なアプローチとして、emscripten_run_script() を使用してスクリプトを実行する方法と、「インライン JavaScript」を記述する方法の2つを提供しています。

つまり、C/C++からJavaScriptを呼び出したいようです。

もう少し調べてみます。

pyxel.js に、 _savePyxelFile() というメソッドがありました。
このファイルは、HTMLで読み込んでいたアレですね!

<html>
<body>
    <script src="https://cdn.jsdelivr.net/gh/kitao/pyxel/wasm/pyxel.js"></script>
    <pyxel-edit name="resource.pyxres"></pyxel-edit>
</body>
</html>

_savePyxelFile() の中身はこのようになっています。
/wasm/pyxel.js#L227

  _savePyxelFile = (filename) => {
    let a = document.createElement("a");
    a.download = filename.split(/[\\/]/).pop();
    a.href = URL.createObjectURL(
      new Blob([fs.readFile(filename)], {
        type: "application/octet-stream",
      })
    );
    a.style.display = "none";
    document.body.appendChild(a);
    a.click();
    setTimeout(() => {
      document.body.removeChild(a);
      URL.revokeObjectURL(a.href);
    }, 2000);
  };
}

アンカータグを動的に作成するという、 Web サイトで自動的にファイルをダウンロードさせるときの典型的な実装方法が書かれています。

これで一連の挙動がつながりました。

まとめ

まとめると、一連の挙動は次のようになっているようです。

  1. Pyxel Editor で保存ボタン押下で、Pyxel ライブラリ内の pyxel.save() を実行
  2. Pyxel ライブラリ内の pyxel.save() が、Emscripten の emscripten_run_script() に対して、JavaScriptの _savePyxelFile() 呼び出しを依頼
  3. pyxel.js 内の _savePyxelFile() が実行されて、リソースファイルをダウンロード

ファイルの保存というのは、OS や実行環境によって具体的な処理が異なります。ですので、Pyxel ライブラリ内に直接実装するのではなく、プラットフォーム側である JavaScript に処理してもらう仕組みになっているのだと思います。

ここまでわかれば改造もできそうです。

たとえば、私の場合は前述のとおり Web サービスに利用したいので、 pyxel.js_savePyxelFile() を改造すれば Web サービス側に送信することができそうです。


また改造にチャレンジしてみようと思っています。
今回はここまで。

これにて御免!

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?