diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index b8f9a0a..ba2d14c 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -61,8 +61,8 @@ If you use JSON, you can make it the value of the `config` attribute: ``` For historical and convenience reasons we still support the inline -specification of configuration information via a _single_ `` or -`` tag in your HTML document: +specification of configuration information for `py` and `mpy` type scripts via a +_single_ `` or `` tag in your HTML document: ```HTML title="Inline configuration via the <py-config> tag." @@ -76,6 +76,10 @@ specification of configuration information via a _single_ `` or Should you use `` or ``, **there must be only one of these tags on the page per interpreter**. + + Additionally, `` only works for `py`/`mpy` type scripts and is not used + with [`py-game`](../pygame-ce) or [`py-editor`](../editor). For these use the config + attribute method. ## Options diff --git a/docs/user-guide/pygame-ce.md b/docs/user-guide/pygame-ce.md index a81766e..0220f2a 100644 --- a/docs/user-guide/pygame-ce.md +++ b/docs/user-guide/pygame-ce.md @@ -27,6 +27,260 @@ your games via a URL! 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](https://pyga.me/docs/). + +1. You can use [pyscript.com](https://pyscript.com) as mentioned in + [Beginning PyScript](../beginning-pyscript.md) for an easy starting + environment. +2. 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](https://pyscript.com), the latest version is not always + used in a new project. +3. 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)` with `await asyncio.sleep(1/fps)`, but there + are better ways (discussed later). +4. If you have multiple Python source files or media such as images or sounds, + you need to use the [config attribute](configuration.md) to load the + files into the PyScript environment. The below example shows how to do this. +5. 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](https://pyga.me/docs/tutorials/en/intro-to-pygame.html) 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](#running-locally). + + +```python title="quickstart.py" +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 `` element where the graphics +will be placed. Make sure to update the pyscript release to the latest version. + +```html title="index.html" + + + + + PyScript Pygame-CE Quickstart + + + + + + + + + + +``` + +!!! 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)](https://github.com/pygame-community/pygame-ce/blob/80fe4cb9f89aef96f586f68d269687572e7843f6/docs/reST/tutorials/assets/intro_ball.gif?raw=true). + +```toml title="pyscript.toml" +[files] +"intro_ball.gif" = "" +``` + +Now you only need to serve the 3 files to a browser. If using +[pyscript.com](https://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: + +``` +python -m http.server -b 127.0.0.1 8000 +``` + +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/](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. + +```python +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: + +```python +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 `` 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 defined, the script assumes there is a `` element already on the page. -A config attribute can be specified to add extra packages but right now that's -all it can do. +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