Game development and more

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:

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

python-logo

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:

  1. You can run it under Python 3, but still stuck with Python 2.x syntax for your Pygame apps.
  2. Program correctness is not guaranteed. Tests also need to be ported to Python 3.
  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.
  4. PyJS doesn't support minifying, obfuscation and compression.
  5. 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.
  6. 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.

Share Back to main