2025/10/26追記
2025.8.1でpy-gameが動かない問題はPyodide0.29で再びpygame-ceが有効になったためそれを採用した2025.10.1で解消されました。
2025/9/22追記
追記時点の最新版であるPyScript 2025.8.1ではpy-gameは動きません。
おそらく2025.8.1から採用したPyodide0.28でpygame-ceパッケージが無効化されているのが原因のようです。
https://blog.pyodide.org/posts/0.28-release/#python-313-support-and-disabled-packages
概要
以前の記事でPyGame-ceをPyScript上で動かそうとしてもCPython上で動いていたネイティブのPyGame用のコードそのままでは動かないという話をしましたが、そういった差異を吸収して共通のコードでPyGame-ceプログラミングをするためのクラスを作りました。
PyScriptとCPythonを判別する
CPythonとPyScriptを判別するためにはsys.platformを使います。これをPyScript環境で実行するとemscriptenとなるのでこれで判別します。(余談ですがこれはPyodideの場合で、MicroPythonではwebassemblyとなる)
ゲームループ
非同期でも同期でもどちらでもいいのですが、今のところ特に非同期を避ける理由がないので非同期でループを回してasyncio.sleepでブラウザに制御を戻す方法にしました。
class PyGameBase:
def __init__(self, title="PyGameBase", width=800, height=600, fps=60):
self.width = width
self.height = height
self.title = title
self.fps = fps
async def pyodide_init(self, js):
pass
def init(self):
pass
def update(self):
pass
def event(self, event):
pass
async def __start(self):
pygame.init()
self.screen = pygame.display.set_mode((self.width, self.height))
pygame.display.set_caption(self.title)
if sys.platform == "emscripten":
import js
loading = js.document.getElementById("loading")
if loading is not None:
loading.style.display = "none"
await self.pyodide_init(js)
self.init()
self.frame = 0
self.clock = pygame.time.Clock()
self.running = True
while self.running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
self.event(event)
self.update()
pygame.display.flip()
self.clock.tick(self.fps)
await asyncio.sleep(0)
self.frame += 1
pygame.quit()
sys.exit()
def run(self):
if sys.platform == 'emscripten':
asyncio.create_task(self.__start())
else:
asyncio.run(self.__start())
テキスト描画
PyGameでデフォルトで用意されたフォントでは日本語を表示することができません。CPythonではpygame.font.SysFontでOSが用意したフォント名を指定すれば日本語表示が可能ですが、この記事のコードを拝借して実行してみた感じPyScriptではデフォルトのフォントしか使えないようです。ノベルゲームなどテキスト表示が重要なゲームならフォントを別途用意して読み込ませてもいいのでしょうが、簡単なゲームでそこまで手間をかけるのもどうかと思ったので何とかしようと思ったのですが、結局PyGameと別のCanvasを上に重ねてCanvasのほうに文字を描画するのが最も手っ取り早い方法でした。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyScript</title>
<script type="module" src="https://pyscript.net/releases/2025.5.1/core.js"></script>
<link rel="stylesheet" href="https://pyscript.net/releases/2025.5.1/core.css" />
<style>
#pygame {
position: relative;
margin: 10px;
z-index: 0;
}
#canvas {
position: absolute;
z-index: 1;
}
#text_canvas {
position: absolute;
pointer-events: none;
z-index: 2;
}
</style>
</head>
<body>
<div id="pygame">
<canvas id="canvas"></canvas>
<canvas id="text_canvas"></canvas>
</div>
<script type="py-game" src="main.py" config="config.toml"></script>
</body>
def __init__(self, title="PyGameBase", width=800, height=600, fps=60):
# -- 省略 --
self.window_font = "msgothic"
self.browser_font = "sans-serif"
def render_text(self, text, x, y, color=(0, 0, 0), font_size=24, bold=False, italic=False):
if sys.platform == "emscripten":
ctx = self.text_canvas_context
boldtext = "bold " if bold else ""
italictext = "italic " if italic else ""
ctx.font = f"{boldtext}{italictext}{font_size}px {self.browser_font}"
if isinstance(color, str):
ctx.fillStyle = color
elif isinstance(color, tuple) and len(color) == 3:
ctx.fillStyle = f"rgb({color[0]}, {color[1]}, {color[2]})"
ctx.fillText(text, x, y)
else:
font = pygame.font.SysFont(self.window_font, font_size, bold, italic)
text_surface = font.render(text, True, color)
self.screen.blit(text_surface, (x, y))
def get_text_size(self, text, font_size=24, bold=False, italic=False):
if sys.platform == "emscripten":
ctx = self.text_canvas_context
boldtext = "bold " if bold else ""
italictext = "italic " if italic else ""
ctx.font = f"{boldtext}{italictext}{font_size}px {self.browser_font}"
metrics = ctx.measureText(text)
return metrics.width, font_size
else:
font = pygame.font.SysFont(self.window_font, font_size, bold, italic)
text_surface = font.render(text, True, (0, 0, 0))
return text_surface.get_size()
async def __start(self):
# -- 省略 --
if sys.platform == "emscripten":
import js
text_canvas = js.document.getElementById("text_canvas")
text_canvas.width = self.width
text_canvas.height = self.height
self.text_canvas_context = text_canvas.getContext("2d")
self.text_canvas_context.textBaseline = "top"
# -- 省略 --
完成品
完成品をgithubにアップロードしました。