【pygbag】「たった10分で」pygameをブラウザ上で動かす！

※とにかく要点だけ教えてくれ！！というせっかちさんは、[まとめ](#まとめ)をまずご覧いただき、pygbagをpipし、手順１～４の通りに進めてください

## はじめに
　先日、Pyxelで作ったゲームをGitHubで公開し、リンクをから誰でも遊べるようにする。というのをやってみました。しかし、筆者はpygameを使ってゲームを作ることが多いので、pygameで同様のことをしたいと思い、色々調べてみました。ドットでゲームを作るなら、やはりPyxelが圧倒的に便利なので、どちらも使っていく予定なのですが、Pyxelだとフォント関連がネックなんですよね。`PyxelUniversalFont`がブラウザ上だと`import`できないので。あとは、普通にドットじゃないゲームを作って公開したい時に、使い分けられた方がいいですからね。
 
:::note warn
念のため前置きしますが、記事の趣旨としては、「10分でできる方法」であって、この記事を読みながらでは、もちろん無理です。電車の中など、隙間時間に読んで、家に帰ってから試してみてください。
　参考までに、筆者が工程を頭に入れた状態でRTAしたら、ファイルの作成から公開までで8分55秒71でした。
:::

## pygameでやる意義
　興味ない人は飛ばしてください。正直Pyxelを使えば、もう少し簡単にブラウザ上でゲームを動かすことは可能です（慣れれば大差ないですが）。じゃあpygameをわざわざ使う意義は何か、すなわちPyxelとの差別化ポイントはどこか、について３点話します。（筆者がPyxelに詳しくないだけの点もあるかもしれません）
　1つ目は**フォント**です。はじめにの項目で話しましたが、Pyxelはデフォルトのフォント関連がpygameと比べると弱く、`PyxelUniversalFont`を使いたいのですが、外部モジュールなので使えないです。よって、フォント関連の自由度が低めです。しかし、pygameで今回紹介する方法なら、ローカルでやるのと同じようにでき、フォント関連の自由度が高いです。
　2つ目は**画像や音声の自由度**です。これはPyxelがあえて制限を付けているコンセプトなので当然ですね。これについては一長一短です。しかし、我々が作りたいゲームはレトロゲームだけではないので、レトロゲームを作らないのであれば、素直にpygameを使った方がいいです。
　3つ目は**情報の得やすさ**です。Qiitaの記事の数を見てもPyxel(58)、pygame(272)とかなり差があります(執筆時)。筆者は普段、AIに簡単なコードを書かせて、いじって組み合わせて...って感じで作っています。正直Pyxelをやらせると、AIがとんちんかんすぎて成り立たないくらいには、Pyxelの情報は少ないです。pygameなら、多少意図と違えど、それなりに惜しいものを勝手に作ってくれます。少なくとも、Pyxelをやらせるときのように、存在しないメソッドや引数を持ち出してきたりはしません。（追記：この点は使っているAIアシスタントによるところかもしれません。）

## pygbagとは
　ここから本題に入ります。`pygbag`とはpygame用のwasm(WebAssembly)です。要は、pygameをWeb上で動かすためのツールです。海外の方が開発したようで、検索しても英語の情報しか出てこないので、聞いたことない人は多いんじゃないでしょうか。何なら記事投稿時点で、`pygbag`のタグの記事が存在していなかったです。もちろん筆者も初心者なので、そんなものは知る由もないです。
 　`pygbag`の使い方ですが、モジュールなので普通にコマンドプロンプトから`pip install pygbag`や`python -m pip install pygbag`でpipできます。筆者は普段から`py -m pip install ～`でやっている人なので、それでやりました。

## ネットに公開するまでの手順
　初回は、まず上の方法でpipしてください。この記事を読んでいる人はpygameをpipできて、普段から使っている人だと思うので、pipの仕方については省略します。したがって、pipした後の手順について、ここでは解説します。先に全体の流れを知っておきたい人は[まとめ](#まとめ)を先にご覧ください。

:::note warn
一応筆者の環境も置いておきます
```
OS 名:                  Microsoft Windows 11 Home
OS バージョン:          10.0.22631 N/A ビルド 22631
Python バージョン:      3.11.3
開発環境:               IDLE (Python3.11.3)
```
　執筆した後に、Python 3.12.7に更新してからも問題なく使えてるので、バージョンはあまり気にしなくていいと思います。
:::


### 手順１：ファイルの作成
　まず普通にフォルダを作ります。適当に`web-pygbag`としました。この部分は適宜読み替えてください。この中にいつも通り`.py`ファイルを作るのですが、**この際に名前を`main.py`にする必要があります**。注意してください。その他のファイルについては特に制約がないようです。

### 手順２：main.pyの中身を書いていく
　とりあえず`main.py`に、普段pygameでゲームを作るように、各自好きなゲームを作っていただければいいのですが、**３点**注意があります。
 
 `main`関数を作成する際に
 ```python
 async def main():
     #中身
 ```
 というように **`async`を使って定義する**必要があります。必然的に
 `import asyncio`が必要になりますね

 もうひとつ、`main`関数のゲームループ内で
 ```python
 await asyncio.sleep(0)    #引数は0で固定
 ```
 という記述が必要です。`clock.tick`の直後がおすすめらしいですが、公式のサンプルではループの頭のほうに書いてあったりするので、あまり厳密ではないかもしれません。筆者は石橋をたたき壊すタイプなので`clock.tick`の後にしておきます。

 最後に、`main`関数を
 ```python
 asyncio.run(main())
 ```
 で呼び出します。
 　以上３点を踏まえてサンプルを書いたのでおいておきます
<details><summary>コードを見る</summary>
  
  ```python:sample.py
import asyncio    #これが必須の奴
import pygame, sys
pygame.init()
screen = pygame.display.set_mode((320, 240))
clock = pygame.time.Clock()
font = pygame.font.SysFont('', 32)
text = font.render('web-pygame', True, (0, 0, 255))

async def main():    #これが必須の奴
    x, y = 50, 50
    while True:
        screen.fill((0, 255, 0))
        screen.blit(text, (40, 100))
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
               pygame.quit()
               sys.exit()
            if event.type == pygame.KEYDOWN:
                x += 5
            if event.type == pygame.MOUSEBUTTONDOWN:
                y += 5
        pygame.draw.rect(screen, ((255, 0, 0)), (x, y, 20, 20))
        pygame.display.update()
        clock.tick(30)
        await asyncio.sleep(0)    #これが必須の奴

asyncio.run(main())    #これが必須の奴

  ```

</details>

  これはシンプルな例なのですが、複雑なプログラムで、ゲームループ中で別のゲームループを呼び出して階層化している場合には、ループを含む関数をすべて`async`で定義して、全てのループに`await asyncio.sleep(0)`を挿入すればいいものと、筆者は解釈しています。（この点について[その他注意事項](#その他注意事項必要な時にご覧ください)で後述します）

　ここまで出来たら、`web-pygame`というフォルダに`main.py`が入っている状態です。この状態でコマンドプロンプトでの作業に移ります。

### 手順３：pygbag フォルダ名をする
　コマンドプロンプトで、**`web-pygame`が入っているディレクトリに移動**します。要は`web-pygame`の一つ上の階層ですね。**そこで`pygbag web-pygame`または`python -m pygbag web-pygame`をします**。例によって筆者は`py -m`を使いましたが。
　これをすると、localhost:8000で自分のPCをサーバーとして、内部ネットワークで動作確認ができるように、いろいろ設定してくれるようです。このコマンドを実行した後、お好きな**ブラウザを開いて、http://localhost:8000/ というURLにアクセス**してみてください。localhostは127.0.0.1のプライベートIPアドレスのことですね。とにかく、これで動けばもうあと一歩！手順に沿ってGitHubへ公開するだけです。ちなみにですが、確認を終えたら`ctrl + C`で強制終了しとけばいいです。あとこの操作をすると`build`っていうフォルダができるんですが、GitHubで公開する分には使わないので気にしないでください。（もしかしてこれ使わないからこの工程はいらないかも？ただし動作確認が楽にできるから、どっちみちやるべき。）

:::note warn
期待通りに動作しない場合、手順の後に記述してある[その他注意事項](#その他注意事項必要な時にご覧ください)をご確認ください
:::

### 手順４：GitHubへ公開する
　そもそもGitHubのアカウントを持っていない人は、アカウント作成を行ってください。Qiitaのアカウントを作るのにも使えますし、せっかくなのでここでやめずにアカウントを作るのをお勧めします。筆者も意味もなく怖がっているときがありましたが、別に怖くなかったです。
 
 https://github.com/
 
　アカウントを作ってきた人、もともと持っている人は次に進んでください。英語ばかりで戸惑うかもしれませんが、操作については、記事と画面を見比べながらやればできます。
#### Step1
　とりあえず`pygbag.yml`というのをダウンロードする必要があるみたいです。**以下のURLにアクセスして、コピー**してください
 
https://pygame-web.github.io/wiki/publishing/github.io/pygbag.yml

　GitHubを開いて、好きな名前でリポジトリを作ります。この時に**Publicで作成してください**。（間違って作ってしまってもSettingsタブの一番下まで行くと切り替えれるのがあります。）`create new file`をして、`your file name ...`って出てる窓に`.github/workflows/pygbag.yml`と打って、コードのところに、先ほどコピーしたものをペーストしておきます。
 AIに聞いたら
 ```
    - uses: actions/checkout@v2
 ```
 という行が古いらしいので
 ```
    - uses: actions/checkout@v3
 ```
 と書き換える必要があるかもしれません。実際に書き換えたらこの後の手順で警告が出なくなったので意味はありそうです。変えなくても警告が出るだけでいけそうですが。
 　あとは`main.py`や、その他ゲームごとに必要なファイルを、リポジトリのルートディレクトリに置くのを忘れていないか確認したら、ファイルのアップロード関連は終わりです。
  
:::note warn
`.github/workflows/`の中に、`pygbag.yml`を入れたあと、**そのままの流れで、ここに`main.py`を入れてしまう事故が100%の人に発生しています**（1人中1人）。これでは当然上手くいかないので、指さし確認してください。...たぶん行けてるからﾖｼｯ
:::

#### Step2
　**Actionタブに移動して、`pygbag_build`っていうのを`run`します**。
  ![スクリーンショット 2024-11-19 002014.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922095/ae2db924-d842-b17d-9217-a54fbde80fd1.png)
一回ミスってるスクショが写ってますが、`main.py`の置忘れです。`code89`というエラーが出たらこれだと思うので、皆さんお気をつけください。`pygbag.yml`の中の`python -m ～`って記述は自分が使っているものと違っても関係ないので、この辺をいじったりしないようにしましょう。それをしたら`code127`というエラーが出てしまいました。

　話を戻します。この時、設定次第でうまくいかないようなので、上手くいかなかったら次のように操作を行ってから再挑戦します。
  Settingsタブの左のActions→Generalを押す
![スクリーンショット 2024-11-19 004608.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922095/1a5c9d96-d4d2-b9f1-ce79-a4679641daf1.png)

ページの下の方に行くと...

  ![スクリーンショット 2024-11-19 004619.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922095/f71e695c-114a-5b89-3925-2c08486c0567.png)

  

こういうのがあるので**Read and write permissionsにチェックがついていることと、しっかりセーブを押したことを確認**してください。（筆者はリポジトリを作るたびに毎回下の方についてるので、もう先にこっちをやってから`run`してます）

#### Step3
　**Settingsタブに移動し、`Pages`から`Branch`に`gh-pages`を設定**します。Step2が成功していれば、`gh-pages`が自動で生成されているはずです。あとはセーブしてしばらく待てば、ページを再読み込みすると、上の方にリンクが出てくるので、そこからアクセスしてみて動作確認します。無事動作すれば完了です。お疲れさまでした！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922095/5ccdca67-4279-73b8-be4c-ac2be8b74169.png)

:::note warn
ファイルを別にアップロードするなど、ファイルを更新したら、再度pygbag_buildをrunしてください。
:::

## その他注意事項（必要な時にご覧ください）
　先に注意事項の注意事項です（？）。いろいろな情報を並べているので、ちょっと煩雑です。必要な情報だけ取捨選択してください。
### モジュールに関する注意
　デフォルトの状態で、**標準ライブラリのモジュールは使える**っぽいんですが、やはり他のWeb対応用のものと同じで、外部のモジュールは使えなさそうです。ただし、**`pyodide`に入っているものは使える**みたいです。よく使う`random`、`math`、`time`、`numpy`辺りは実際に`import`できるのを確認済みです。なのでこちらはあまり気にならないと思います。ちなみに、できない奴は`import`しただけで手順3が動かなくなるのでわかります。`openpyxl`とか。
#### おまけ
　どうしても外部モジュールを`import`したい方へ。正直なところ、ここについて紹介するには、知識も検索力も足らなかったので、辛うじて見つけた情報を置いておきます。
 　[こちら](https://github.com/pygame-web/python-wasm-sdk)を自分のアカウントにフォークしてCPythonをビルドすれば、自分で使いたいモジュールをいろいろ設定できる的な何某がREADMEのBUILDINGって項目に載っていました。
　あと[こちら](https://pygame-web.github.io/wiki/pygbag/)のAdding Modulesもモジュールの話が載っているのですが、「numpyのような複雑なモジュール(？)を使うときは`main.py`の先頭で`import`するとともに、PEP723に沿ってメタデータヘッダーを書け」と書いてあるので、こちらも関係あると思います。

### ファイルに関する注意（特に音声ファイル）
　以下がgitignoreを使うおすすめの例で挙げられているので、これらは使えない（もしくは使うべきでない）と解釈しています。これらがプロジェクトに含まれている場合、注意してください。
  - *.wav
  - *.mp3
  - *.pyc
  - *.egg-info
  - *-pygbag.???
  - /build
  - /dist

　特に問題になりそうなのは`.wav`と`.mp3`でしょうか。`.ogg`なら動作することを確認できましたので、音声ファイルを使いたい場合は、**これらの代わりに`.ogg`を使う事をお勧めします**。単に拡張子を書き換えるよりも、変換サイトを使った方が確実と思われます。 

### 日本語フォントに関する注意
　日本語フォントを使いたい場合は、`pygame.font.SysFont('MSP Gothic', 32)`のように使うことが、通常できますよね。しかし、ブラウザ上では、これらは`□□□□`と表示されてしまいました。**日本語のフォントを`.ttf`形式でダウンロードしてくれば、解決する**ので安心してください。その際、ちゃんとGitHub上にttfファイルをあげるのを忘れないようにしてください。

https://fonts.google.com/

↑特に気に入っているフォントがなければ、こちらからお好きなものをお選びください。

### 状態遷移に関する注意
　ゲームループ中でほかのゲームループを呼び出したいこともあると思います。その際は以下のようにすれば、動作することが確認できました。
```python
async def other_loop():
    #何か処理
    await asyncio.sleep(0)
async def main():
    #何か処理
    await other_loop()
    #何か処理
    await asyncio.sleep(0)
asyncio.run(main())
```
**ループ用の関数をすべて`async`で定義して、内部で`asyncio.sleep` を呼び出してください**。`asyncio.run`は最初だけでいいです（というか内部で重ねて呼び出せません）。
`async`で定義されている関数を呼び出すために`await`を付ける必要がある→`await`を内部で使うために`async`で定義する必要がある→.....この連鎖で、プログラムによっては、ちょっと変更が面倒な場合がありますが、頑張ってください。この記事を読んで最初からゲームを作ってみる場合には、この点に気を付けながらプログラムを作っておくといいでしょう。

### セーブとロードに関する注意
　セーブやロードを実装したい場合には`pyodide`に入っているjsモジュールというのを使う必要があります。
```python
from js import window

# セーブ
save_data = "hogehoge"    #保存したい内容
window.localStorage.setItem("hoge", save_data)    #保存先、内容

# ロード
load_data = window.localStorage.getItem("hoge")    #読み込み元
print(load_data)    #読み込んだ内容
```

https://note.com/frenchbread/n/n770ceda149e3

↑こちらPyxel向けの解説記事ですが、参考にさせていただきました。使用方法から注意事項まで簡潔にまとまっているので、細かいことはこちらにお任せしたいと思います。

:::note warn
jsモジュールはローカルだと使えないので、このモジュールを使ったうえで、動作確認したい場合は、毎回手順3をすることになります。普段コマンドプロンプトから実行してる人には誤差程度の手間ですが、IDLEとかでF5使って実行している人は、ちょっとめんどくさいかも.....
:::

#### おまけ１
 
　辞書とかリストみたいなオブジェクトを保存したい場合には以下のように`json`と組み合わせてください。（一回文字列にする必要があるみたい）この時にクラスのインスタンスや関数が含まれていると上手く変換できないようです。
 <details><summary>コードを見る</summary>

```python
from js import window
import json

# 保存するオブジェクト
save_data = {"name": "Alice",
             "age": 30,
             "is_active": True
}

# オブジェクトをJSON文字列に変換して保存
save_data_str = json.dumps(save_data)
window.localStorage.setItem("user_data", save_data_str)

# 保存したデータを読み込む
load_data_str = window.localStorage.getItem("user_data"
load_data = json.loads(load_data_str)

print(load_data) # 読み込んだオブジェクト
```

</details>

#### おまけ２

　セーブの際にサムネイルを付けたい場合、`pygame.image.save`で画面のスクショを取って、保存するというのが一つの手ですが、この戻り値がインスタンスなので、普通では`json`を使えません。ですが、さらに`base64`まで組み合わせれば無理やり保存できました。
<details><summary>コードを見る</summary>

```python
from js import window
import json, base64
#セーブ側
pixel_data = pygame.image.tostring(screen, 'RGB')
width, height = #任意の値
base64_str = base64.b64encode(pixel_data).decode('utf-8')
save_data = [width, height, base64_str]
save_data_str = json.dumps(save_data)
window.localStorage.setItem("user_data", save_data_str)

#ロード側
load_data_str = window.localStorage.getItem("user_data")
load_data = json.loads(load_data_str)
width, height, base64_str = load_data
pixel_data = base64.b64decode(base64_str)
image_surface = pygame.image.fromstring(pixel_data, (width, height), 'RGB')
```

</details>

画像のインスタンスをバイナリに変換、バイナリを文字列に変換し、`json`で変換。それをjsモジュールで`user_data`に保存。逆に読み込むときは、jsモジュールで`user_data`から読み込み、`json`で変換し、文字列をバイナリに変換。それを基に画像のインスタンスを復元となっています。

:::note warn
この点について、あくまでこれで実装できたというだけで、ここまでややこしい変換が必須かどうかは分かりません
:::



## まとめ
　注意事項や独り言が多くて要点が分かりづらいという人のために、手順の要点だけ抜き出したものを置いておきます。
- pygbagをpipする（`python -m pip install pygbag`）
- フォルダを作り、中に`main.py`を作る。（他の必要なファイルも入れる）
- 以下の3点に留意しつつ、普段通りにゲームを作成する
  - `main`関数を`async`で定義
  - ループの中で`await asyncio.sleep(0)`
  - `main`関数の呼び出しを`asyncio.run(main())`でする
- コマンドプロンプトで`pygbag フォルダ名`または、`python -m pygbag フォルダ名`をする
- GitHubで公開する
  - リポジトリを作り必要なファイルを用意
  - Actionタブから`pygbag_build`を`run`
  - Settingsタブの`Pages`から`Branch`に`gh-pages`を選択し`Save`

## 最後に
　所々どうでもいいことを喋るので、解説記事としては読みにくい部分もあったと思いますが、ここまで読んで下さりありがとうございました。全部の手順を調べて、実際に動かしつつ、記事を書きつつって感じで進めたので、筆者は結構時間かけてやりました。`main.py`の置忘れで30分くらい無駄にしたり、文献が全て英語だったりで大変だったので、日本語の情報がすぐ手に入る皆さんは、とてもラッキーだと思います。この記事さえあれば、筆者が30分無駄にしてた間に、全部の手順ができてしまいますからね。実際にもっと複雑なゲームを作ってみたら分かりませんが、少なくともこの記事内のシンプルなコードについては、ブラウザ上で動くことをここに保証します。~~こんなのが動いてもって言わないで~~　一応このコードが動くってことは、キー入力もマウス操作もちゃんと生きてるってのは確認できましたので。
 
  https://prosamo.github.io/pygbag-test/
  
↑サンプルコードのフォントの部分をttf読み込んで日本語フォントに変えたやつ。これもちゃんと動きます。

:::note warn
なんでそんな仕様なのか分からないですが、スマホからだとReady to start!の、明らかにタップしてね感ある場所じゃなくて、画面外をタップしないと始まってくれません。
:::


## 参考

- README見たい人はこちら

https://github.com/pygame-web/pygbag

- 公式のサンプルコードとか

https://pygame-web.github.io/wiki/pygbag/

- 手順1～3

https://youtu.be/6PhDmpBcezQ

- 手順4

https://pygame-web.github.io/wiki/publishing/github.io/

- セーブとロードについて

https://note.com/frenchbread/n/n770ceda149e3

- スペシャルサンクス
Microsoft Copilot

## 宣伝
↓Pyxel版こちら

https://qiita.com/Prosamo/items/6e2f7147e38aca3db5ca

これからいろいろゲームを公開するつもりなので、ゲームを作ったら記事を書いてここに貼っていきます！サンプルプログラム的なノリで活用してください！

フォント関連と音声の動作確認済みの証拠（タイピングゲーム）

https://qiita.com/Prosamo/items/e77e592ac82b2e51a8a7

スマホでも動くので、その動作確認済みの証拠（シューティングゲーム的なやつ）

https://qiita.com/Prosamo/items/03d3538f322ddeb39927

状態遷移とセーブの動作確認済みの証拠（ノベルゲームの予定）
coming soon？（作成中につき未定）