Overview
A lot of developers get into software development because they want to build games. Not everybody can be a professional game developer, but everybody can build their own games for fun and maybe profit. In this five-part series, I’ll show you how to create 2D single-player games using Python 3 and the excellent Pygame framework.
We will build a version of the classic Breakout game. When all is said and done, you’ll have a clear understanding of what it takes to create your own game, you’ll be familiar with Pygame’s capabilities, and you’ll have a sample game.
Here are the features and capabilities we’ll implement:
- simple generic GameObject and TextObject
- simple generic Game object
- simple generic button
- config file
- handling keyboard and mouse events
- bricks, paddle, and ball
- managing paddle movement
- handling collisions of the ball with everything
- background image
- sound effects
- extensible special effects system
What you should not expect is a visually pleasing game. I’m a programmer and not an artist. I worry more about the esthetics of the code. The result of my visual design can be quite shocking. On the plus side, if you want to improve how this version of Breakout looks, you have tons of room for improvement. With that dire warning out of the way, here is a screenshot:
The full source code is available here.
Quick Introduction to Game Programming
Games are about moving pixels on the screen and making noise. Pretty much all video/computer games have most of the following elements. Out of scope of this article are client-server games and multi-player games, which involve a lot of network programming too.
Main Loop
The main loop of a game runs and refreshes the screen at fixed intervals. This is your frame rate, and it dictates how smooth things are. Typically, games refresh the screen 30 to 60 times a second. If you go slower, objects on the screen will seem jerky.
Inside the main loop, there are three main activities: handling events, updating the game state, and drawing the current state of the screen.
Handling Events
Events in a game consist of everything that happens outside the control of the game’s code but is relevant to the operation of the game. For example, if in Breakout the player presses the left arrow key, the game needs to move the paddle to the left. Typical events are key presses (and releases), mouse movement, mouse button clicks (especially in menus), and timer events (e.g. a special effect expires after 10 seconds).
Updating State
The core of each game is its state: the stuff it keeps track of and draws on the screen. In Breakout, the state includes the location of all the bricks, the position and speed of the ball, and the position of the paddle, as well as lives and the score.
There is also the auxiliary state that helps manage the game:
- Are we showing a menu now?
- Is the game over?
- Did the player win?
Drawing
The game needs to display its state on the screen. This includes drawing geometrical shapes, images, and text.
Game Physics
Most games simulate a physical environment. In Breakout, the ball bounces off objects and has a very crude rigid-body physics system in place (if you can call it that).
More advanced games may have more sophisticated and realistic physics systems (especially 3D games). Note that some games like card games don’t have much physics at all, and that’s totally fine.
AI (Artificial Intelligence)
There are many games where you play against an artificial computer opponent or opponents, or there are enemies that try to kill you or worse. These figments of the game’s imagination often behave in a seemingly intelligent way in the game’s world.
For example, enemies will chase you and be aware of your location. Breakout doesn’t present an AI. You play against the cold, hard bricks. However, the AI in games is often very simple and just follows simple (or complex) rules to achieve pseudo-intelligent outcomes.
Playing Audio
Playing audio is another important aspect of games. There are in general two types of audio: background music and sound effects. In Breakout, I focus on sound effects that play briefly when various events happen.
Background music is just music that plays constantly in the background. Some games don’t use background music, and some switch it every level.
Lives, Score, and Levels
Most games give you a certain amount of lives, and when you run out of lives, the game is over. You also often have a score that gives you a sense of how well you’re doing and a motivation to improve next time you play or just brag to your friends about your Breakout mad skills. Many games have levels that are either completely different or raise the level of difficulty.
Meet Pygame
Before diving in and starting to implement, let’s learn a little about Pygame, which will do a lot of the heavy lifting for us.
What’s Pygame?
Pygame is a Python framework for game programming. It is built on top of SDL and has all the good stuff:
-
mature
-
great community
-
open source
-
cross-platform
-
good docs
-
plenty of sample games
-
easy to learn
Installing Pygame
Type pip install pygame
in order to install it. If you need something else, follow the instructions in the Getting Started section of the Wiki. If you run macOS Sierra as I do, you may run into some trouble. I was able to install Pygame with no trouble, and the code seemed to run just fine, but the game window never showed up.
That’s kind of a bummer when you run a game. I eventually had to resort to running on Windows in a VirtualBox VM. Hopefully, by the time you read this article, the issue will have been resolved.
Game Architecture
Games need to manage a lot of information and perform similar operations on many objects. Breakout is a mini-game, yet trying to manage everything in one file would be overwhelming. Instead, I opted to create a file structure and architecture that would be suitable for much larger games.
Directory and File Structure
├── Pipfile ├── Pipfile.lock ├── README.md ├── ball.py ├── breakout.py ├── brick.py ├── button.py ├── colors.py ├── config.py ├── game.py ├── game_object.py ├── images │ └── background.jpg ├── paddle.py ├── sound_effects │ ├── brick_hit.wav │ ├── effect_done.wav │ ├── level_complete.wav │ └── paddle_hit.wav └── text_object.py
The Pipfile and Pipfile.lock are the modern way of managing dependencies in Python. The images directory contains images used by the game (only the background image in this incarnation), and the sound_effects directory contains short audio clips used as (you guessed it) sound effects.
The ball.py, paddle.py, and brick.py files contain code specific to each one of these Breakout objects. I will cover them in depth later in the series. The text_object.py file contains code for displaying text on the screen, and the background.py file contains the Breakout-specific game logic.
However, there are several modules that form a loose, general-purpose skeleton. The classes defined there can be reused for other Pygame-based games.
The GameObject Class
The GameObject represents a visual object that knows how to render itself, maintain its boundaries, and move around. Pygame actually has a Sprite class that has a similar role, but in this series I want to show how things work at a low level and not rely on too much prepackaged magic. Here is the GameObject class:
from pygame.rect import Rect class GameObject: def __init__(self, x, y, w, h, speed=(0,0)): self.bounds = Rect(x, y, w, h) self.speed = speed @property def left(self): return self.bounds.left @property def right(self): return self.bounds.right @property def top(self): return self.bounds.top @property def bottom(self): return self.bounds.bottom @property def width(self): return self.bounds.width @property def height(self): return self.bounds.height @property def center(self): return self.bounds.center @property def centerx(self): return self.bounds.centerx @property def centery(self): return self.bounds.centery def draw(self, surface): pass def move(self, dx, dy): self.bounds = self.bounds.move(dx, dy) def update(self): if self.speed == [0, 0]: return self.move(*self.speed)
The GameObject is designed to serve as a base class for other objects. It exposes directly a lot of the properties of its self.bounds rectangle, and in its update()
method it moves the object according to its current speed. It doesn’t do anything in its draw()
method, which should be overridden by sub-classes.
The Game Class
The Game class is the core of the game. It runs the main loop. It has a lot of useful functionality. Let’s take it method by method.
The __init__()
method initializes Pygame itself, the font system, and the audio mixer. The reason you need to make three different calls is because not all Pygame games use all components, so you control what subsystems you use and initialize only those with their specific parameters. It creates the background image, the main surface (where everything is drawn), and the game clock with the correct frame rate.
The self.objects member will keep all the game objects that need to be rendered and updated. The various handlers manage lists of the handler function that should be called when certain events happen.
import pygame import sys from collections import defaultdict class Game: def __init__(self, caption, width, height, back_image_filename, frame_rate): self.background_image = pygame.image.load(back_image_filename) self.frame_rate = frame_rate self.game_over = False self.objects = [] pygame.mixer.pre_init(44100, 16, 2, 4096) pygame.init() pygame.font.init() self.surface = pygame.display.set_mode((width, height)) pygame.display.set_caption(caption) self.clock = pygame.time.Clock() self.keydown_handlers = defaultdict(list) self.keyup_handlers = defaultdict(list) self.mouse_handlers = []
The update()
and draw()
methods are very simple. They just iterate over all the managed game objects and call their corresponding methods. If two game objects overlap, the order in the objects list determines which object will be rendered first, and the other will partially or fully cover it.
def update(self): for o in self.objects: o.update() def draw(self): for o in self.objects: o.draw(self.surface)
The handle_events()
method listens to events generated by Pygame, like key and mouse events. For each event, it invokes all the handler functions that are registered to handle this type of event.
def handle_events(self): for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() elif event.type == pygame.KEYDOWN: for handler in self.keydown_handlers[event.key]: handler(event.key) elif event.type == pygame.KEYUP: for handler in self.keydown_handlers[event.key]: handler(event.key) elif event.type in (pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION): for handler in self.mouse_handlers: handler(event.type, event.pos)
Finally, the run()
method runs the main loop. It runs until the game_over
member becomes True. In each iteration, it renders the background image and invokes in order the handle_events()
, update()
, and draw()
methods.
Then it updates the display, which actually updates the physical display with all the content that was rendered during this iteration. Last, but not least, it calls the clock.tick()
method to control when the next iteration will be called.
def run(self): while not self.game_over: self.surface.blit(self.background_image, (0, 0)) self.handle_events() self.update() self.draw() pygame.display.update() self.clock.tick(self.frame_rate)
Conclusion
In this part, you’ve learned the basics of game programming and all the components involved in making games. Then, we looked at Pygame itself and how to install it. Finally, we delved into the game architecture and examined the directory structure, the GameObject
class, and the Game class.
In part two, we’ll look at the TextObject
class used to render text on the screen. We’ll create the main window, including a background image, and then we’ll learn how to draw objects like the ball and the paddle.
Additionally, please see what we have available for sale and for study in the Envato Market, and don’t hesitate to ask any questions and provide your valuable feedback using the feed below.
Powered by WPeMatico