Python Game Programming by Example - Sample Chapter
Python Game Programming by Example - Sample Chapter
$ 39.99 US
25.99 UK
P U B L I S H I N G
Sa
m
pl
Joseph Howse
ee
C o m m u n i t y
D i s t i l l e d
E x p e r i e n c e
Seville, Spain.
He came across Python back in 2009, while he was studying at the University of
Seville. Alejandro developed several academic projects with Python, from web
crawlers to artificial intelligence algorithms. In his spare time, he started building
his own games in Python. He did a minor in game design at Hogeschool van
Amsterdam, where he created a small 3D game engine based on the ideas he
learned during this minor.
He has also developed some open source projects, such as a Python API for the
Philips Hue personal lighting system. You can find these projects in his GitHub
account at https://github.com/aleroddepaz.
Prior to this publication, Alejandro collaborated with Packt Publishing as a
technical reviewer on the book Tkinter GUI Application Development Hotshot.
Joseph Howse is a writer, software developer, and business owner from Halifax,
Nova Scotia, Canada. Computer games and code are imbibed in his earliest memories,
as he learned to read and type by playing text adventures with his older brother, Sam,
and watching him write graphics demos in BASIC.
Joseph's other books include OpenCV for Secret Agents, OpenCV Blueprints, Android
Application Programming with OpenCV 3, and Learning OpenCV 3 Computer Vision with
Python. He works with his cats to make computer vision systems for humans, felines,
and other users. Visit http://nummist.com to read about some of his latest projects
done at Nummist Media Corporation Limited.
Preface
Welcome to Python Game Programming By Example. As hobbyist programmers or
professional developers, we may build a wide variety of applications, from large
enterprise systems to web applications made with state-of-the-art frameworks.
However, game development has always been an appealing topic, maybe simply
for creating casual games and not just for high-budget AAA titles.
If you want to explore the different ways of developing games in Python, a language
with clear and simple syntax, then this is the book for you. In each chapter, we will
build a new game from scratch, using several popular libraries and utilities. By the
end of this book, you will be able to quickly create your own 2D and 3D games, and
have a handful of Python libraries in your tool belt to choose from.
Preface
Chapter 5, Pygame and 3D, presents the foundations of 3D and guides you through
the basic structure of an OpenGL program.
Chapter 6, PyPlatformer, is where you develop a 3D platformer game with all the
techniques learned in the previous chapter.
Chapter 7, Augmenting a Board Game with Computer Vision, introduces the topic of
computer vision, which allows software to learn about the real world via a camera.
In this chapter, you build a system to analyze a game of checkers (draughts) in real
time as players move pieces on a physical board.
Hello, Pong!
Game development is a highly evolving software development process, and it has
improved continuously since the appearance of the first video games in the 1950s.
Nowadays, there are a wide variety of platforms and engines, and this process has
been facilitated with the arrival of open source tools.
Python is a free high-level programming language with a design intended to
write readable and concise programs. Thanks to its philosophy, we can create our
own games from scratch with just a few lines of code. There are a plenty of game
frameworks for Python, but for our first game, we will see how we can develop it
without any third-party dependency.
In this chapter, we will cover the following topics:
Installing Python
You will need Python 3.4 with Tcl / Tk 8.6 installed on your computer. The
latest branch of this version is Python 3.4.3, which can be downloaded from
https://www.python.org/downloads/. Here, you can find the official binaries for
the most popular platforms, such as Windows and Mac OS. During the installation
process, make sure that you check the Tcl/Tk option to include the library.
[1]
Hello, Pong!
The code examples included in the book have been tested against Windows 8 and
Mac, but can be run on Linux without any modification. Note that some distributions
may require you to install the appropriate package for Python 3. For instance, on
Ubuntu, you need to install the python3-tk package.
Once you have Python installed, you can verify the version by opening Command
Prompt or a terminal and executing these lines:
$ python --version
Python 3.4.3
After this check, you should be able to start a simple GUI program:
$ python
>>> from tkinter import Tk
>>> root = Tk()
>>> root.title('Hello, world!')
>>> root.mainloop()
These statements create a window, change its title, and run indefinitely until the
window is closed. Do not close the new window that is displayed when the second
statement is executed. Otherwise, it will raise an error because the application has
been destroyed.
We will use this library in our first game, and the complete documentation of the
module can be found at https://docs.python.org/3/library/tkinter.html.
Tkinter and Python 2
The Tkinter module was renamed to tkinter in Python 3. If you have
Python 2 installed, simply change the import statement with Tkinter
in uppercase, and the program should run as expected.
An overview of Breakout
The Breakout game starts with a paddle and a ball at the bottom of the screen and
some rows of bricks at the top. The player must eliminate all the bricks by hitting
them with the ball, which rebounds against the borders of the screen, the bricks,
and the bottom paddle. As in Pong, the player controls the horizontal movement
of the paddle.
[2]
Chapter 1
The player starts the game with three lives, and if they miss the ball's rebound and it
reaches the bottom border of the screen, one life is lost. The game is over when all the
bricks are destroyed, or when the player loses all their lives.
This is a screenshot of the final version of our game:
[3]
Hello, Pong!
With Tkinter, this can easily be achieved using the following code:
import tkinter as tk
lives = 3
root = tk.Tk()
frame = tk.Frame(root)
canvas = tk.Canvas(frame, width=600, height=400, bg='#aaaaff')
frame.pack()
canvas.pack()
root.title('Hello, Pong!')
root.mainloop()
Through the tk alias, we access the classes defined in the tkinter module, such as
Tk, Frame, and Canvas.
Notice the first argument of each constructor call which indicates the widget (the
child container), and the required pack() calls for displaying the widgets on their
parent container. This is not necessary for the Tk instance, since it is the root window.
However, this approach is not exactly object-oriented, since we use global variables
and do not define any new classes to represent our new data structures. If the code
base grows, this can lead to poorly organized projects and highly coupled code.
We can start encapsulating the pieces of our game in this way:
import tkinter as tk
class Game(tk.Frame):
def __init__(self, master):
super(Game, self).__init__(master)
self.lives = 3
self.width = 610
self.height = 400
self.canvas = tk.Canvas(self, bg='#aaaaff',
width=self.width,
height=self.height)
[4]
Chapter 1
self.canvas.pack()
self.pack()
if __name__ == '__main__':
root = tk.Tk()
root.title('Hello, Pong!')
game = Game(root)
game.mainloop()
Our new type, called Game, inherits from the Frame Tkinter class. The class
Game(tk.Frame): definition specifies the name of the class and the superclass
between parentheses.
If you are new to object-oriented programming with Python, this syntax may
not sound familiar. In our first look at classes, the most important concepts are
the __init__ method and the self variable:
The __init__ method is a special method that is invoked when a new class
instance is created. Here, we set the object attributes, such as the width, the
height, and the canvas widget. We also call the parent class initialization with
the super(Game, self).__init__(master) statement, so the initial state of
the Frame is properly initialized.
The self variable refers to the object, and it should be the first argument of a
method if you want to access the object instance. It is not strictly a language
keyword, but the Python convention is to call it self so that other Python
programmers won't be confused about the meaning of the variable.
[5]
Hello, Pong!
See the chapter1_01.py script, which contains this code. Since no external assets are
needed, you can place it in any directory and execute it from the Python command
line by running chapter1_01.py. The main loop will run indefinitely until you click
on the close button of the window, or you kill the process from the command line.
This is the starting point of our game, so let's start diving into the Canvas widget and
see how we can draw and animate items in it.
Keeping this layout in mind, we can use two methods of the Canvas widget to draw
the paddle, the bricks, and the ball:
Each of these calls returns an integer, which identifies the item handle. This reference
will be used later to manipulate the position of the item and its options. The
**options syntax represents a key/value pair of additional arguments that can be
passed to the method call. In our case, we will use the fill and the tags option.
[6]
Chapter 1
The x0 and y0 coordinates indicate the top-left corner of the previous screenshot, and
x1 and y1 are indicated in the bottom-right corner.
For instance, we can call canvas.create_rectangle(250, 300, 330, 320,
fill='blue', tags='paddle') to create a player's paddle, where:
The fill='blue' means that the background color of the item is blue.
The tags='paddle' means that the item is tagged as a paddle. This string
will be useful later to find items in the canvas with specific tags.
We will invoke other Canvas methods to manipulate the items and retrieve widget
information. This table gives the references to the Canvas widget that will be used in
this chapter:
Method
Description
canvas.coords(item)
canvas.move(item, x, y)
canvas.delete(item)
canvas.winfo_width()
canvas.itemconfig(item,
**options)
canvas.bind(event,
callback)
canvas.unbind(event)
canvas.create_text
(*position, **opts)
canvas.find_withtag(tag)
canvas.find_overlapping
(*position)
[7]
Hello, Pong!
You can check out a complete reference of the event syntax as well as some practical
examples at http://effbot.org/tkinterbook/tkinter-events-and-bindings.
htm#events.
Assuming that we have created a Canvas widget as shown in our previous code
samples, a basic usage of this class and its attributes would be like this:
item = canvas.create_rectangle(10,10,100,80, fill='green')
game_object = GameObject(canvas,item) #create new instance
print(game_object.get_position())
# [10, 10, 100, 80]
game_object.move(20, -10)
print(game_object.get_position())
# [30, 0, 120, 70]
game_object.delete()
In this example, we created a green rectangle and a GameObject instance with the
resulting item. Then we retrieved the position of the item within the canvas, moved
it, and calculated the position again. Finally, we deleted the underlying item.
[8]
Chapter 1
The methods that the GameObject class offers will be reused in the subclasses that
we will see later, so this abstraction avoids unnecessary code duplication. Now that
you have learned how to work with this basic class, we can define separate child
classes for the ball, the paddle, and the bricks.
Therefore, by changing the sign of one of the vector components, we will change the
ball's direction by 90 degrees. This will happen when the ball bounces against the
canvas border, when it hits a brick, or the player's paddle:
class Ball(GameObject):
def __init__(self, canvas, x, y):
self.radius = 10
self.direction = [1, -1]
self.speed = 10
item = canvas.create_oval(x-self.radius, y-self.radius,
x+self.radius, y+self.radius,
fill='white')
super(Ball, self).__init__(canvas, item)
[9]
Hello, Pong!
For now, the object initialization is enough to understand the attributes that the
class has. We will cover the ball rebound logic later, when the other game objects
have been defined and placed in the game canvas.
The move method is responsible for the horizontal movement of the paddle. Step by
step, the following is the logic behind this method:
[ 10 ]
Chapter 1
If both the minimum and maximum x-axis coordinates, plus the offset
produced by the movement, are inside the boundaries of the canvas,
this is what happens:
If the paddle still has a reference to the ball (this happens when the
game has not been started), the ball is moved as well
This method will be bound to the input keys so that the player can use them to
control the paddle's movement. We will see later how we can use Tkinter to process
the input key events. For now, let's move on to the implementation of the last one of
our game's components.
[ 11 ]
Hello, Pong!
As you may have noticed, the __init__ method is very similar to the one in the
Paddle class, since it draws a rectangle and stores the width and the height of the
shape. In this case, the value of the tags option passed as a keyword argument is
'brick'. With this tag, we can check whether the game is over when the number of
remaining items with this tag is zero.
Another difference from the Paddle class is the hit method and the attributes it uses.
The class variable called COLORS is a dictionarya data structure that contains key/
value pairs with the number of hits that the brick has left, and the corresponding
color. When a brick is hit, the method execution occurs as follows:
For instance, if we call this method for a brick with two hits left, we will
decrease the counter by 1 and the new color will be #999999, which is the
value of Brick.COLORS[1]. If the same brick is hit again, the number of
remaining hits will become zero and the item will be deleted.
Chapter 1
self.add_brick(x + 37.5, 50, 2)
self.add_brick(x + 37.5, 70, 1)
self.add_brick(x + 37.5, 90, 1)
self.hud = None
self.setup_game()
self.canvas.focus_set()
self.canvas.bind('<Left>',
lambda _: self.paddle.move(-10))
self.canvas.bind('<Right>',
lambda _: self.paddle.move(10))
def setup_game(self):
self.add_ball()
self.update_lives_text()
self.text = self.draw_text(300, 200,
'Press Space to start')
self.canvas.bind('<space>',
lambda _: self.start_game())
This initialization is more complex that what we had at the beginning of the chapter.
We can divide it into two sections:
Our new add_ball and add_brick methods are used to create game objects and
perform a basic initialization. While the first one creates a new ball on top of the
player's paddle, the second one is a shorthand way of adding a Brick instance:
def add_ball(self):
if self.ball is not None:
self.ball.delete()
[ 13 ]
Hello, Pong!
paddle_coords = self.paddle.get_position()
x = (paddle_coords[0] + paddle_coords[2]) * 0.5
self.ball = Ball(self.canvas, x, 310)
self.paddle.set_ball(self.ball)
def add_brick(self, x, y, hits):
brick = Brick(self.canvas, x, y, hits)
self.items[brick.item] = brick
The draw_text method will be used to display text messages in the canvas. The
underlying item created with canvas.create_text() is returned, and it can be
used to modify the information:
def draw_text(self, x, y, text, size='40'):
font = ('Helvetica', size)
return self.canvas.create_text(x, y, text=text,
font=font)
The update_lives_text method displays the number of lives left and changes its
text if the message is already displayed. It is called when the game is initializedthis
is when the text is drawn for the first timeand it is also invoked when the player
misses a ball rebound:
def update_lives_text(self):
text = 'Lives: %s' % self.lives
if self.hud is None:
self.hud = self.draw_text(50, 20, text, 15)
else:
self.canvas.itemconfig(self.hud, text=text)
We leave start_game unimplemented for now, since it triggers the game loop, and
this logic will be added in the next section. Since Python requires a code block for
each method, we use the pass statement. This does not execute any operation,
and it can be used as a placeholder when a statement is required syntactically:
def start_game(self):
pass
[ 14 ]
Chapter 1
See the chapter1_02.py module, a script with the sample code we have so far. If
you execute this script, it will display a Tkinter window like the one shown in the
following figure. At this point, we can move the paddle horizontally, so we are
ready to start the game and hit some bricks:
Hello, Pong!
It gets the current position and the width of the canvas. It stores the values in
the coords and width local variables, respectively.
If the position collides with the left or right border of the canvas, the
horizontal component of the direction vector changes its sign
If the position collides with the upper border of the canvas, the vertical
component of the direction vector changes its sign
For instance, if the ball hits the left border, the coords[0] <= 0 condition evaluates
to true, so the x-axis component of the direction changes its sign, as shown in
this diagram:
If the ball hits the top-right corner, both coords[2] >= width and
coords[1] <= 0 evaluate to true. This changes the sign of both the
components of the direction vector, like this:
[ 16 ]
Chapter 1
The logic of the collision with a brick is a bit more complex, since the direction of the
rebound depends on the side where the collision occurs.
We will calculate the x-axis component of the ball's center and check whether it is
between the lowermost and uppermost x-axis coordinates of the colliding brick. To
translate this into a quick implementation, the following snippet shows the possible
changes in the direction vector as per the ball and brick coordinates:
coords = self.get_position()
x = (coords[0] + coords[2]) * 0.5
brick_coords = brick.get_position()
if x > brick_coords[2]:
self.direction[0] = 1
elif x < brick_coords[0]:
self.direction[0] = -1
else:
self.direction[1] *= -1
For instance, this collision causes a horizontal rebound, since the brick is being hit
from above, as shown here:
[ 17 ]
Hello, Pong!
On the other hand, a collision from the right-hand side of the brick would be
as follows:
This is valid when the ball hits the paddle or a single brick. However, the ball can
hit two bricks at the same time. In this situation, we cannot execute the previous
statements for each brick; if the y-axis direction is multiplied by -1 twice, the value in
the next iteration of the game loop will be the same.
We could check whether the collision occurred from above or behind, but the
problem with multiple bricks is that the ball may overlap the lateral of one of the
bricks and, therefore, change the x-axis direction as well. This happens because
of the ball's speed and the rate at which its position is updated.
We will simplify this by assuming that a collision with multiple bricks at the
same time occurs only from above or below. That means that it changes the y-axis
component of the direction without calculating the position of the colliding bricks:
if len(game_objects) > 1:
self.direction[1] *= -1
With these two conditions, we can define the collide method. As we will see later,
another method will be responsible for determining the list of colliding bricks,
so this method only handles the outcome of a collision with one or more bricks:
def collide(self, game_objects):
coords = self.get_position()
x = (coords[0] + coords[2]) * 0.5
if len(game_objects) > 1:
self.direction[1] *= -1
elif len(game_objects) == 1:
game_object = game_objects[0]
coords = game_object.get_position()
if x > coords[2]:
[ 18 ]
Chapter 1
self.direction[0] = 1
elif x < coords[0]:
self.direction[0] = -1
else:
self.direction[1] *= -1
for game_object in game_objects:
if isinstance(game_object, Brick):
game_object.hit()
Note that this method hits every brick instance that is colliding with the ball, so the
hit counters are decreased and the bricks are removed if they reach zero hits.
Hello, Pong!
If the number of bricks left is zero, it means that the player has won, and a
congratulations text is displayed.
Then, the player loses one life. If the number of lives left is zero, it
means that the player has lost, and the Game Over text is shown.
Otherwise, the game is reset
def check_collisions(self):
ball_coords = self.ball.get_position()
items = self.canvas.find_overlapping(*ball_coords)
objects = [self.items[x] for x in items \
if x in self.items]
self.ball.collide(objects)
The check_collisions method links the game loop with the ball collision method.
Since Ball.collide receives a list of game objects and canvas.find_overlapping
returns a list of colliding items with a given position, we use the dictionary of items
to transform each canvas item into its corresponding game object.
Remember that the items attribute of the Game class contains only those canvas items
that can collide with the ball. Therefore, we need to pass only the items contained in
this dictionary. Once we have filtered the canvas items that cannot collide with the
ball, such as the text displayed in the top-left corner, we retrieve each game object
by its key.
[ 20 ]
Chapter 1
With list comprehensions, we can create the required list in one simple statement:
objects = [self.items[x] for x in items if x in self.items]
This means that the new_list variable will be a list whose elements are the result of
applying the expr function to each elem in the list collection.
We can filter the elements to which the expression will be applied by adding an
if clause:
new_list = [expr(elem) for elem in collection if elem is not None]
In our case, the initial list is the list of colliding items, the if clause filters the items
that are not contained in the dictionary, and the expression applied to each element
retrieves the game object associated with the canvas item. The collide method is
called with this list as a parameter, and the logic for the game loop is completed.
Playing Breakout
Open the chapter1_complete.py script to see the final version of the game, and run
it by executing chapter1_complete.py, as you did with the previous code samples.
[ 21 ]
Hello, Pong!
When you press the spacebar, the game starts and the player controls the paddle
with the right and left arrow keys. Each time the player misses the ball, the lives
counter will decrease, and the game will be over if the ball rebound is missed
again and there are no lives left:
In our first game, all the classes have been defined in a single script. However, as the
number of lines of code increases, it becomes necessary to define separate scripts for
each part. In the next chapters, we will see how it is possible to organize our code
by modules.
Summary
In this chapter, we built out first game with vanilla Python. We covered the basics of
the control flow and the class syntax. We used Tkinter widgets, especially the Canvas
widget and its methods, to achieve the functionality needed to develop a game based
on collisions and simple input detection.
Our Breakout game can be customized as we want. Feel free to change the color
defaults, the speed of the ball, or the number of rows of bricks.
However, GUI libraries are very limited, and more complex frameworks are required
to achieve a wider range of capabilities. In the next chapter, we will introduce
Cocos2d, a game framework that helps us with the development of our next game.
[ 22 ]
www.PacktPub.com
Stay Connected: