2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita全国学生対抗戦Advent Calendar 2024

Day 2

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

Last updated at Posted at 2024-12-01

※とにかく要点だけ教えてくれ!!というせっかちさんは、まとめをまずご覧いただき、pygbagをpipし、手順1~4の通りに進めてください

はじめに

 先日、Pyxelで作ったゲームをGitHubで公開し、リンクを貼れば、Qiitaのページからもすぐ遊べるようにする。というのをやってみました。しかし、筆者は普段pygameを使ってゲームを作っている人間なので、できればpygameでこれがしたいなと思い、色々調べてみました。ドットでゲームを作るなら、正直Pyxelが良かったので、どちらも使っていくつもりなのですが、Pyxelだとフォント関連がネックなんですよね。PyxelUniversalFontがブラウザ上だとimportできないので。あとは、普通にドットじゃないゲームを作って公開したい時に、使い分けられた方がいいですからね。

念のため言っておきますが、記事の趣旨としては、「10分でできる方法」であって、この記事を読みながらではもちろん無理です。電車の中とかの隙間時間に読んで、家に帰ってからやってみてください。
 参考までに、筆者が工程を頭に入れた状態でRTAしたら、ファイルの作成から公開までで8分55秒71でした。

pygameでやる意義

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

pygbagとは

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

ネットに公開するまでの手順

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

一応筆者の環境も置いておきます

OS 名:                  Microsoft Windows 11 Home
OS バージョン:          10.0.22631 N/A ビルド 22631
Python バージョン:      3.11.3
開発環境:               IDLE (Python3.11.3)

 執筆した後に、Python 3.12.7に更新してからも問題なく使えてるので、バージョンはあまり気にしなくていいと思います。

手順1:ファイルの作成

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

手順2:main.pyの中身を書いていく

 とりあえずmain.pyに、普段pygameでゲームを作るように、各自好きなゲームを作っていただければいいのですが、3点注意があります。

main関数を作成する際に

async def main():
    #中身

というように asyncを使って定義する必要があります。必然的に
import asyncioが必要になりますね

もうひとつ、main関数のゲームループ内で

await asyncio.sleep(0)    #引数は0で固定

という記述が必要です。clock.tickの直後がおすすめらしいですが、公式のサンプルではループの頭のほうに書いてあったりするので、あまり厳密ではないかもしれません。筆者は石橋をたたき壊すタイプなのでclock.tickの後にしておきます。

最後に、main関数を

asyncio.run(main())

で呼び出します。
 以上3点を踏まえてサンプルを書いたのでおいておきます

コードを見る
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())    #これが必須の奴

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

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

手順3: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で公開する分には使わないので気にしないでください。(もしかしてこれ使わないからこの工程はいらないかも?ただし動作確認が楽にできるから、どっちみちやるべき。)

期待通りに動作しない場合、手順の後に記述してあるその他注意事項をご確認ください

手順4:GitHubへ公開する

 そもそもGitHubのアカウントを持っていない人は、アカウント作成を行ってください。Qiitaのアカウント作るのにも使えますし、せっかくなのでここでやめずにアカウントを作るのをお勧めします。筆者も意味もなく怖がっているときがありましたが、別に怖くなかったです。

 アカウントを作ってきた人、もともと持っている人は次に進んでください。英語ばかりで戸惑うかもしれませんが、操作については、記事と画面を見比べながらやればできます。

Step1

 とりあえずpygbag.ymlというのをダウンロードする必要があるみたいです。以下のURLにアクセスして、コピーしてください

 GitHubを開いて、好きな名前でリポジトリを作ります。この時にPublicで作成してください。(間違って作ってしまってもSettingsタブの一番下まで行くと切り替えれるのがあります。)create new fileをして、your file name ...って出てる窓に.github/workflows/pygbag.ymlと打って、コードのところに、先ほどコピーしたものをペーストしておきます。
AIに聞いたら

   - uses: actions/checkout@v2

という行が古いらしいので

   - uses: actions/checkout@v3

と書き換える必要があるかもしれません。実際に書き換えたらこの後の手順で警告が出なくなったので意味はありそうです。変えなくても警告が出るだけでいけそうですが。
 あとはmain.pyや、その他ゲームごとに必要なファイルを、リポジトリのルートディレクトリに置くのを忘れていないか確認したら、ファイルのアップロード関連は終わりです。

.github/workflows/の中に、pygbag.ymlを入れたあと、そのままの流れで、ここにmain.pyを入れてしまう事故が100%の人に発生しています(1人中1人)。これでは当然上手くいかないので、指さし確認してください。...たぶん行けてるからヨシッ

Step2

 Actionタブに移動して、pygbag_buildっていうのをrunします
スクリーンショット 2024-11-19 002014.png
一回ミスってるスクショが写ってますが、main.pyの置忘れです。code89というエラーが出たらこれだと思うので、皆さんお気をつけください。pygbag.ymlの中のpython -m ~って記述は自分が使っているものと違っても関係ないので、この辺をいじったりしないようにしましょう。それをしたらcode127というエラーが出てしまいました。

 話を戻します。この時、設定次第でうまくいかないようなので、上手くいかなかったら次のように操作を行ってから再挑戦します。
Settingsタブの左のActions→Generalを押す
スクリーンショット 2024-11-19 004608.png

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

スクリーンショット 2024-11-19 004619.png

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

Step3

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

image.png

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

その他注意事項(必要な時にご覧ください)

 先に注意事項の注意事項です(?)。いろいろな情報を並べているので、ちょっと煩雑です。必要な情報だけ取捨選択してください。

モジュールに関する注意

 デフォルトの状態で、標準ライブラリのモジュールは使えるっぽいんですが、やはり他のWeb対応用のものと同じで、外部のモジュールは使えなさそうです。ただし、pyodideに入っているものは使えるみたいです。よく使うrandommathtimenumpy辺りは実際にimportできるのを確認済みです。なのでこちらはあまり気にならないと思います。ちなみに、できない奴はimportしただけで手順3が動かなくなるのでわかります。openpyxlとか。

おまけ

 どうしても外部モジュールをimportしたい方へ。正直なところ、ここについて紹介するには、知識も検索力も足らなかったので、辛うじて見つけた情報を置いておきます。
 こちらを自分のアカウントにフォークしてCPythonをビルドすれば、自分で使いたいモジュールをいろいろ設定できる的な何某がREADMEのBUILDINGって項目に載っていました。
 あとこちらの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ファイルをあげるのを忘れないようにしてください。

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

状態遷移に関する注意

 ゲームループ中でほかのゲームループを呼び出したいこともあると思います。その際は以下のようにすれば、動作することが確認できました。

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モジュールというのを使う必要があります。

from js import window

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

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

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

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

おまけ1

 辞書とかリストみたいなオブジェクトを保存したい場合には以下のようにjsonと組み合わせてください。(一回文字列にする必要があるみたい)この時にクラスのインスタンスや関数が含まれていると上手く変換できないようです。

コードを見る
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) # 読み込んだオブジェクト

おまけ2

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

コードを見る
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')

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

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

まとめ

 注意事項や独り言が多くて要点が分かりづらいという人のために、手順の要点だけ抜き出したものを置いておきます。

  • 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_buildrun
    • SettingsタブのPagesからBranchgh-pagesを選択しSave

最後に

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

↑サンプルコードのフォントの部分をttf読み込んで日本語フォントに変えたやつ。これもちゃんと動きます。

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

参考

  • README見たい人はこちら

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

  • 手順1~3

  • 手順4

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

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

宣伝

↓Pyxel版こちら

これからいろいろゲームを公開するつもりなので、ゲームを作ったら記事を書いてここに貼っていきます!サンプルプログラム的なノリで活用してください!(実はもう出来てるのもありますが、無理なく毎週投稿するために寝かせてます)

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

スマホでも動くので、その動作確認済みの証拠(シューティングゲーム的なやつ)
coming soon!(12月中予定)

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

2
1
1

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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?