0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PyScriptとCPythonで共通のコードで使えるPyGame-ceクラスを作る

Last updated at Posted at 2025-05-31

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にアップロードしました。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?