DEV Community

Eric Dong
Eric Dong

Posted on • Edited on • Originally published at github.com

Playable Sandbox Now Available

This is my blog part 1 of 3 for the v0.4 release of Space War RL!


Normally games made with pygame are not playable from the web. They can only be run from the command line or use PyInstaller or cx_Freeze to create a standalone executable.

I recently discovered pygbag that allows python code to run in a web browser.

You can play my stable version here. You can play my dev version here.

Stable Version

This build will follow my latest github releases/tags.

Dev Version

This build will serve as my testing grounds for new features to solicit more feedback. At the time of writing this post, the dev version contains weapons, ship collisions, and more. My next 2 blog posts in this series will cover my updates.

What is pygbag?

Pygbag is a C runtime linked to cython-wasm compiled to WebAssembly by Emscripten and hosted on pygame-web.github.io. You can read more about pygbag on their wiki page. You can see some demos here.

Issues/Challenges I faced

Patching pygame.time.set_timer

Due to pygbag#16, the built-in function does not work correctly in the pygame-wasm environment. I noticed the patch to set_timer was buggy, so I made an improvement to the patch by handling other behaviors mentioned in the docs (e.g. disabling the timer and discarding an old timer for the same event).

Implementation Details

I created a THREADS dictionary to keep track of all the spawned threads. It is keyed by the event type and contains the uuid of the thread. A delay of 0 will cancel the timer by deleting the event type from the dictionary, which causes the while loop to break in the fire_event function. If there are multiple threads for the same event type, only the latest one is considered due to this condition in the if block: THREADS[event] != thread_uuid.

This is only a temporary workaround until a fix is released upstream. See below for the code example:

def patch_timer():
    import asyncio
    import aio.gthread

    # Global var to keep track of timer threads
    #   - key: event type
    #   - value: thread uuid
    THREADS = {}

    def patch_set_timer(
        event: Union[int, pygame.event.Event], millis: int, loops: int = 0
    ):
        """Patches the pygame.time.set_timer function to use gthreads"""

        dlay = float(millis) / 1000
        cevent = pygame.event.Event(event)
        event_loop = asyncio.get_event_loop()

        async def fire_event(thread_uuid):
            """The thread's target function to handle the timer

            Early exit conditions:
            - event loop is closed
            - event type is no longer in THREADS dictionary
            - the thread's uuid is not the latest one
            - Max loop iterations if loops param is not zero
            """
            loop_counter = 0
            while True:
                await asyncio.sleep(dlay)
                if (
                    event_loop.is_closed()
                    or event not in THREADS
                    or THREADS[event] != thread_uuid
                    or (loops and loop_counter >= loops)
                ):
                    break

                pygame.event.post(cevent)
                loop_counter += 1 if loops else 0

        if dlay > 0:
            # uuid is used to track the latest thread,
            # stale threads will be terminated
            thread_uuid = uuid.uuid4()
            Thread(target=fire_event, args=[thread_uuid]).start()
            THREADS[event] = thread_uuid

        else:
            # This cancels the timer for the event
            if event in THREADS:
                del THREADS[event]

    pygame.time.set_timer = patch_set_timer


if platform.system().lower() == "emscripten":
    patch_timer()
Enter fullscreen mode Exit fullscreen mode

Top comments (0)