Skip to main content

Lesson 14: Share Your Game (pygbag + zip)

  • Module 7: Finish & Share
  • Lesson 14 of 14
  • โฑ๏ธ About 1 h 30 min (instruction + lab)

A game only you can play is a game only you will ever play. In this lesson you make three small changes so Space Salvage runs in a web browser, publish it on itch.io so anyone can play it from a link, and package the source so friends can run it, change it and learn from it.

๐ŸŽฏ Learning Objectives

By the end of this lesson, you will be able to:

  • Explain why a browser game's loop must hand control back to the browser every frame.
  • Convert a pygame-ce game for pygbag with asyncio: an async main(), await asyncio.sleep(0) each frame, and asyncio.run(main()).
  • Build and test the web version locally, then upload it to itch.io as a playable HTML game.
  • Package a clean source zip with main.py, the assets, requirements.txt and a README, using a short Python script.
  • Debug the common web-build problems: a blank page, missing assets and silent audio.

Project: Space Salvage on the web (an itch.io page you can share) and a source zip your friends can run.

In This Lesson

๐ŸŒ Ways to Share a Game

Right now, playing Space Salvage means installing Python, installing pygame-ce and running a .py file. That is fine for a programmer and a wall for everyone else. There are three common ways past it, each for a different audience:

Way to shareWhat players needBest for
Web build (pygbag, hosted on itch.io)A modern web browser and an internet connectionAnyone: family, friends, strangers, a portfolio
Source zipPython and pygame-ceOther learners and programmers who want to read or change your code
Desktop app (PyInstaller)Nothing; they double-click a program built for their operating systemPlayers who want an offline, installed game

This lesson does the first two in full, because together they reach almost everyone: a link anyone can click, and a zip other coders can learn from. The desktop app gets a short optional section at the end of Share the Source.

Where can a web build live? Any site that serves static files can host it: itch.io (made for indie games, with a built-in way to play HTML games on a game page), GitHub Pages, or Netlify. This lesson uses itch.io, because it is free to publish, it puts your game on a page with a description and screenshots, and pygbag's documentation walks through it.

๐Ÿงฉ How pygbag Runs Python in a Browser

Browsers don't run Python. They do run WebAssembly, a compact program format that many languages can be compiled to. The pygbag project provides a copy of CPython and pygame-ce compiled to WebAssembly. pygbag, the tool you install with pip, packs your main.py and assets into a web page that downloads that runtime and runs your game on a <canvas>.

There is one catch. A web page shares a single line of work with the browser. While your code runs, the browser can't draw the canvas, read the keyboard or play sound. A desktop while running: loop never stops, so in a browser it would freeze the tab. The fix is to make the loop pause briefly once per frame and hand control back to the browser, then carry on. Python's built-in asyncio module is designed exactly for code that pauses and resumes like this, so pygbag uses it.

graph LR A["Your loop: events,<br/>update, draw, flip()"] --> B["await asyncio.sleep(0)<br/>pause this frame"] B --> C["Browser: show the canvas,<br/>read keys, play sound"] C --> A

You don't need to learn asyncio in depth for this. Three keywords are enough: async def marks a function that can pause, await is where it pauses, and asyncio.run() starts it.

โœ๏ธ Three Changes for the Web

pygbag's quick-start guide lists what it needs, and your Part 2 game already follows most of it: one game loop inside main(), assets loaded once at the start, every file inside the game folder. What is left: name the file main.py (pygbag looks for that exact name), then make three changes:

  1. Add import asyncio at the top.
  2. Make the loop's function async def main():, and put await asyncio.sleep(0) inside the loop, once per frame. Right after pygame.display.flip() is a good place. Keep the number 0: it means "pause just long enough for the browser to catch up".
  3. Start the game with asyncio.run(main()) at the end of the file, and add nothing after it.

Here are the changed lines of Space Salvage. Everything else stays exactly as it was:

import asyncio                             # change 1
import random
from pathlib import Path

import pygame


async def main():                          # change 2: async def
    # ... set up the window, fonts, assets and sounds exactly as before ...
    while running:
        # ... events, update and draw exactly as before ...
        pygame.display.flip()
        await asyncio.sleep(0)             # change 2: hand control to the browser (keep it 0)

    pygame.quit()


if __name__ == "__main__":
    asyncio.run(main())                    # change 3: start the async main()

The async version still runs on your desktop with python main.py, so you keep one file for both. Here is a complete, short program with the same shape that you can run anywhere. It moves a circle for about five seconds and stops:

import asyncio

import pygame

pygame.init()
screen = pygame.display.set_mode((640, 360))
pygame.display.set_caption("Async loop test")
clock = pygame.time.Clock()
font = pygame.font.Font(None, 48)


async def main():
    x = 0.0
    frames = 0
    running = True
    while running and frames < 300:          # about 5 seconds, then it stops
        dt = clock.tick(60) / 1000
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
        x = (x + 200 * dt) % 640
        frames += 1
        screen.fill((20, 24, 36))
        pygame.draw.circle(screen, (255, 200, 80), (x, 180), 30)
        screen.blit(font.render(f"frame {frames}", True, (230, 230, 230)), (20, 20))
        pygame.display.flip()
        await asyncio.sleep(0)               # let the browser draw and read input
    pygame.quit()
    print(f"Ran {frames} frames.")


asyncio.run(main())

๐Ÿ“ Assets that work on the web

  • Everything inside the game folder. pygbag packages only the folder you give it. A path like ../art/ship.png works on your desktop and fails in the browser.
  • Paths built from the script's folder, as the capstone already does with ASSETS = Path(__file__).parent / "assets", and forward slashes if you ever type a path by hand.
  • OGG for audio, PNG for images. pygbag's documentation asks for OGG sound files (not WAV or MP3) and compressed images rather than BMP. The capstone's assets already are.
  • Load assets at the start, not in the middle of play, which the capstone does in load_assets() and load_sounds().

๐Ÿงช Build and Test Locally

Set up a folder that holds only the game. The folder name becomes the game's name in pygbag:

space_salvage/
โ”œโ”€โ”€ main.py              the async game
โ”œโ”€โ”€ assets/              images and .ogg sounds
โ”œโ”€โ”€ CREDITS.md           where the art and sound came from
โ”œโ”€โ”€ requirements.txt     for the source zip (next section)
โ””โ”€โ”€ README.txt           for the source zip

Install pygbag once, then, from the folder that contains space_salvage, run it on the game folder:

python -m pip install pygbag          # once (py -m pip on Windows)
python -m pygbag space_salvage        # build, then serve it on your computer

pygbag builds the web version into space_salvage/build/web/ and starts a small test server. Open http://localhost:8000 in your browser. The first load is slow, because the browser downloads the Python and pygame-ce runtime from pygbag's servers; later loads use a cache. When the loading bar finishes, click the page if the game waits for you: browsers don't let a page play sound until the player has interacted with it. Stop the server with Ctrl + C in the terminal.

Changed your code? Run the same command again: pygbag rebuilds the package every time.

What you seeLikely causeFix
The page loads, then stays blank or the tab freezesThe loop never awaitsCheck await asyncio.sleep(0) is inside the while loop
Nothing happens at allThe file isn't called main.py, or asyncio.run(main()) is missingRename it; add the last line
Pink placeholder circles instead of artAn image isn't inside the game folder, or its name's capitals differMove it into assets; match names exactly (the web is case-sensitive)
No soundYou haven't clicked the page yet, or a sound isn't OGGClick once; convert the file to OGG
pygbag is "not recognized"The script folder isn't on your PATHUse python -m pygbag (or py -m pygbag on Windows)

To read errors from the web version, open http://localhost:8000?-i for pygbag's debug console, or open your browser's developer tools (F12) and look at the Console tab. A Python traceback shows up there just as it would in a terminal.

โœ… Growth Mindset: A Blank Page Is Just a Quieter Error

The first web build fails for almost everyone, and a blank page can feel worse than a traceback because it doesn't seem to say anything. It does; you just have to open the console to hear it. Your game isn't on the web yet, not "can't be". Check the table above one row at a time, change one thing, rebuild, and reload. You have been debugging this way since your first game window.

๐Ÿ•น๏ธ Publish on itch.io

The local server only works on your computer. To let anyone play, upload the build to itch.io. First, have pygbag make a zip of the web build:

python -m pygbag --archive space_salvage

This writes space_salvage/build/web.zip, containing index.html, the packed game and an icon. Then, on itch.io (following the steps in pygbag's own itch.io guide):

  1. Create a free account, then choose Upload new project.
  2. Give it a title and set Kind of project to HTML.
  3. Upload web.zip and tick This file will be played in the browser.
  4. In the embed options, set the viewport to your window size, 960 ร— 540, and turn on the fullscreen button if you like.
  5. Write a short description: your pitch from the design doc, the controls, and a credits line (for example "Art and sound: Kenney, kenney.nl, CC0").
  6. Save it as a draft first and play it from its page. When it works, set visibility to Public (or restricted, if you only want people with the link) and share the URL.

Each time you improve the game, rebuild with --archive and upload the new web.zip. itch.io also has a command-line upload tool, butler, for people who update often; the web page is all you need for now.

๐Ÿ’ก Why this matters

A link is the difference between "I made a game" and "play my game". It is also the best playtest you will get: people you have never met, playing with nobody explaining anything. Watch for comments, and keep that design doc's could column handy.

๐Ÿ“ฆ Share the Source

Other learners will want to see how you did it. A good source zip holds everything needed to run the game and nothing else:

  • main.py and the assets folder;
  • requirements.txt, the file pip reads to install dependencies. For this game it is one line:
    pygame-ce>=2.5
  • README.txt: what the game is, the controls, how to run it (python -m pip install -r requirements.txt, then python main.py), and the credits;
  • CREDITS.md, so the asset licenses travel with the files.

Leave out the build folder (pygbag's output, which can be rebuilt), __pycache__ folders and old zips. Zipping by hand makes it easy to forget one, so the lab includes a short script, make_source_zip.py, that uses Python's built-in zipfile module. Put it next to the game folder (not inside it) and run:

python make_source_zip.py space_salvage

It walks the folder with Path.rglob("*"), skips anything inside a skipped folder or with a skipped extension, and writes space_salvage_source.zip beside the folder. The heart of it is one small decision function:

SKIP_FOLDERS = {"build", "dist", "__pycache__", ".git", ".venv", "venv"}
SKIP_SUFFIXES = {".pyc", ".zip"}


def should_skip(relative_path):
    """True for files a friend does not need: build output, caches, old zips."""
    if any(part in SKIP_FOLDERS for part in relative_path.parts):
        return True
    return relative_path.suffix in SKIP_SUFFIXES

Before you send it, test it the way a friend would: unzip it into a new folder, install the requirements, and run python main.py. If it runs there, it will run for them.

๐Ÿ–ฅ๏ธ Optional: a desktop app with PyInstaller

PyInstaller bundles Python, pygame-ce and your game into one program people can run without installing Python. It builds for the operating system you run it on, so a Windows build needs a Windows computer. From inside the game folder:

python -m pip install pyinstaller
python -m PyInstaller --onefile --windowed --name SpaceSalvage --add-data "assets:assets" main.py

The program appears in the dist folder. --add-data "assets:assets" copies the assets folder into the bundle, and the game's Path(__file__).parent / "assets" finds it there. (Older PyInstaller versions on Windows need a semicolon: "assets;assets".) Add dist, build and the generated SpaceSalvage.spec to the list of things you don't put in the source zip.

๐Ÿ‹๏ธ Practice Exercise: Space Salvage Goes Live

Objective: make your capstone run in a browser, publish it on itch.io, and create a source zip that runs on someone else's computer.

Time: about 60 minutes. Starter files: space_salvage_web_starter.py (your finished Part 2 game, with to-do comments for the async changes; use your own game instead if you prefer), make_source_zip_starter.py, requirements.txt and README_template.txt.

  1. Make a space_salvage folder. Copy your game into it as main.py, and copy in assets, CREDITS.md and requirements.txt. (โ‰ˆ 5 min)
  2. Make the three async changes. Run python main.py: it must still work on your desktop. (โ‰ˆ 10 min)
  3. Install pygbag, run python -m pygbag space_salvage, and play it at http://localhost:8000. Fix anything the troubleshooting table covers. (โ‰ˆ 15 min)
  4. Run python -m pygbag --archive space_salvage and upload build/web.zip to a new HTML project on itch.io as a draft. Play it from the itch.io page. (โ‰ˆ 10 min)
  5. Fill in README_template.txt and save it as README.txt in the game folder. (โ‰ˆ 5 min)
  6. In make_source_zip_starter.py, finish should_skip() (to-do 1) and the loop that writes each file into the zip (to-do 2). Run it on your game folder. (โ‰ˆ 10 min)
  7. Unzip the result into a new folder and run it from there, then send the itch.io link to one person. (โ‰ˆ 5 min)

You are done when:

  • python main.py still runs the game on your desktop;
  • the game plays at http://localhost:8000 with its art and sound;
  • your itch.io draft page plays the game in the browser;
  • your source zip holds main.py, assets, CREDITS.md, requirements.txt and README.txt, and no build folder, .pyc files or zips;
  • the unzipped copy runs with python main.py.
๐Ÿ’ก Hint

For should_skip(), relative_path.parts is a tuple of the folder and file names, like ("build", "web", "index.html"), and relative_path.suffix is the extension, like ".pyc". In the loop, store each file under the game folder's name, (Path(game_folder.name) / relative).as_posix(), so it unzips into a tidy space_salvage/ folder. If pygbag shows a blank page, check that await asyncio.sleep(0) is indented inside the while loop.

โœ… Example Solution

This is the finished make_source_zip.py. The finished async game is space_salvage_web_solution.py in the lab folder; its only differences from your Part 2 game are the three changes shown in Three Changes for the Web.

"""Make a source zip of your game folder: Intro Lesson 14 (solution).

Put this file NEXT TO your game folder (not inside it) and run:
    python make_source_zip.py space_salvage
It writes space_salvage_source.zip beside the folder, with your code,
assets, requirements.txt and README.txt, and leaves out pygbag's build
folder and Python's cache files.
"""
import sys
import zipfile
from pathlib import Path

SKIP_FOLDERS = {"build", "dist", "__pycache__", ".git", ".venv", "venv"}
SKIP_SUFFIXES = {".pyc", ".zip"}


def should_skip(relative_path):
    """True for files a friend does not need: build output, caches, old zips."""
    if any(part in SKIP_FOLDERS for part in relative_path.parts):
        return True
    return relative_path.suffix in SKIP_SUFFIXES


def make_source_zip(game_folder):
    """Zip game_folder into <name>_source.zip next to it. Returns (zip path, names)."""
    game_folder = Path(game_folder)
    zip_path = game_folder.parent / f"{game_folder.name}_source.zip"
    names = []
    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as archive:
        for path in sorted(game_folder.rglob("*")):
            relative = path.relative_to(game_folder)
            if path.is_file() and not should_skip(relative):
                name = (Path(game_folder.name) / relative).as_posix()
                archive.write(path, name)
                names.append(name)
    return zip_path, names


def main():
    if len(sys.argv) < 2:
        print("Usage: python make_source_zip.py GAME_FOLDER")
        return
    game_folder = Path(sys.argv[1])
    if not (game_folder / "main.py").exists():
        print(f"{game_folder} has no main.py. Is that your game folder?")
        return
    missing = [n for n in ("requirements.txt", "README.txt") if not (game_folder / n).exists()]
    if missing:
        print("Warning: add these before you share:", ", ".join(missing))
    zip_path, names = make_source_zip(game_folder)
    for name in names:
        print("  added", name)
    print(f"Wrote {zip_path} ({len(names)} files).")


if __name__ == "__main__":
    main()

๐Ÿ““ Learning Journal

Take five minutes to write in your learning journal (a notebook or a plain text file works). Jot down:

  • Key concepts you learned today
  • Techniques that clicked (and the ones that haven't, yet)
  • Questions or confusion to bring to the next session
  • Ideas to try in your own game
  • Progress and feelings: how did this lesson go for you?

โœ๏ธ This lesson's prompts:

  1. Who was the first person you sent your game to, and what did they say or do?
  2. Explain in your own words why a browser game needs await asyncio.sleep(0) every frame.
  3. Look back at your first game window from the start of this course. What can you do now that you couldn't then? What game do you want to make next?

๐Ÿ“ Summary

You took your capstone from "runs on my computer" to "anyone can play it". pygbag runs Python and pygame-ce in the browser through WebAssembly, and because a browser tab can't be held by a never-ending loop, the game hands control back once per frame with await asyncio.sleep(0) inside an async main() started by asyncio.run(). You built and tested the web version locally, uploaded pygbag's web.zip to itch.io as an HTML game, and packaged a clean source zip with requirements.txt, a README and credits, checked by unzipping and running it the way a friend would. Congratulations: you have designed, built, polished, playtested and shipped a game.

๐ŸŽ“ Key Takeaways

  • A web build reaches anyone with a browser; a source zip reaches other programmers; a desktop app reaches players who want to play offline.
  • For pygbag: the file is main.py, the loop is in async def main(), it awaits asyncio.sleep(0) every frame, and asyncio.run(main()) is the last line.
  • Keep every asset inside the game folder, load it relative to the script, and use OGG for sound.
  • pygbag folder tests at http://localhost:8000; pygbag --archive folder makes build/web.zip for itch.io.
  • A source zip holds code, assets, requirements.txt, README and credits, never build output or caches; test it by unzipping and running it.

๐Ÿ”ญ Looking Ahead

This is the last lesson of Game Dev I: Intro. Game Dev II: Intermediate builds on everything here, starting with Groups, Layers & Masks and working up to a tower-defense capstone and real desktop builds.

โ“ Common Questions

Does the async version still work on my desktop?

Yes. asyncio.run(main()) runs the same loop on the desktop, and await asyncio.sleep(0) costs almost nothing there. Keep one file, main.py, for both.

Why does the web version need an internet connection?

The page pygbag builds downloads the Python and pygame-ce runtime from pygbag's servers when it loads. Players need a connection to start the game, just like any other web page.

Is itch.io free? Can I charge for my game?

Publishing is free. You can release a game for free, as pay-what-you-want, or at a price; itch.io lets creators choose how much of each sale goes to the site. For a first game, free is a great choice: more people play it, and players are what you want right now.

Can I use modules other than pygame-ce in the web version?

Pure-Python standard-library code usually works, but not everything is included, and packages with compiled parts need versions built for the browser. pygbag's documentation lists what is available. The capstone uses only pygame-ce, random, pathlib and asyncio, which all work.

My game works locally but not on itch.io. What now?

Make sure you uploaded the zip made by --archive (not your source zip) and ticked "This file will be played in the browser". Then open the browser's developer console (F12) on the itch.io page and read the error, as you did locally.

Why use a script for the zip instead of right-click, Compress?

Right-clicking works, but it is easy to include the build folder (making the zip much bigger) or forget the README. A script does it the same way every time, and you can run it again after every update in one command.

๐ŸŽฏ Quick Quiz

Question 1: What does await asyncio.sleep(0) do inside the game loop when it runs under pygbag?

Question 2: What must the file with your game loop be called for pygbag?

Question 3: Which command makes the zip you upload to itch.io?

Question 4: Your ship shows up as a pink circle in the browser but looks fine on your desktop. What is the most likely cause?

Question 5: Which of these belongs in the source zip you share with other programmers?

๐ŸŒŸ Going Further

  • Make it look good on its page: take two screenshots and a short description for your itch.io page, and add a 32 ร— 32 favicon.png to the game folder; pygbag uses it as the web page's icon.
  • Detect the browser: sys.platform == "emscripten" is true when running under pygbag. Use it to skip the Esc-to-quit key on the web, where quitting just leaves a blank canvas.
  • Read the docs: pygbag's guide, its itch.io publishing page, and itch.io's HTML5 games documentation.
  • Thanks: Space Salvage's art and sound come from Kenney's Space Shooter Extension and Sci-Fi Sounds packs (CC0). Credit isn't required, but it is a kind thing to do on your game page.
  • Coming up in Game Dev II: Intermediate: Building Executables covers PyInstaller properly (spec files, where to save settings, macOS apps), and Launch & Live Ops covers store pages, butler uploads and updates.