Lesson 14: Share Your Game (pygbag + zip)
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 asyncmain(),await asyncio.sleep(0)each frame, andasyncio.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.txtand 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 share | What players need | Best for |
|---|---|---|
| Web build (pygbag, hosted on itch.io) | A modern web browser and an internet connection | Anyone: family, friends, strangers, a portfolio |
| Source zip | Python and pygame-ce | Other learners and programmers who want to read or change your code |
| Desktop app (PyInstaller) | Nothing; they double-click a program built for their operating system | Players 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.
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:
- Add
import asyncioat the top. - Make the loop's function
async def main():, and putawait asyncio.sleep(0)inside the loop, once per frame. Right afterpygame.display.flip()is a good place. Keep the number0: it means "pause just long enough for the browser to catch up". - 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.pngworks 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()andload_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 see | Likely cause | Fix |
|---|---|---|
| The page loads, then stays blank or the tab freezes | The loop never awaits | Check await asyncio.sleep(0) is inside the while loop |
| Nothing happens at all | The file isn't called main.py, or asyncio.run(main()) is missing | Rename it; add the last line |
| Pink placeholder circles instead of art | An image isn't inside the game folder, or its name's capitals differ | Move it into assets; match names exactly (the web is case-sensitive) |
| No sound | You haven't clicked the page yet, or a sound isn't OGG | Click once; convert the file to OGG |
pygbag is "not recognized" | The script folder isn't on your PATH | Use 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):
- Create a free account, then choose Upload new project.
- Give it a title and set Kind of project to HTML.
- Upload
web.zipand tick This file will be played in the browser. - In the embed options, set the viewport to your window size, 960 ร 540, and turn on the fullscreen button if you like.
- 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").
- 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.pyand theassetsfolder;requirements.txt, the file pip reads to install dependencies. For this game it is one line:pygame-ce>=2.5README.txt: what the game is, the controls, how to run it (python -m pip install -r requirements.txt, thenpython 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.
- Make a
space_salvagefolder. Copy your game into it asmain.py, and copy inassets,CREDITS.mdandrequirements.txt. (โ 5 min) - Make the three async changes. Run
python main.py: it must still work on your desktop. (โ 10 min) - Install pygbag, run
python -m pygbag space_salvage, and play it at http://localhost:8000. Fix anything the troubleshooting table covers. (โ 15 min) - Run
python -m pygbag --archive space_salvageand uploadbuild/web.zipto a new HTML project on itch.io as a draft. Play it from the itch.io page. (โ 10 min) - Fill in
README_template.txtand save it asREADME.txtin the game folder. (โ 5 min) - In
make_source_zip_starter.py, finishshould_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) - 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.pystill 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.txtandREADME.txt, and nobuildfolder,.pycfiles 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:
- Who was the first person you sent your game to, and what did they say or do?
- Explain in your own words why a browser game needs
await asyncio.sleep(0)every frame. - 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 inasync def main(), it awaitsasyncio.sleep(0)every frame, andasyncio.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 foldertests at http://localhost:8000;pygbag --archive foldermakesbuild/web.zipfor 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.pngto 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.