This very last weekend of 2018, I decided to play with Python for my really first time. Honestly I don’t remember playing any game for last years, so I thought why not write my own. In this article I’ll have a look into PyGame, one of many basic “gaming” libraries for Python.
Firstly, I’m not a Python programmer. Yet, or actually, I’ll never be one. I don’t have such aspirations. Frankly, I have read my first documentation line of Python couple years ago, to see if it could fit my needs. At that moment I needed fun replacement for bash (and PHP) to write my everyday utility/support scripts. Why fun? To break the routine :).
I have lot of accretions from PHP, Python programmers will surely find annoying and unacceptable in their code. That’s fine, stick to that. This article, however, is not about code organization, formatting, best practices etc. It’s about making first steps in the gaming universe.
If you want to read more about my technical background, feel free to visit my about page. You will find out there what languages I’m programming in, and what kind of experience do I have.
If you have been programming in Python or PyGame I’m afraid you won’t find anything interesting here. This article is designed for the beginners, but it doesn’t cover Python syntax explanation. If you’re new to Python, maybe this is not place for you. Also, you won’t find any deep-explanation of how PyGame is working here.
If you want to be sure, you are not wasting your time over here, go straight away to my public repository I created for purpose of this article. If you don’t understand what’s going out there, you might be interested in reading below.
Well, for no certain reason. Really. I could use different language, but I don’t want to spend more time reading documentation. For me, this is the shortest path.
It is said, Python has its extremely easy to learn, and there are plenty of resources around the internet. Until now, I haven’t created anything worth attention in this language, but only played around with some API’s and Django.
I really like games, and I’m a bit bored with my day-to-day back-end role. Knowing basics of Python, this will be easy to make my own first steps into gaming world. I dream about creating my own game in the future, but this can’t be done without any experience.
This is experiment, to see how effective Python is, to see how well such project can be organized and, of course, see how games are done.
Hands on PyGame!
Before starting to develop my dream game, I want to gain some gaming experience. PyGame seems to be fairly well documented, easy to use and has enough functionality to build basic game.
I assume you have Python installed in your system. PyGame installation is straight forward.
$ pip3 install pygame
After successful installation, you might want to do a quick version checkup.
$ python3 -V Python 3.6.7 $ pip3 show pygame Name: pygame Version: 1.9.4 Summary: Python Game Development Home-page: https://www.pygame.org Author: Pete Shinners, Rene Dudfield, Marcus von Appen, Bob Pendleton, others... Author-email: firstname.lastname@example.org License: LGPL Location: /home/marcin/.local/lib/python3.6/site-packages Requires:
What possibly could I write?
Boring Tetris, of course!
This Russian (Soviet Union, to be exact) game from middle eighties is extremely simple. Everybody knows it, rules are clear and simple. Since no project can start without, at least basic, specification, let’s summarize its main concepts.
If you are familiar with Tetris rules, you can safely skip preparation chapter. But please, pay attention to the beginning of next chapter – you will find list of missing functionalities in my program.
Tetris board is an rectangle of size 10 columns an 20 rows. During the play seven different figures are appearing randomly from the middle top of this board. They all are made of four smaller squares. Those seven different figures are called Tetrominos and reflect to alfabet letters I, J, L, O, S, T and Z.
Every time they appear from top randomly (with optional preview of upcoming one). Each short period of time block steps down automatically (this period decreases while leveling up).
User has control of block horizontal position and block rotation. It is possible to drop block immediately to the bottom to gain more points.
Tetromino gets blocked once it can’t move further. Game ends when there is no space for next figure can’t move.
User role is to move those figures around board to put all small squares of all figures in a single row. Once that is done, rows are removed from stack and more space for next figures is created. Each row completion success in additional points.This is basically it.
Additionally, many Tetris clones have possibility to pause or reset game completely. There is also impressive Tetris Effect. Who said it has to dull and flat, right :)?
For this article I’ll be creating a prototype, really basic POC. I tend to overengineer things, that’s why you might find some pieces of code redundant. Please excuse me that, but I don’t yet know where this project will end at.
This project will only cover game board behaviour. Infinitely reappearing new blocks, block horizontal movement, rotation and dropping. As long as there is still space for new block game never ends.
There won’t be any HUD telling how many points do user have, or which block will be next. Game speed won’t increase. No music will be playing either in the background or during movement.
I’m willing to create feature rich Tetris game, but this, desirably, will be topic for next article.
I’m hardly overcoming my strong temptation to write whole logic in back-end style. Keep state inside logic structures and use PyGame to display block only. But not this time. In this project I’ll depend on PyGame structures only.
For those who never wrote a game, or never played with embedded systems, this might be awkward piece of code below. What is typical for such programs is main loop (or game loop, if you prefer). Don’t be concerned, you will use to it shortly.
Such loop is usually never-ending while, however, can be achieved in other ways. Feel free to use your preferred one.
import pygame class Game(object): def run(self) -> None: pygame.init() # Surface and optional game objects preparation. # Miscellaneous utils initialization. while running: # Handle input. # Draw sprites. # Other magic. # Cleanup, etc. pygame.quit()
Each iteration is dedicated to handle everything game can do – keystrokes, mouse clicks, printing blocks, displaying score, updating surfaces, etc. The most challenging part here is to find out how to write clear and maintainable code inside it.
For better maintenance, I created Board class, where to I extracted printing and Tetromino creation logic. My game loop reads input and passes it to Board every iteration.
while running: direction = None for event in pygame.event.get(): if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE): running = False if event.type == pygame.KEYDOWN and event.key == pygame.K_RIGHT: direction = pygame.K_RIGHT # Other directions, rotation or even pause board.update(direction)
Code above shows simplified game loop I’m using, to capture keyboard input. PyGame event module delivers useful pygame.event.get() method to read all occurring events (this also includes joystick and mouse state). For other, real-time, games you might be interested in key modules pygame.key.get_pressed() method.
Now, let’s see what’s inside board.update() method.
class Board(Surface): def update(self, direction) -> None: # Detect collisions. index = self.check_lines() if index: self.clear_line() # Move and print Tetrominos. self.tetromino_layer.update(direction) self.tetromino_layer.draw() # Add new tetromino when necessary. self.add_tetromino() return
As mentioned before, I wan’t to use PyGame structures only. That’s why I decided to use Sprites and Layers, to ease collision detection and printing every frame. Here is how I implemented independent Tetromino.
class Base(Sprite): def update(self, drop=False, pull=False, rotation=None, direction=None) -> None: if drop: return self.drop() if pull: return self.pull() if rotation: self.rotate(rotation) if direction: self.move(direction) return
The most important method of every Sprite is update method which has to be defined consciously because it’s executed indirectly. This means, update is called from Layer scope – you update layer, not sprites directly. Let’s have a look on pygame.sprite.Sprite.update() method definition.
def update(self, *args): """call the update method of every member sprite Group.update(*args): return None Calls the update method of every member sprite. All arguments that were passed to this method are passed to the Sprite update function. """ for s in self.sprites(): s.update(*args)
Each figure inherits from Base class and defines it’s color and distinct shape.
class I(Base): def __init__(self) -> None: super().__init__(color=(170, 0, 0)) def get_shape(self) -> Surface: shape = Surface((4, 1)) shape.fill(self.color) return shape
As you can see, shape is 4 pixels wide and 1 pixel height. It’s done this way, to simplify shape creation. Later on, you will find out it’s being stretched 20 times.
In my game there are three layers. Working one, containing moving Tetromino. Frozen one, containing figures that can’t move anymore and shape left from cleaning completed row. Line collision groups, used to check if line is completed.
Each printable object in PyGame is a rectangle (even ball image you import). This particularly can be tricky while detecting collisions.
Collision problems doesn’t appear with O and I figures, but others, they’re other story. This is essential to exclude empty space from collision so that they can overlay each other in empty blocks.
This can be done thanks to PyGame’s mask module pygame.mask.from_threshold() method. It basically creates collision model where specified color appears. Have look at my mask initialization
self.mask = pygame.mask.from_threshold(self.image, self.color, (1, 1, 1, 255))
After every move, collisions between moving Tetromino and frozen ones is checked. If found, figure freezes (gets transferred from working layer to frozen one) and new one is generated randomly by Board object. Randomization is done with Factory and it’s straight forward.
class TetrominoFactory(object): available = ('I', 'J', 'L', 'O', 'S', 'S', 'Z') def randomize(self) -> Base: tetromino_class = globals()[choice(self.available)] return tetromino_class()
Every board update checks if there is any line completed with assistance of Line collisions Layer. When line is found magic happens.
def clear_line(self, screen: Surface, index: int) -> None: """ Collect all frozen sprites, draw them, remove line, create new frozen sprite. """ above_line = screen.subsurface((0, 0, Config().width, index * Config.scale)).copy() screen.blit(above_line, (0, Config.scale)) self.frozen_tetrominos_layer.empty() self.frozen_tetrominos_layer.add(FrozenTetrominos(screen.copy())) return
Ironically, this is the most complicated part of my application. What’s going on here is basically cutting out one line from frozen layer. This is done by moving what’s displayed above colliding line one step down.
What’s left is transformed into one single Sprite and pushed onto Frozen layer.
What do we have here?
Well, decent prototype modestly speaking. This is working base of the game, ready for final polishing and distribution ;).
This is clearly not the simplest example of how PyGame works, but it’s easy to extend in the future.
For those who want to have look at whole code base, I invite you to visit my Github repository. Go ahead! Fork it, and have fun :).
Python and PyGame puts me into sentimental mood. Ah… Pascal (and Delphi), C, embedded systems. Nevertheless, I enjoyed writing this code (finally, it was something different that PHP).
It took me way to much time to deliver and required lot of reading (including various tutorials and documentation), but it was certainly worth it. This was priceless experience without which, I could not move towards my dream game.
PyGame has definitely way more to offer. I strongly encourage to to play with it on your own, and dig deeper into it’s documentation.
PS. I can’t pass by the fact, and proudly share it with you, that there was a 3D variant of Tetris – Blackout, made by polish programmers (California Dreams) back in 1989. Check it out!