19
11

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 24

「Pyxel × Pymunkで物理シミュレーションを始めよう!」を Webで動かしたい🌎

Last updated at Posted at 2024-12-23

はじめに

malo21st さんの「Pyxel × Pymunkで物理シミュレーションを始めよう! (Pyxel Advent Calendar 2024 の 15日目の投稿)」を「Webで動かしたい」という動機の投稿です🌎

この投稿は、malo21st 様の投稿の上に成り立っております。
素晴らしい投稿ありがとうございます👾👾👾

Web実行版

若干の紆余曲折はありましたが、Web上で動かすことが出来ました。
以下はお試し用のURLと実行例の画像です。

Web実行版URL:https://kazuhito00.github.io/pyxel-pymunk-web-test/

mqgaj-qat4l.gif

今回試したソースコードは以下リポジトリで公開しています。

以降は紆余曲折部分の解説です🦔

Pyxel Web版で使用可能なパッケージ

Pyxelユーザーの方には、おなじみだと思いますが、PyxelはWeb実行をサポートしています。
ただし、Web実行はPyodideの仕組みを利用しており、使用できるパッケージに制限があります。
具体的には、Pyodide の Packages built in Pyodide のページに記載のパッケージが使用できます(+純粋にPythonのみで書かれたパッケージ)

別件で、このページを眺めた時に Pymunk は無かった気がしましたが、、、
改めて確認しても、やはり Pymunk を探してもリストにはありませんでした。
(b2dという少々マイナー?な物理エンジンはサポートされていましたが)

ダメ元でPymunk公式を確認

Pyodide公式にサポートされていないのであれば、望み薄であると思いながら、
ダメ元でPymunk公式に何か情報は無いか探しにいったところ、、、
image.png
Wasm wheelがリリースされていました👀
※PyodideはCPythonをWebAssembly/Emscriptenに移植したもので、Wasm wheelがあればパッケージを動かせる可能性あり

リリースを見に行くと
image.png
明らかに、Pyodide向けっぽい命名がされていました。

Pymunk Wasm wheelを動作確認

Pyodide向けっぽい命名だったので、期待しつつ、
とりあえず Pymunk単体で動作確認をしてみようと、以下のスクリプトを用意して確認しました。

main.py
try:
    import micropip

    print("micropip successfully imported")
except ImportError as e:
    print(f"Error importing micropip: {e}")

try:
    await micropip.install("pymunk-6.9.0-cp312-cp312-pyodide_2024_0_wasm32.whl")
    print("Pymunk installed successfully")
except Exception as e:
    print(f"Error installing Pymunk: {e}")

try:
    import pymunk

    print("Pymunk successfully imported")
except ImportError as e:
    print(f"Error importing Pymunk: {e}")
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <title>Test</title>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="https://pyscript.net/releases/2024.11.1/core.css">
    <script type="module" src="https://pyscript.net/releases/2024.11.1/core.js"></script>
</head>
<body>
    <py-config src="./pyscript.toml"></py-config>
    <py-script src="./main.py"></py-script>
</body>
</html>
pyscript.toml
packages = ["micropip", "cffi"]
name = "Test"

実行確認は簡易HTTPサーバー( http://localhost:8000 )で動作確認↓

python -m http.server 8000

しかし、実行したところ「Error installing Pymunk: Requested 'cffi>=1.17.1', but cffi==1.16.0 is already installed」と言うエラーが出てしまいました。
Pyodideのサポートしているパッケージバージョンと、Pymunkの要求するパッケージバージョンが食い違ってしまっていますね👀
image.png

Pymunk公式へIssueで問い合わせてみる

「サポート対象外です」とか言われたら、サクッと諦めようかと思いつつ、、、
とりあえずエラー内容をIssueで問い合わせてみました。

問い合わせてみたら、サクッと対応版がリリースされてしまいました。
上記のお試しスクリプトで6.10.0版をインストール・インポートすると、問題無く動作しました。
viblo様、ありがとうございます👾👾👾

image.png

Web実行に向けて「3_2_shot_bullet.py」をちょっと改造

Web実行に向けてオリジナルの「malo21st/Pyxel_Pymunk/3_2_shot_bullet.py」をちょっと改造しています。

Web実行では、Pymunkをインストールする処理が必要になります。
今回は、ファイル冒頭でインポートしないようにして、Appクラスの生成直前でインポートして、インポートしたインスタンスをAppクラスに渡すように変更しています(もっと良い方法あるかも)

このようにした理由は以下2点です
・Pymunkのインストールが完了する前に、インポートしてしまうとエラーになるため
・「if __name__ == "__main__":」を利用して、ローカルPC上での実行と、インストールと言う前処理が必要なWeb上での実行を切り分けるため

shot_bullet.py
import pyxel

class App:
    def __init__(self, pymunk, fps=60):
        self.pymunk = pymunk
        self.fps = fps

        pyxel.init(256, 256, fps=fps, title="Vertical Stack with Pyxel")
        pyxel.load("shot_bullet.pyxres")
        self.create_world()
        pyxel.run(self.update, self.draw)

    def create_world(self):
        self.space = self.pymunk.Space()
        self.space.gravity = 0, 900
        self.space.sleep_time_threshold = 0.3

        # Static lines
        static_lines = [
            self.pymunk.Segment(self.space.static_body, (10, 200), (240, 200), 1),
            self.pymunk.Segment(self.space.static_body, (230, 200), (230, 50), 1),
        ]
        for line in static_lines:
            line.friction = 0.3
        self.space.add(*static_lines)

        # Stacked boxes
        self.shapes = []
        for x in range(5):
            for y in range(10):
                size = 10
                mass = 10.0
                moment = self.pymunk.moment_for_box(mass, (size, size))
                block_body = self.pymunk.Body(mass, moment)
                block_body.position = 100 + x * 20, 100 + y * (size + 0.1)
                block_shape = self.pymunk.Poly.create_box(block_body, (size, size))
                block_shape.friction = 0.3
                self.space.add(block_body, block_shape)
                self.shapes.append(block_shape)
        # Bullets
        self.bullets = []

    def update(self):
        step = 5  # Run multiple steps for more stable simulation
        step_dt = 1 / self.fps / step
        for _ in range(step):
            self.space.step(step_dt)

        if pyxel.btnp(pyxel.KEY_SPACE):
            mass = 100
            r = 5
            moment = self.pymunk.moment_for_circle(mass, 0, r, (0, 0))
            bullet_body = self.pymunk.Body(mass, moment)
            bullet_body.position = (10, 165)
            bullet_shape = self.pymunk.Circle(bullet_body, r, (0, 0))
            bullet_shape.friction = 0.3
            self.space.add(bullet_body, bullet_shape)
            self.bullets.append(bullet_shape)

            impulse = 150_000
            bullet_body.apply_impulse_at_local_point((impulse, 0), (0, 0))

        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

        if pyxel.btnp(pyxel.KEY_A):
            self.create_world()

    def draw(self):
        pyxel.cls(0)

        # Draw bullets
        for bullet_shape in self.bullets:
            x, y, *_ = bullet_shape.bb
            angle = -bullet_shape.body.angle * 180 / 3.14  # radian => degree
            pyxel.blt(x, y, 1, 0, 0, 10, 10, rotate=angle)

        # Draw boxes
        for block_shape in self.shapes:
            x, y, *_ = block_shape.bb
            angle = -block_shape.body.angle * 180 / 3.14  # radian => degree
            pyxel.blt(x, y, 0, 0, 0, 10, 10, rotate=angle)

        # Draw static lines
        pyxel.line(10, 200, 240, 200, 7)
        pyxel.line(230, 200, 230, 50, 7)

        # Draw Key explanation
        pyxel.text(4, 4, "SPACE: Shot Bullet", 7)
        pyxel.text(4, 12, "A    : Reset", 7)


if __name__ == "__main__":
    import pymunk

    FPS = 60
    App(pymunk, FPS)

Web版ではなく、ローカルPC上で単体動作させる時は以下です↓

python shot_bullet.py

Web用の「web_test.py」と「index.html」を用意

こちらも、もっと良い書き方がありそうですが、Web実行用に実行スクリプトを用意しています(await使う都合上、ちょっとゴチャっとしています)
micropipを利用してPymunkをインストールした後、Appクラスを初期化して実行しています。
micropipでインストールするwhlファイルはダウンロードして、同ディレクトリ内に格納してください。

web_test.py
import asyncio
import micropip

from shot_bullet import App


# メイン関数を非同期で実行するための関数
def run_asyncio():
    # 現在のイベントループを取得
    loop = asyncio.get_event_loop()

    # イベントループが実行中か確認
    if loop.is_running():
        # 実行中の場合は、非同期タスクとしてmain()を登録
        asyncio.ensure_future(main())
    else:
        # 実行中でない場合は、新しいイベントループでmain()を実行
        asyncio.run(main())


# 非同期のメイン関数
async def main():
    try:
        # micropipを使ってwhlファイルからpymunkをインストール
        print("Installing Pymunk...")
        await micropip.install("pymunk-6.10.0-cp312-cp312-pyodide_2024_0_wasm32.whl")
        print("Pymunk installed successfully")

        # pymunkをインポート
        import pymunk

        # Appクラスを初期化して実行
        App(pymunk)
    except Exception as e:
        print(f"Error: {e}")


# メイン関数を実行
run_asyncio()

index.html は、公式サンプルとほぼ同じですが、micropipを利用する都合上、
「packages="micropip"」を、pyxel-playタグに追加しています。

index.html
<!DOCTYPE html>
<html>
    <head>
        <script src="https://cdn.jsdelivr.net/gh/kitao/pyxel/wasm/pyxel.js"></script>
    </head>

    <body>
        <pyxel-play root="." name="pyxel-pymunk-web-test.pyxapp" packages="micropip"></pyxel-play>
    </body>
</html>

Pyxelパッケージ化を行い、簡易HTTPサーバー( http://localhost:8000 )で動作確認↓
※パッケージ化する対象スクリプトは「shot_bullet.py」ではなく「web_test.py」
※Web用ではないパッケージ化の場合は「shot_bullet.py」を対象とする

pyxel package ./ web_test.py
python -m http.server 8000

以上。

19
11
5

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
19
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?