Pygame for web, part 2
In 2018 I was wondering how to make a web build of a Pygame-based application. The only option at the time was Pyjsdl by James Garnon aka jggatc. It is a re-implementation of Pygame library that can be transpiled into Javascript using pyjs transpiler.
In May 2021 James Garnon released a new project: pyjsdl-ts. It's almost the same as pyjsdl
, but based on Transcrypt, another python-to-javascript transpiler. Unlike the abandoned pyjs
, Transcrypt is still being developed and maintained. I'll review pyjsdl-ts
in part 3, along with alternatives and similar projects. In this post I'll focus on pyjs
and how to make it work under Python 3.
Revisiting part one
Looking back at my old post, I see that I got some things wrong.
It is slow, slower than the original pygame (which is already slow).
Partially true. But Javascript produced by transpiler can be as fast or faster than Python. Sometimes performance can be bad because of ineffective drawing — pyjsdl
doesn't use hardware acceleration for browser canvas.
Debugging compiled code is impossible.
Not true. Pyjsdl can keep track of original Python source and line numbers if you provide debug flags to the transpiler. Though I didn't figure out how to retrieve this information during runtime.
Pyjsdl depends on some legacy libraries (e.g.: GTK2) that can be a real pain to install, especially for Windows users.
Mostly not true. Yes, Pyjsdl depends on Pyjs, which is a legacy library. The rest is wrong. You need python-gtk2
package only to build and run some pyjs
GUI examples. python-gtk2
also depends on gobject
package, and getting those under Windows with pip
requires C++ compiler, some additional libraries installed, etc. But you don't need them to build and run Pygame based apps. In fact, pyjsdl
is very easy to use.
Installing and using pyjsdl
Since I had posted the first part, I received several comments asking me to provide a complete tutorial. Also, the developer of pyjsdl
already has a comprehensive guide posted in their blog, so you may want to check it out first.
Requirements:
- Python 2.7. See the section below about running Pyjsdl with Python 3
- git. On Windows, you can use git for windows.
Steps:
1. Install pyjs
pyjs
is not listed in Python Package index (PyPI). Install it from github:
pip install git+https://github.com/pyjs/pyjs.git#egg=pyjs
2. Install pyjsdl
Download it from the developer's site, clone it from github or download a zip archive from github.
git clone https://github.com/jggatc/pyjsdl.git
3. Prepare your code
Here's a minimal "Hello world" example that should run both in Python interpreter and in browser after having been transpiled to Javascript. It will place the Python logo where you've clicked your mouse. Save this code as hello.py
import os try: import pygame as pg platform = "standalone" res_dir = 'public' except ImportError: import pyjsdl as pg platform = "web" res_dir = '' class Game: def game_loop(self): self.clock.tick(60) # Handle input for event in pg.event.get(): # Check for window close event or escape key if ( event.type == pg.QUIT or event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE ): self.running = False return # Save the the position of mouse click if event.type == pg.MOUSEBUTTONDOWN and event.button == 1: self.click = pg.mouse.get_pos() # Clear screen self.screen.fill((144, 144, 144)) # Draw Python logo at the position of mouse click self.screen.blit(self.logo, self.click) pg.display.flip() def load_resources(self): # Load your images here self.logo = pg.image.load(os.path.join(res_dir, 'python-logo.png')) def web_init(self): self.load_resources() pg.set_callback(self.game_loop) def start(self): pg.init() pg.display.set_caption("Hello Pygame") self.screen = pg.display.set_mode((640, 480)) self.clock = pg.time.Clock() self.click = (0, 0) self.running = True if platform == "web": pg.setup(self.web_init, ['python-logo.png', ]) else: self.load_resources() while self.running: self.game_loop() if __name__ == "__main__": game = Game() game.start()
Note the pg.setup(...)
function call. It is required to make a Pyjsdl build. It accepts two arguments.
The first argument is the function that should be called after all the images have been preloaded. It is where you need to make pygame.image.load(...)
calls. It is required in web context, because browsers have to preload images before they can be used by your app. The second argument is the list of images.
Also note the pg.set_callback(...)
function call. It accepts the function that should be called each frame, your main game loop. In the transpiled code, it will be registered as a callback to the Javascript function window.requestAnimationFrame
. Since it's not an endless while
loop, the application won't quit when you return
from this function.
4. Prepare your resources
Create a folder named public
in your app's directory. Put your image (the Python logo above) in this folder. Pyjs transpiler will pick up all resources from the public
directory and copy them to the output directory.
Notice that for the standalone version you need to prepend public
to the name of your images.
5. Translate your code with pyjs
Make sure that you've put the folder with pyjsdl
source code in the same directory as your hello.py
so it can be imported as a module. Then run the following command:
pyjsbuild hello.py -S
Instead of copying the pyjsdl
module, include the folder containing it with the -I
parameter.
E.g. if you copied (or git cloned) the repository into C:\myfolder\pyjsdl
and the folder containing source code resides in C:\myfolder\pyjsdl\pyjsdl
, the command would be:
pyjsbuild hello.py -S -I "C:\myfolder\pyjsdl"
pyjsbuild
doesn't automatically pick up Python modules to be transpiled from your Python path. They have either to be placed in your app's working directory or to be in the included directories specified with -I
parameter.
Also, you dont' want to accidentally include modules your app doesn't need, so make sure the included directory has only one module in it, pyjsdl
. Otherwise, the program may not compile properly.
In both cases, you need to specify -S
parameter to compile in strict mode. You can check out all available build flags by running pyjsbuild -h
.
HTML and Javascript files are generated in output
directory by default. You can specify a different directory with -o
parameter.
6. Serve your HTML
HTML files with Javascript need to be served to display correctly. Use Python 2 built-in web server:
cd output python -m SimpleHTTPServer
and navigate to http://127.0.0.1:8000. Click on `hello.html' to see your app running.
For Python 3 launch the server with
python -m http.server -d output
7. Distributing your app
To distribute your app, you need to upload the contents of your output
folder — except the lib
folder — to a web server. But if you've built with --dynamic-link
flag, you need to distribute lib
folder as well.
PyJS for Python 3
Here's my attempt at porting pyjs
from Python 2 to Python 3: https://github.com/maximryzhov/pyjs. The updated package contains quick and dirty fixes and shouldn't be considered for production.
If you want to use my version instead of the default version, in step 1 run the following command:
pip install git+https://github.com/maximryzhov/pyjs.git#egg=pyjs
While it may work in some cases, it may not in some others, use it on your own risk.
The incompatibility issues between Python 2 and 3 that I've encountered can be classified into the following groups:
1. Invalid syntax
That was the easiest issue to fix. It even can be automated with tools such as 2to3. In fact, pyjs
installation comes with this library bundled, though it's unclear for me how it is used.
2. String, bytes and unicode
Some built-in functions that return str
in Python 2 return bytes
in Python 3. That results in TypeError
when trying to concatenate bytes object with string. Also, in Python 3 encoding should be specified when opening files.
3. Parsing numbers from string
In Python 2 syntax integers can start with zero. Also it distinguishes between int
and long
type, appending "L" to the latter. For Python 2 this is correct code, but for Python 3 this isn't:
eval("007") eval("1L")
Pyjs uses python 2 module compiler
to analyze the syntax tree of the source code. 'complier' uses eval
function to convert strings to numbers.
The original number parsing function looks like this:
k = eval(nodelist[0].value)
During the transpiling it receives values such as 007
, 1L
as well as valid number strings like 250
, 0.01
, 0x00aabb
, etc. I rewrited it like this:
val = nodelist[0].value # strip leading zeros, except for hex and float if len(val) > 1 and val[0] == '0': if val[1] not in ('.', 'x'): val = val.lstrip('0') # strip L from long integers val = val.rstrip('L') if val: k = eval(val) # account for empty string else: k = 0
It's probably a terrible solution, it would be better to fix the source that emits these string values, not the parse function.
Also the best solution would be to replace obsolete compiler
module with the newer Python 3 ast
module. But my fix is good enough for what it does.
Conclusion
Even if you got it working under Python 3, I still don't recommend using pyjs
+ pyjsdl
because:
- You can run it under Python 3, but still stuck with Python 2.x syntax for your Pygame apps.
- Program correctness is not guaranteed. Tests also need to be ported to Python 3.
- The latest version of
pyjs
was released in 2012. That means:- Incompatibility with newer browsers. Some of the official
pyjs
demos don't work in modern Chrome, for example. - No support for newer Javascript standards.
- Incompatibility with newer browsers. Some of the official
- PyJS doesn't support minifying, obfuscation and compression.
- Performance is still bad. 200 moving sprites was enough to drop FPS below 30, while original Pygame can handle about 20000 on my PC before going under 30.
- There is a better solution, the pyjsdl-ts library by the same author. I'll look at
pyjsdl-ts
and other similar projects in more details in part 3.