Python 3 and PyGame in 2018

Table of Contents

1 Why PyGame?

PyGame is a nice library for getting started with game development. It is a good way to learn programming in a "real programming language" while making games1. There is very "magic" going on in PyGame, and you can go from print("Hello World") to drawing a triangle on the screen and then to Pong or Tetris with a smooth learning curve.

PyGame is also an easy way to get your feet wet in game development if you know and use Python already, for example in machine learning or web development. That's how I came to learn PyGame.

2 Why Not?

PyGame is just a low-level library for creating windows, drawing on the screen, reading joystick input, and playing sounds. OpenFL, Löve2D, FNA and LibGDX fill similar niches in the Haxe, Lua, .Net and Java ecosystems, respectively. If you want a full-featured engine, use Unity3D, UE4, or Godot. If you don't want to learn to code, you will probably fare better with GameMaker or Stencyl2. If you want to tell an interactive story with little actual gameplay, maybe Twine or Ren'Py are more your thing.

3 Explicit Is Better Than Implicit

Maybe PyGame is not for everybody… However, if you do just want to make small 2D games, and don't want to rely on automagic functionality, PyGame could for you. That means you have to code your own game loop3, poll the keyboard or game pad, and update the screen yourself.

Your game loop will look something like this:

import pygame
pygame.init()

# ...
# setup stuff here

screen=pygame.display.set_mode(screen_size)
clock=pygame.clock.Clock()
running=True

while running:
    clock.tick(30)

    events = pygame.event.get()
    keys =   pygame.key.get_pressed()
    for e in events:
        if e.type==pygame.QUIT:
            running=False
        elif e.type==pygame.KEYDOWN and #...
            # handle jumping etcetera

    # ...
    # move sprites around, collision detection, etc

    # ...
    # draw 

    pygame.display.update()
pygame.quit()

The ease of just drawing pixels on the screen and the lack of hidden features sometimes makes it harder to get started making certain games genres. If you start from scratch in Unity3D, you already have a level and a camera and you can just create a sphere GameObject and check a box to enable the physics engine. If you draw a circle on the screen in PyGame, it's just pixels on the screen. These pixels don't know anything about cameras, raycasting, or physics, so if you want to move the view around, make your circle clickable, or have it bounce off walls, you have to connect the systems yourself. PyGame gives you tools to check for collisions, a Sprite class that is the closest equivalent to GameObject, and a way to find out where on the window the mouse was clicked.

If you want to use a pre-made physics engine like Box2D, or roll your own, you have to write some glue code, but PyGame will stay out of your way.

This can even be a tremendous advantage when you want to make a game like Tetris4, or like SimCity, where you don't want to use 3D collision detection and a physics engine anyway, and collision detection happens on a grid. A game like Braid, where you can rewind the entire game state, will also be much easier if you have full control over your game loop. You can do all these things in a big and opinionated engine, as Hearthstone (a card game made in Unity3D) has showed us, but if don't need these features, you sometimes have to code around them. If you code around features you don't need, or re-build your own systems on top of a commercial engine, you might end up writing more code than if you had just started from scratch5.

All of this goes double for tools like PuzzleScript, RPG Maker, GameMaker, Bitsy or Twine. These are all geared towards certain kinds of games, and while they make it really easy to make that kind of game, they make you jump through hoops if you want to build anything else.

4 Three Pain Points

Although I just told you that PyGame is a great library, it is not as popular among game developers as it should be. This is mostly due to three problems dating back to the Python 3 days: multi-threading, deployment, and HD screen resolutions.

Multi-threading: Due to the GIL, an unfortunate implementation detail in CPython6, you can't have two threads execute Python code at the same time. That means your multi-threaded Python programs will still run in parallel, but there will only ever be one thread running at any time. On a single-core machine, there is not that much to worry about, because you only have one core that can run code anyway, but on a multi-core machine, you can't really take advantage of the additional cores. Still, what you could do is have one thread handle input and draw to the screen, while another, long-running thread does complicated enemy AI calculations that take longer than one frame to complete. That won't make your code run any faster in aggregate, but it will can at least ensure that the tick-input-update-draw cycle of the game loop runs at a smooth frame rate while the AI takes a lot of time.

If you're using i/o operations from the Python standard library, long-running numpy code or clock.tick(FPS) in PyGame, these libraries do the right thing and tell the Python interpreter that they are entering a section of native code during which Python code can safely run in another thread. But in general, you could have a Python function that calls native code that doesn't return for a while, so the Python interpreter is executing the same line of Python and other threads can't run. If that bit of native code is waiting for input, your whole program will freeze until the input arrives, even if you have ten other threads waiting that could do useful things in the mean time.

There are two modules to help you with concurrency in Python 3: multiprocessing and asyncio. The first has been around since Python 2.6, and lets you execute Python code in another process. This is useful if you have long-running computations like enemy AI that you want to run on another core. The other, asyncio is new in Python 3.37. With asyncio, you can run multiple coroutines in the same thread. A scheduling object called an event loop will execute coroutines piecemeal whenever there is input available. Because asynchronous coroutines are basically Python functions with some extra syntactic sugar, you can have tens of thousands of them running at the same time, unlike processes or kernel threads, which need a comparatively large amount of RAM to create and some time to context-switch. The use case for asyncio is pretty much the opposite of the use case of multiprocessing: Lots of threads8 waiting for i/o instead of long-lived processes that crunch numbers.

Deployment: There used to be no simple and easy way to package Python 2 games for end users. Sure, you could ask them to install python, either from Python.org or from the package manager of their choice, and give them the source code. Or you could use py2exe or py2app and create portable versions of a python script, but you couldn't build Windows, Mac and Linux executables from the same configuration file, and only on the respective platform. There were multiple competing standards for packaging python modules as source distributions: distutils, setuptools, distribute, and distutils2.

In the mean time, a smart person invented cxfreeze, which would automatically package every single dependency, every resource file and library used by your program, into one big archive. Unfortunately, that approach tended to pull in a bunch of DLLs that are already present on most end users machines. You also need to have the script already running on the target platform to analyse the dependencies at runtime.

With Python 3, setuptools has won, and the others have been merged into it9. There is a new file format for packaging Python modules called wheels10, that lets users install modules with native code without a C compiler. PyInstaller11 lets you build self-contained, portable versions of scripts on Windows, Mac or Linux based on the specification, and pynsist12 lets you create a windows installer for a Python script from any platform, because it downloads windows-compatible wheels and includes them without setting up a cross-compiling environment. The Panda3D project is even working on a cross-platform binary packaging system for Python 3 games based on setuptools!

HD screens: The software-based rendering in PyGame gets slower proportionally to the number of pixels. If you draw a 16x16 character sprite onto the screen, that makes 256 pixels. If you blit a background picture into a 640x480 (VGA) window, that's 307200 pixels already. If you scale that window up to 1280x960, that's four times as many pixels, and 1080p full-HD resolution has 6.75 times as many pixels as VGA. Things that were quite fast at low resolutions become a real bottleneck on HD screens, and you really feel the burn when you run PyGame on devices with a slower CPU and a HD screen, like a phone or a Raspberry PI.

Even if you run your game logic at a low resolution, and just scale up the screen to HD with pygame.transform.scale in the final step, that still means moving lots of pixels around in software. Scaling your game up to full-screen on a HD display can easily take the majority of your processing time. And remember, we effectively have only one core!

If you could copy your 320x240 surface into a texture and scale up your pixel art with the GPU, you eliminate a major bottleneck. The GPU is surprisingly fast and energy-efficient in phones, tablets and on the raspberryPI.

Fortunately, instead of directly drawing pixels into your window, PyGame can give you an OpenGL context and let you use whatever OpenGL bindings you like. In the past, that would have been with PyOpenGL, or pyglet. With Python 3, we can use ModernGL. ModernGL is much simpler than the other two. It targets only modern versions of OpenGL. Unlike PyOpenGL and pyglet, there is no support for immediate mode or the old fixed-function pipeline. You have to use vertex buffer objects and custom shaders. It's fast, and it lets you do fancy post-processing effects in addition to scaling.

If you need more performance, or fancier effects, you can incrementally transition from drawing into PyGame Surfaces towards creating individual textures for your Sprites and rendering directly with OpenGL.

5 Other Cool Libraries

The missing level editor: Tiled13 is a great level editor. You can use PyTMX14 to read the maps files, and pyscroll15 to render them.

Discord Rich Presence: All the cool kids are on Discord16. With pypresence17, you can let your player's friends know what the game is called, which level they are on, and how many points they have scored. It definitely beats having "now playing Python3.exe" as your Discord status line.

Creating Videos: moviePy18 gives you a simple API to create, load and edit GIFs, WEBM and other video file formats.

6 More To Come

The problems I used to have in PyGame haven't disappeared, but in 2018 and with Python 3.6, they have become solvable. It is possible to started making games with PyGame, and to polish and deploy them on Steam and itch.io. There used to be a tool to deploy PyGame to Android phones and tablets, but that was based on Python 2.7 and some hacks to get everything to run. With the current effort to port PyGame to SDL2, we can soon deploy games written in PyGame to Windows, Mac, Linux, Android and all major consoles, which support SDL219

This post is longer than I thought already, so I'll take a break here. In the coming days I will write more about using the asyncio module to do i/o in PyGame without tanking your frame rate, using the moderngl module to scale up your pixel art, and trying out deploy-NG, a setuptools extension from Panda3D to distribute games to end users.

Footnotes:

1
I recommend Al Sweigart's tutorials at http://inventwithpython.com/, and http://inventwithpython.com/pygame/
2
Maybe you want to start designing games first instead of learning to program, so you start out with these tools, but I would recommend you learn at least a little bit of a real programming language as well, to get some perspective. If you learn C#, which is a real programming language, together with Unity or Godot, please look up which features are part of C# and which are unique to your engine.
4
I am speaking from experience here. We made a Tetris-like game in Unity, and it was not worth the trouble. You can see the result here, but it's nothing special: https://blubberquark.itch.io/tetrominions
5
Please don't write an engine from scratch! People like Jon Blow, Tommy Refenes, John Carmack, David Pittman or Notch wrote their own 3D engines for various reasons. That said, if you actually need a big 3D game engine for your game, please don't write your own on top of PyGame. You are not John Carmack (unless you are, in which case you should stop reading this because you know more about programming than I do). Use Panda3D or Godot if you want something that is open source and Python-based (Godot Python support is still in beta, but getting there). Or use one of the commercial engines listed above.
6
I'm simplifying massively here. Look up the Global Intepreter Lock if you want to know more. The GIL also allows PyGame to call SDL functions without worrying too much about whether SDL is thread-safe. It's not all bad.
7
Only with Python 3.5 it has become really usable, though, and they are still working out the kinks in the API.
8
I mean that as in lightweight threads, Erlang threads, green threads.
9
You can still find distutils in Python 3 for backward compatibility reasons.
16
It manages to combine the good parts of Skype, Slack, and TeamSpeak, but it's not open source. https://discordapp.com/
19
SDL2 already runs on the Nintendo Switch, for example. Unfortunately, the licensing terms of the Switch developer kit prohibit you from publicly sharing code that targets the Switch API in public. You have to contact Ryan Gordon and ask him for access, after signing the NDA. I am not a lawyer, this is not legal advice, but as far as I understand it, although the licensing terms of PyGame and SDL allow you to share the code, Nintendo could still sue you for breach of contract. That problem is not unique to PyGame though. A similar workaround exists in the Haxe world, where you have to contact Lars Doucet to get access to a version of OpenFL for Switch. There is also a homebrew version of Python available at https://github.com/nx-python/PyNX, but the NDA might prohibit you from using homebrew and reverse engineering.

Author: Robert Pfeiffer

Created: 2018-08-29 Mi 14:05

Validate