PyGame Support
Danger
Support for PyGame-CE is experimental and its behaviour is likely to change as we get feedback and bug reports from the community.
Please bear this in mind as you try PyGame-CE with PyScript, and all feedback, bug reports and constructive critique is welcome via discord or GitHub.
PyGameCE is a Python library for building powerful games
(so says their website). They also say, to get started you just need to
pip install pygame-ce
.
Thanks to work in the upstream Pyodide project
PyGame-CE is available in PyScript and to get started all you need to do is:
<script type="py-game" src="my_game.py"></script>
Now you don't even need to
pip install
the library! It comes with PyScript by default, and you can share
your games via a URL!
Info
Please refer to PyGame-CE's extensive documentation for how to create a game. Some things may not work because we're running in a browser context, but play around and let us know how you get on.
Getting Started
Here are some notes on using PyGame-CE specifically in a browser context with pyscript versus running locally per PyGame-CE's documentation.
- You can use pyscript.com as mentioned in Beginning PyScript for an easy starting environment.
- Pyscript's PyGame-CE is under development, so make sure to use the latest
version by checking the
index.html
and latest version on this website. If using pyscript.com, the latest version is not always used in a new project. - The game loop needs to allow the browser to run to update the canvas used as
the game's screen. In the simplest projects, the quickest way to do that is
to replace
clock.tick(fps)
withawait asyncio.sleep(1/fps)
, but there are better ways (discussed later). - If you have multiple Python source files or media such as images or sounds, you need to use the config attribute to load the files into the PyScript environment. The below example shows how to do this.
- The integrated version of Python and PyGame-CE may not be the latest. In the browser's console when PyGame-CE starts you can see the versions, and for example if 2.4.1 is included, you can't use a function marked in the documentation as "since 2.5".
Example
This is the example quickstart taken from the Python Pygame
Introduction on the
PyGame-CE website, modified only to add await asyncio.sleep(1/60)
(and the
required import asyncio
) to limit the game to roughly 60 fps.
Note: since the await
is not in an async
function, it cannot run using
Python on your local machine, but a solution is
discussed later.
import asyncio
import sys, pygame
pygame.init()
size = width, height = 320, 240
speed = [2, 2]
black = 0, 0, 0
screen = pygame.display.set_mode(size)
ball = pygame.image.load("intro_ball.gif")
ballrect = ball.get_rect()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT: sys.exit()
ballrect = ballrect.move(speed)
if ballrect.left < 0 or ballrect.right > width:
speed[0] = -speed[0]
if ballrect.top < 0 or ballrect.bottom > height:
speed[1] = -speed[1]
screen.fill(black)
screen.blit(ball, ballrect)
pygame.display.flip()
await asyncio.sleep(1/60)
To run this game with PyScript, use the following HTML file, ensuring a call
to the Python program and a <canvas id="canvas">
element where the graphics
will be placed. Make sure to update the pyscript release to the latest version.
<!DOCTYPE html>
<html lang="en">
<head>
<title>PyScript Pygame-CE Quickstart</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="stylesheet" href="https://pyscript.net/releases/2025.5.1/core.css">
<script type="module" src="https://pyscript.net/releases/2025.5.1/core.js"></script>
</head>
<body>
<canvas id="canvas" style="image-rendering: pixelated"></canvas>
<script type="py-game" src="quickstart.py" config='pyscript.toml'></script>
</body>
</html>
Info
The style="image-rendering: pixelated
on the canvas preserves the
pixelated look on high-DPI screens or when zoomed-in. Remove it to have a
"smoothed" look.
Lastly, you need to define the pyscript.toml
file to expose any files that
your game loads -- in this case, intro_ball.gif
(download from pygame GitHub).
Now you only need to serve the 3 files to a browser. If using pyscript.com you only need to ensure the content of the files, click save then run and view the preview tab. Or, if you are on a machine with Python installed you can do it from a command line running in the same directory as the project:
This will start a website accessible only to your machine (-b 127.0.0.1
limits
access only to "localhost" -- your own machine). After running this, you can
visit http://localhost:8000/ to run the game in your
browser.
Congratulations! Now you know the basics of updating games to run in PyScript. You can continue to develop your game in the typical PyGame-CE way.
Running Locally
Placing an await
call in the main program script as in the example is not
technically valid Python as it should be in an async
function. In the
environment executed by PyScript, the code runs in an async
context so this
works; however, you will notice you cannot run the quickstart.py
on your
local machine with Python. To fix that, you need to add just a little more
code:
Place the entire game in a function called run_game
so that function can be
declared as async
, allowing it to use await
in any environment. Import the
asyncio
package and add the try ... except
code at the end. Now when running
in the browser, asyncio.create_task
is used, but when running locally
asyncio.run
is used. Now you can develop and run locally but also support
publish to the web via PyScript.
import asyncio
import sys, pygame
async def run_game():
pygame.init()
# Game init ...
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT: sys.exit()
# Game logic ...
await asyncio.sleep(1/60)
try:
asyncio.get_running_loop() # succeeds if in async context
asyncio.create_task(run_game())
except RuntimeError:
asyncio.run(run_game()) # start async context as we're not in one
Info
In the web version, the sys.exit()
was never used because the QUIT
event is not generated, but in the local version, responding to the event
is mandatory.
Advanced Timing
While the await asyncio.sleep(1/60)
is a quick way to approximate 60 FPS,
like all sleep-based timing methods in games this is not precise. Generating
the frame itself takes time, so sleeping 1/60th of a second means total frame
time is longer and actual FPS will be less than 60.
A better way is to do this is to run your game at the same frame rate as the
display (usually 60, but can be 75, 100, 144, or higher on some displays). When
running in the browser, the proper way to do this is with the JavaScript API
called requestAnimationFrame
. Using the FFI (foreign function interface)
capabilities of PyScript, we can request the browser's JavaScript runtime to
call the game. The main issue of this method is it requires work to separate the
game setup from the game's execution, which may require more advanced Python
code such as global
or class
. However, one benefit is that the asyncio
usages are gone.
When running locally, you get the same effect from the vsync=1
parameter on
pygame.display.set_mode
as pygame.display.flip()
will pause until the screen
has displayed the frame. In the web version, the vsync=1
will do nothing,
flip
will not block, leaving the browser itself to control the timing using
requestAnimationFrame
by calling run_one_frame
(via on_animation_frame
)
each time the display updates.
Additionally, since frame lengths will be different on each machine, we need to
account for this by creating and using a dt
(delta time) variable by using a
pygame.time.Clock
. We update the speed to be in pixels per second and multiply
by dt
(in seconds) to get the number of pixels to move.
The code will look like this:
import sys, pygame
pygame.init()
size = width, height = 320, 240
speed = pygame.Vector2(150, 150) # use Vector2 so we can multiply with dt
black = 0, 0, 0
screen = pygame.display.set_mode(size, vsync=1) # Added vsync=1
ball = pygame.image.load("intro_ball.gif")
ballrect = ball.get_rect()
clock = pygame.time.Clock() # New clock defined
def run_one_frame():
for event in pygame.event.get():
if event.type == pygame.QUIT: sys.exit()
# in this 300 is for maximum frame rate only, in case vsync is not working
dt = clock.tick(300) / 1000
ballrect.move_ip(speed * dt) # use move_ip to avoid the need for "global"
# Remaining game code unchanged ...
pygame.display.flip()
# PyScript-specific code to use requestAnimationFrame in browser
try:
from pyscript import window
from pyscript import ffi
# Running in PyScript
def on_animation_frame(dt):
# For consistency, we use dt from pygame's clock even in browser
run_one_frame()
window.requestAnimationFrame(raf_proxy)
raf_proxy = ffi.create_proxy(on_animation_frame)
on_animation_frame(0)
except ImportError:
# Local Execution
while True:
run_one_frame()
A benefit of vsync
/ requestAnimationFrame
method is that if the game is
running too slowly, frames will naturally be skipped. A drawback is that in the
case of skipped frames and different displays, dt
will be different. This can
cause problems depending on your game's physics code; the potential solutions
are not unique to the PyScript situation and can be found elsewhere online as an
exercise for the reader. For example, the above example on some machines the
ball will get "stuck" in the sides. In case of issues the asyncio.sleep
method
without dt
is easier to deal with for the beginning developer.
How it works
When a <script type="py-game"></script>
element is found on the page a
Pyodide instance is bootstrapped with the pygame-ce
package already included.
Differently from other scripts, py-game
cannot currently work through a
worker and it uses an optional target attribute to define the <canvas>
element id that will be used to render the game. If no target attribute is
defined, the script assumes there is a <canvas id="canvas">
element already
on the page.
A config attribute can be specified to add extra packages or bring in additional files such as images and sounds but right now that's all it can do.
Info
Sometimes you need to gather text based user input when starting a game.
The usual way to do this is via the builtin input
function.
Because PyGame-CE only runs on the main thread, the only way to block
your code while it waits for user input
is to use a
JavaScript prompt
instead of input typed in via a terminal. PyScript handles this
automatically for you if you use the input
function.
This is an experiment, but:
- It is possible to use regular PyScript to load the pygame-ce package and use
all the other features. But there be dragons! This helper simply allows
multiple games on a page and forces game logic to run on the main thread to
reduce confusion around attributes and features when the
pygame-ce
package is meant to be used. Put simply, we make it relatively safe and easy to use. - The fact
pygame-ce
is the default "game engine" does not mean in the future PyScript won't have other engines also available. - Once again, as this is an experiment, we welcome any kind of feedback, suggestions, hints on how to improve or reports of what's missing.
Other than that, please go make and share wonderful games. We can't wait to see what you come up with.