概要
PyScript(Pyodide)がPygame-ceをサポートしたのですが、別の選択肢としてMicroPytonからJavaScriptゲームライブラリを使う方法もメモとして残しておきます。
JavaScriptライブラリの使い方
PyScriptでは通常のJavaScript同様にHTMLのscript要素のsrc属性からロードする方法とESModuleを使う方法どちらもサポートしています。
ESModuleを使う方法では静的なimportも動的なimportもどちらも可能ですが、PyScript独自の設定やAPIでロードする必要があります。
今回利用するp5.jsやPhaser3では、script要素のsrc属性を使う方法でロードするので、ESModuleについては別の記事にまとめようかと思います。
p5.jsもPhaser3もロードするとglobalThis(window)にそれぞれp5やPhaserオブジェクトが追加されています。PyScriptではjsモジュールをインポートするとjs
がglobalThis(window)相当の変数になるので、これを通じてp5やPhaserオブジェクトにアクセスすることでJavaScriptライブラリを利用する形になります。
p5.jsを使う
p5.jsで赤い円が勝手に動くだけの単純なサンプルを作ってみました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://pyscript.net/releases/2025.3.1/core.css" />
<script type="module" src="https://pyscript.net/releases/2025.3.1/core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@1/lib/p5.js"></script>
</head>
<body>
<script type="mpy" src="main.py"></script>
</body>
</html>
import js
def sketch(p5):
def setup():
global count
count = 100
p5.createCanvas(500, 500)
def draw():
global count
p5.background(0)
p5.fill(255, 0, 0)
p5.ellipse(count + 100, 250, 100, 100)
count += 1
count %= 300
p5.setup = setup
p5.draw = draw
js.p5.new(sketch)
HTMLのscript要素でp5.jsを指定してロードするようにしていれば、js.p5
でp5.jsのAPIを呼び出せるようになります。
自分がプログラミングした範囲ではPyScriptだからと特に気を付ける要素はほとんどありませんでした(コールバック関数くらい)。JavaScriptとほぼ同じ感覚だと思います。
MicroPythonではPyodideと違い、JavaScriptにPythonのコールバック関数を渡す際にproxyを作る必要はないらしいので、そのまま関数を渡せます。
コールバック関数はJavaScriptと違い、PyScript側の関数の引数の数がJavaScriptから呼び出した時の引数と異なると実行時エラーになるようなのでそれだけは気を付ける必要があります。
Phaser3を使う
p5は機能が限られているので、もっと機能豊富なPhaser3も使ってみることにしました。
Phaser3ではGraphicsやSprite等はコンストラクタにオブジェクトを渡して設定する方法が一般的のようですが、PyScriptの辞書をそのままインスタンスに渡すとPhaser3が例外を送出して動かないので、pyscript.ffi
モジュールのto_js
関数で変換する必要があるようです。PyScriptの辞書とJavaScriptオブジェクトは厳密には違うものなのでPhaser3内部でチェックする際に弾かれているようです。
また、Phaser3の要となるシーンはSceneクラスを継承してサブクラスを作るのが一般的なようですが、PyScriptではJavaScriptクラスをそのまま継承することはできないようです。幸い、Phaser3は継承するほかにもSceneコンストラクタにオブジェクトを渡して一括設定したり、Sceneクラスを直接インスタンス化してそのプロパティに代入する方法でもコーディングできるので使えないというわけではないです。ここではSceneクラスを継承せずインスタンス化してそのプロパティに代入する形でコーディングしています。
p5.jsと同じように赤い円が動くだけのサンプルを作ってみました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://pyscript.net/releases/2025.3.1/core.css" />
<script type="module" src="https://pyscript.net/releases/2025.3.1/core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/phaser@3/dist/phaser.js"></script>
</head>
<body>
<script type="mpy" src="main.py">
</script>
</body>
</html>
import js
from pyscript.ffi import to_js
Phaser = js.Phaser
scene = None
cursor = None
graphics = None
count = 100
def create(data):
global cursor, graphics
cursor = scene.input.keyboard.createCursorKeys()
graphics = scene.add.graphics(to_js({'fillStyle': {'color': 0xff0000}}))
def update(time, delta):
global count
graphics.clear()
graphics.fillCircle(count + 100, 250, 50)
count += 1
count %= 300
scene = Phaser.Scene.new('SampleScene')
scene.create = create
scene.update = update
config = {
'type': Phaser.AUTO,
'width': 500,
'height': 500,
'scene': [scene]
}
Phaser.Game.new(to_js(config))
感想
Pygame-ce(Pygame)あるんだからそれ使えばいいやんとかJavaScriptライブラリなんだからわざわざ(Micro)Python使わなくてもJavaScriptでいいやんと言われるかもしれませんが、現状はPygame-ce(Pyodide)はロード時間に難があるので、それがどうしても許容できないときの選択肢として考えておいても損はないかなと考えています。