Over the last year, I have been teaching Python games development at a company called Software Academy. I have also been teaching VFX and animation.
After learning the basics with text based games and the Turtle module, we use Pygame to create our first 2D sprite based games. The students are quite young, the youngest being about 8 years old, and I soon found that Pygame was difficult for them to break into. There is a lot of information to learn in a short space of time, before they even get started. Often students forget important things before they need to use them again.
Given that this term was also intended as an introduction to object-oriented programming, I needed some simple objects to teach them with. Pygame sprites and other objects are far from simple.
While teaching my first couple of python courses, I was developing a solution—a wrapper module that interfaces with pygame, wrapping and simplifying many of the classes. Additionally, it includes the GameController, a class that handles the input/update/render loop, loading certain resources, and gamestates.
This has worked well in every python course I have taught since, where we were able to create more interesting and varied games, and proved simpler and easier to understand for the young students. They grasp it much more easily than plain pygame, and gain an understanding of object-oriented programming quite quickly. They often come up with their own additions to our games, and the simplified nature of the module allows them to achieve these self-appointed goals.
The biggest advantage by far however, is that it requires very little boilerplate code, meaning kids can jump straight in and start writing game logic. The following is all that is needed to create our first “game”.
import sa_pgtraining as pgt class GameController(pgt.GameController): #This object controls all the others. It makes our game run def game_start(self): pgt.Text(self, text="Hello World!", colour=pgt.WHITE) Terrance(self) class Terrance(pgt.Sprite): #This object uses the default image of a 32*32 red square def update_event(self): #this method runs once per frame when not paused. #just like in pygame, sprites can be moved like this: self.rect.x += 10 #after setting up our game, we run it by calling the GameController GameController()
The first major addition to the module was the GameController class. This handles most of the boilerplate code for a pygame program. It initiates pygame, sets up a display, organises sprite groups and collision groups, and runs the main game loop—gathering inputs, running update methods and rendering. It also has a number of other methods and features.
The update event can be paused by changing the controller’s “paused” attribute to True. This results in the game being frozen until unpaused, as none of the objects are updated. While the game is paused, the “pause_update” function is run instead. This can be used to create a pause menu, or a game over screen, where the game will be left unchanged until triggered to resume.
The GameController handles loading of fonts, and creating different versions of fonts at user specified scales and styles.
The GameController also acts as a useful reference point to all other objects, as every other game object stores a pointer to it ( “self.controller” ). This makes it easier to reference specific instances of a class – for example the player object, because they can be stored as an attribute of the controller. In the same way, it can be used to store variables which should be accessible to all objects (score for example), without needing to resort to the global keyword, which is often seen as an anti-pattern in Python, and would need further explanation to the students.
The biggest changes I made with sa_pgtraining were with the sprite class. Firstly I simplified the image loading and rendering process quite a bit. Pygame does not initially support rotating or scaling images. The surface.rotozoom function exists, however this creates a new surface with the transformation applied. Since horizontal and vertical dimensions of a rotated image are always larger than the original, if you simply replace your image with a rotated version, the empty pixels at the edge of the sprite will quickly grow exponentially larger until the game runs low on memory. This also does not preserve the original image, so even if you counter this, the image quality will degrade slightly each time a transformation is applied.
I fixed this by storing an original, unmodified image that is then copied to self.image with transformations applied. I also ensured that all transformations are centred, and update the collision mask.
I built an animation system, where a sequence of images can be loaded instead, either from a sequence of files, or from a spritesheet. the self.image_frame attribute can be changed to switch frames.
Sprites, as well as most other objects, now have event functions, which are empty by default, but can be overwritten to create certain behaviour at specific times. The most important are the update_event and create_event, which run every frame, and when that object is created, respectively. The GameController also has the pause_update and the game_start functions.
Finally, sprites can be made invisible by removing them from the visible_sprites group, but are still updated and trigger collisions.
Pygame’s image functions are fairly basic, and lack any form of spritesheets. Spritesheets are a much more efficient way to store and load many sprites at once, as only one set of metadata is needed for all the sprites it contains, rather than one per frame per sprite. Another benefit is that it is faster since the hard-drive only needs to seek one file. Pygame’s image functions are fairly basic, and lack any form of spritesheets. Spritesheets are a much more efficient way to store and load many sprites at once, as only one set of metadata is needed for all the sprites it contains, rather than one per frame per sprite. This means that it is faster, due to the fact that the hard-drive only needs to seek one file.
Spritesheets are loaded just the same as any other image. Once instantiated however, the “get_sprite” method can be used to extract the image that is needed. This takes x and y coordinates of the top left corner of the first frame, the dimensions of the image to load, and the number of frames needed for an animation. It will automatically wrap around to the next row if it reaches the edge of the sheet, so a single animation can take up multiple rows if it has a lot of frames. The direction the frames are organised in can be changed if necessary, so if frames are stacked vertically, the animation should still work.
Another feature missing from pygame is backgrounds that can be tiled and scrolled around. Using sa_pgtraining you can now add backgrounds. These are tiled horizontally and vertically to the edges of the screen by default. You can choose whether to tile along each axis separately, and using the x_offset and y_offset attributes you can reposition them.
Text is now easier to use, with font loading being handled by the GameController class, and new methods including create and update events, and kill and refresh functions, so they are treated in a very similar way to the sprite class. This consistency makes them much easier to learn.
Just for fun, I added particle effects, these are more performant than loads of sprites scattered about. A particle system class contains a list of all particles currently active. Each particle is just a collection of values stored in a dictionary inside this list, and the particle system renders and updates these particles. In class we used particle effects to create a smoke trail behind our airplane character.
I have simplified pygame’s collision systemPixel perfect/masked collisions can be activated for the sprite class. This is optimised to first check for bounding box collisions, then when one is detected, to double check for a pixel perfect collision between the two colliding sprites.
The GameController class contains a debug attribute, that when enabled, displays some extra information on screen. This information includes an FPS display, as well as drawing the bounding boxes of all sprites currently on screen.
File paths can now be given in multiple optional ways. You can type out the full relative file path, with directories separated by double backslashes or single forward slashes inside a string, or you can create a list containing strings specifying each step in the file path.
A set of default colours have been created, which makes it easier to quickly enter colours rather than having to explain what RGB values are and how to mix colours as 3 numbers in a tuple.
Input inside of sa_pgtraining is handled by the controller’s get_input function. It calls for two arguments, the key to check, and the state that is being checked for. This state is a string matching one of the following:
“press”: is the button currently held down?
“start”: has the button just started to be pressed this frame?
“end”: has the button just been let go this frame?
This makes it easier to trigger events such as jumping, only once per button press, instead of triggering every frame the button is held.