Skip to content

Commit 3021be5

Browse files
authored
Merge pull request #183 from gillius/pygame-getting-started
pygame-ce getting started and timing tutorial
2 parents e4f2287 + ffce2b4 commit 3021be5

File tree

2 files changed

+262
-4
lines changed

2 files changed

+262
-4
lines changed

docs/user-guide/configuration.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ If you use JSON, you can make it the value of the `config` attribute:
6161
```
6262

6363
For historical and convenience reasons we still support the inline
64-
specification of configuration information via a _single_ `<py-config>` or
65-
`<mpy-config>` tag in your HTML document:
64+
specification of configuration information for `py` and `mpy` type scripts via a
65+
_single_ `<py-config>` or `<mpy-config>` tag in your HTML document:
6666

6767
```HTML title="Inline configuration via the &lt;py-config&gt; tag."
6868
<py-config>
@@ -76,6 +76,10 @@ specification of configuration information via a _single_ `<py-config>` or
7676

7777
Should you use `<py-config>` or `<mpy-config>`, **there must be only one of
7878
these tags on the page per interpreter**.
79+
80+
Additionally, `<py-config>` only works for `py`/`mpy` type scripts and is not used
81+
with [`py-game`](../pygame-ce) or [`py-editor`](../editor). For these use the config
82+
attribute method.
7983

8084
## Options
8185

docs/user-guide/pygame-ce.md

Lines changed: 256 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,260 @@ your games via a URL!
2727
create a game. Some things may not work because we're running in a
2828
browser context, but play around and let us know how you get on.
2929

30+
## Getting Started
31+
32+
Here are some notes on using PyGame-CE specifically in a browser context with
33+
pyscript versus running locally per
34+
[PyGame-CE's documentation](https://pyga.me/docs/).
35+
36+
1. You can use [pyscript.com](https://pyscript.com) as mentioned in
37+
[Beginning PyScript](../beginning-pyscript.md) for an easy starting
38+
environment.
39+
2. Pyscript's PyGame-CE is under development, so make sure to use the latest
40+
version by checking the `index.html` and latest version on this website. If
41+
using [pyscript.com](https://pyscript.com), the latest version is not always
42+
used in a new project.
43+
3. The game loop needs to allow the browser to run to update the canvas used as
44+
the game's screen. In the simplest projects, the quickest way to do that is
45+
to replace `clock.tick(fps)` with `await asyncio.sleep(1/fps)`, but there
46+
are better ways (discussed later).
47+
4. If you have multiple Python source files or media such as images or sounds,
48+
you need to use the [config attribute](configuration.md) to load the
49+
files into the PyScript environment. The below example shows how to do this.
50+
5. The integrated version of Python and PyGame-CE may not be the latest. In the
51+
browser's console when PyGame-CE starts you can see the versions, and for
52+
example if 2.4.1 is included, you can't use a function marked in the
53+
documentation as "since 2.5".
54+
55+
### Example
56+
57+
This is the example quickstart taken from the [Python Pygame
58+
Introduction](https://pyga.me/docs/tutorials/en/intro-to-pygame.html) on the
59+
PyGame-CE website, modified only to add `await asyncio.sleep(1/60)` (and the
60+
required `import asyncio`) to limit the game to roughly 60 fps.
61+
62+
Note: since the `await` is not in an `async` function, it cannot run using
63+
Python on your local machine, but a solution is
64+
[discussed later](#running-locally).
65+
66+
67+
```python title="quickstart.py"
68+
import asyncio
69+
import sys, pygame
70+
71+
pygame.init()
72+
73+
size = width, height = 320, 240
74+
speed = [2, 2]
75+
black = 0, 0, 0
76+
77+
screen = pygame.display.set_mode(size)
78+
ball = pygame.image.load("intro_ball.gif")
79+
ballrect = ball.get_rect()
80+
81+
while True:
82+
for event in pygame.event.get():
83+
if event.type == pygame.QUIT: sys.exit()
84+
85+
ballrect = ballrect.move(speed)
86+
if ballrect.left < 0 or ballrect.right > width:
87+
speed[0] = -speed[0]
88+
if ballrect.top < 0 or ballrect.bottom > height:
89+
speed[1] = -speed[1]
90+
91+
screen.fill(black)
92+
screen.blit(ball, ballrect)
93+
pygame.display.flip()
94+
await asyncio.sleep(1/60)
95+
```
96+
97+
To run this game with PyScript, use the following HTML file, ensuring a call
98+
to the Python program and a `<canvas id="canvas">` element where the graphics
99+
will be placed. Make sure to update the pyscript release to the latest version.
100+
101+
```html title="index.html"
102+
<!DOCTYPE html>
103+
<html lang="en">
104+
105+
<head>
106+
<title>PyScript Pygame-CE Quickstart</title>
107+
<meta charset="UTF-8">
108+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
109+
<link rel="stylesheet" href="https://pyscript.net/releases/2025.5.1/core.css">
110+
<script type="module" src="https://pyscript.net/releases/2025.5.1/core.js"></script>
111+
</head>
112+
<body>
113+
<canvas id="canvas" style="image-rendering: pixelated"></canvas>
114+
<script type="py-game" src="quickstart.py" config='pyscript.toml'></script>
115+
</body>
116+
</html>
117+
```
118+
119+
!!! Info
120+
121+
The `style="image-rendering: pixelated` on the canvas preserves the
122+
pixelated look on high-DPI screens or when zoomed-in. Remove it to have a
123+
"smoothed" look.
124+
125+
Lastly, you need to define the `pyscript.toml` file to expose any files that
126+
your game loads -- in this case, `intro_ball.gif`
127+
[(download from pygame GitHub)](https://github.com/pygame-community/pygame-ce/blob/80fe4cb9f89aef96f586f68d269687572e7843f6/docs/reST/tutorials/assets/intro_ball.gif?raw=true).
128+
129+
```toml title="pyscript.toml"
130+
[files]
131+
"intro_ball.gif" = ""
132+
```
133+
134+
Now you only need to serve the 3 files to a browser. If using
135+
[pyscript.com](https://pyscript.com) you only need to ensure the content of the
136+
files, click save then run and view the preview tab. Or, if you are on a machine
137+
with Python installed you can do it from a command line running in the same
138+
directory as the project:
139+
140+
```
141+
python -m http.server -b 127.0.0.1 8000
142+
```
143+
144+
This will start a website accessible only to your machine (`-b 127.0.0.1` limits
145+
access only to "localhost" -- your own machine). After running this, you can
146+
visit [http://localhost:8000/](http://localhost:8000/) to run the game in your
147+
browser.
148+
149+
Congratulations! Now you know the basics of updating games to run in PyScript.
150+
You can continue to develop your game in the typical PyGame-CE way.
151+
152+
## Running Locally
153+
154+
Placing an `await` call in the main program script as in the example is not
155+
technically valid Python as it should be in an `async` function. In the
156+
environment executed by PyScript, the code runs in an `async` context so this
157+
works; however, you will notice you cannot run the `quickstart.py` on your
158+
local machine with Python. To fix that, you need to add just a little more
159+
code:
160+
161+
Place the entire game in a function called `run_game` so that function can be
162+
declared as `async`, allowing it to use `await` in any environment. Import the
163+
`asyncio` package and add the `try ... except` code at the end. Now when running
164+
in the browser, `asyncio.create_task` is used, but when running locally
165+
`asyncio.run` is used. Now you can develop and run locally but also support
166+
publish to the web via PyScript.
167+
168+
```python
169+
import asyncio
170+
import sys, pygame
171+
172+
async def run_game():
173+
pygame.init()
174+
175+
# Game init ...
176+
177+
while True:
178+
for event in pygame.event.get():
179+
if event.type == pygame.QUIT: sys.exit()
180+
181+
# Game logic ...
182+
183+
await asyncio.sleep(1/60)
184+
185+
try:
186+
asyncio.get_running_loop() # succeeds if in async context
187+
asyncio.create_task(run_game())
188+
except RuntimeError:
189+
asyncio.run(run_game()) # start async context as we're not in one
190+
```
191+
192+
!!! Info
193+
194+
In the web version, the `sys.exit()` was never used because the `QUIT`
195+
event is not generated, but in the local version, responding to the event
196+
is mandatory.
197+
198+
## Advanced Timing
199+
200+
While the `await asyncio.sleep(1/60)` is a quick way to approximate 60 FPS,
201+
like all sleep-based timing methods in games this is not precise. Generating
202+
the frame itself takes time, so sleeping 1/60th of a second means total frame
203+
time is longer and actual FPS will be less than 60.
204+
205+
A better way is to do this is to run your game at the same frame rate as the
206+
display (usually 60, but can be 75, 100, 144, or higher on some displays). When
207+
running in the browser, the proper way to do this is with the JavaScript API
208+
called `requestAnimationFrame`. Using the FFI (foreign function interface)
209+
capabilities of PyScript, we can request the browser's JavaScript runtime to
210+
call the game. The main issue of this method is it requires work to separate the
211+
game setup from the game's execution, which may require more advanced Python
212+
code such as `global` or `class`. However, one benefit is that the `asyncio`
213+
usages are gone.
214+
215+
216+
When running locally, you get the same effect from the `vsync=1` parameter on
217+
`pygame.display.set_mode` as `pygame.display.flip()` will pause until the screen
218+
has displayed the frame. In the web version, the `vsync=1` will do nothing,
219+
`flip` will not block, leaving the browser itself to control the timing using
220+
`requestAnimationFrame` by calling `run_one_frame` (via `on_animation_frame`)
221+
each time the display updates.
222+
223+
Additionally, since frame lengths will be different on each machine, we need to
224+
account for this by creating and using a `dt` (delta time) variable by using a
225+
`pygame.time.Clock`. We update the speed to be in pixels per second and multiply
226+
by `dt` (in seconds) to get the number of pixels to move.
227+
228+
The code will look like this:
229+
230+
```python
231+
import sys, pygame
232+
233+
pygame.init()
234+
235+
size = width, height = 320, 240
236+
speed = pygame.Vector2(150, 150) # use Vector2 so we can multiply with dt
237+
black = 0, 0, 0
238+
239+
screen = pygame.display.set_mode(size, vsync=1) # Added vsync=1
240+
ball = pygame.image.load("intro_ball.gif")
241+
ballrect = ball.get_rect()
242+
clock = pygame.time.Clock() # New clock defined
243+
244+
def run_one_frame():
245+
for event in pygame.event.get():
246+
if event.type == pygame.QUIT: sys.exit()
247+
248+
# in this 300 is for maximum frame rate only, in case vsync is not working
249+
dt = clock.tick(300) / 1000
250+
251+
ballrect.move_ip(speed * dt) # use move_ip to avoid the need for "global"
252+
# Remaining game code unchanged ...
253+
254+
pygame.display.flip()
255+
256+
257+
# PyScript-specific code to use requestAnimationFrame in browser
258+
try:
259+
from pyscript import window
260+
from pyscript import ffi
261+
# Running in PyScript
262+
def on_animation_frame(dt):
263+
# For consistency, we use dt from pygame's clock even in browser
264+
run_one_frame()
265+
window.requestAnimationFrame(raf_proxy)
266+
raf_proxy = ffi.create_proxy(on_animation_frame)
267+
on_animation_frame(0)
268+
269+
except ImportError:
270+
# Local Execution
271+
while True:
272+
run_one_frame()
273+
```
274+
275+
A benefit of `vsync` / `requestAnimationFrame` method is that if the game is
276+
running too slowly, frames will naturally be skipped. A drawback is that in the
277+
case of skipped frames and different displays, `dt` will be different. This can
278+
cause problems depending on your game's physics code; the potential solutions
279+
are not unique to the PyScript situation and can be found elsewhere online as an
280+
exercise for the reader. For example, the above example on some machines the
281+
ball will get "stuck" in the sides. In case of issues the `asyncio.sleep` method
282+
without `dt` is easier to deal with for the beginning developer.
283+
30284
## How it works
31285

32286
When a `<script type="py-game"></script>` element is found on the page a
@@ -37,8 +291,8 @@ element id that will be used to render the game. If no target attribute is
37291
defined, the script assumes there is a `<canvas id="canvas">` element already
38292
on the page.
39293

40-
A config attribute can be specified to add extra packages but right now that's
41-
all it can do.
294+
A config attribute can be specified to add extra packages or bring in additional
295+
files such as images and sounds but right now that's all it can do.
42296

43297
!!! Info
44298

0 commit comments

Comments
 (0)