Keeping it Small and Simple

2008.03.01

Pygame tutorial #9: first improvements to the game

Filed under: Pygame Tutorial — Tags: , , , , — Lorenzo E. Danielsson @ 21:35

In the last pygame tutorial we wrote a functional, but not very interesting worm game. In this tutorial and the next I will making various small improvements to the game. After that, I will finally leave the worm game and move on to other things. There will still be improvements to make, but those will be left as an exercise for you.

Bigger food

One problem with the game that we developed last time was that the game is insanely difficult unless your eyesight happens to be really good (which mine is not). Let’s start by sizing up the food a little. I will use a method called pygame.draw.rect which (surprise, suprise) draws a rectangle (and squares if width and height are equal). We modify the draw method of the Food class:

1 def draw(self):
2     pygame.draw.rect(self.surface, self.color, (self.x, self.y, 3, 3), 0)
3
4

This draws a square at the point (x, y). The width and the height are both 3 pixels. The final zero is the line width. A line width of zero draws a filled rectangle, which is what we want here.

To check if the worm has eaten the food we add a new method to Food, called check:


1 def check(self, x, y):
2     if x < self.x or x > self.x + 3:
3         return False
4     elif y < self.y or y > self.y + 3:
5         return False
6     else:
7         return True
8

This will check if the point (x, y) is a point within the food square. The method will return True if this is the case, or False if it isn’t.

Finally, in the game loop we need to change the test for if the worm has eaten the food. Instead of using the location() calls, we utilize the new check() method:

1 elif food.check(worm.x, worm.y):
2     score += 1
3     worm.eat()
4     print "Score: %d" % score
5     food = Food(screen)
6

Improving the keyboard handler

Another thing you will have noticed while playing is that it is far too easy to crash into yourself by “reversing” you direction. For example, if the worm is moving upwards, pressing down will cause the worm to crash onto itself. Let’s prevent that from happening:


 1 def event(self, event):
 2     """ Handle keyboard events. """
 3     if event.key == pygame.K_UP:
 4         if self.vy == 1: return
 5         self.vx = 0
 6         self.vy = -1
 7     elif event.key == pygame.K_DOWN:
 8         if self.vy == -1: return
 9         self.vx = 0
10         self.vy = 1
11     elif event.key == pygame.K_LEFT:
12         if self.vx == 1: return
13         self.vx = -1
14         self.vy = 0
15     elif event.key == pygame.K_RIGHT:
16         if self.vx == -1: return
17         self.vx = 1
18         self.vy = 0
19

For each key, there is one extra test here. This should not be difficult to understand.

A little optimization

Our little game could do with some optimization. The call to screen.fill() each time we run the loop slows the game down quite a bit. Also, since we are redrawing entire worm all the game should get just a little bit slower each time the worm’s length increases.

But why do we draw the entire worm? If we skip clearing the screen all the time, there are only to points we should draw: the first one and the last one. The last point of the worm should be plotted with the color of the background. We can change the draw() method in the worm class like this:


1 def draw(self):
2     #for x, y in self.body:
3     #    self.surface.set_at((x, y), self.color)
4     x, y = self.body[0]
5     self.surface.set_at((x, y), self.color)
6     x, y = self.body[-1]
7     self.surface.set_at((x, y), (0, 0, 0))
8
9

This requires us to comment out the screen.fill() call in the game loop. But we also need to be able to erase the food once it has been eaten. We add an erase() method to Food. This looks similar to the draw() method, but uses a different color:


1 def erase(self):
2     pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, 3, 3), 0)
3
4

Of course, we have to remember to call the erase() method each time the worm eats the food.

Adding sound

Let’s make the worm make a chomping sound any time it eats the food. To do this we first have to initialize the mixer and load a sample. You can search on the ‘Net for a sound file, or if you have a microphone, record your own, using software like sox. I recorded myself saying “GULP!” and saved the file as chomp.wav in the same directory as the worm game. It’s a good idea to make sure that the sound is short.


1 pygame.mixer.init()
2 chomp = pygame.mixer.Sound("chomp.wav")

Once we have done this, the variable chomp will hold a Sound object that we can play when we want to. In our case that is any time the worm eats the food.

The improved game

Here is the full code for the game as it is now:

  1 #! /usr/bin/env python
  2
  3 # A simple worm game, 2nd attempt.
  4
  5 import pygame
  6 import random
  7
  8 class Worm:
  9     def __init__(self, surface):
 10         self.surface = surface
 11         self.x = surface.get_width() / 2
 12         self.y = surface.get_height() / 2
 13         self.length = 1
 14         self.grow_to = 50
 15         self.vx = 0
 16         self.vy = -1
 17         self.body = []
 18         self.crashed = False
 19         self.color = 255, 255, 0
 20
 21     def eat(self):
 22         self.grow_to += 25
 23
 24     def event(self, event):
 25         """ Handle keyboard events. """
 26         if event.key == pygame.K_UP:
 27             if self.vy == 1: return
 28             self.vx = 0
 29             self.vy = -1
 30         elif event.key == pygame.K_DOWN:
 31             if self.vy == -1: return
 32             self.vx = 0
 33             self.vy = 1
 34         elif event.key == pygame.K_LEFT:
 35             if self.vx == 1: return
 36             self.vx = -1
 37             self.vy = 0
 38         elif event.key == pygame.K_RIGHT:
 39             if self.vx == -1: return
 40             self.vx = 1
 41             self.vy = 0
 42
 43     def move(self):
 44         """ Move the worm. """
 45         self.x += self.vx
 46         self.y += self.vy
 47
 48         if (self.x, self.y) in self.body:
 49             self.crashed = True
 50
 51         self.body.insert(0, (self.x, self.y))
 52
 53         if (self.grow_to > self.length):
 54             self.length += 1
 55
 56         if len(self.body) > self.length:
 57             self.body.pop()
 58
 59     def draw(self):
 60         #for x, y in self.body:
 61         #    self.surface.set_at((x, y), self.color)
 62         x, y = self.body[0]
 63         self.surface.set_at((x, y), self.color)
 64         x, y = self.body[-1]
 65         self.surface.set_at((x, y), (0, 0, 0))
 66
 67
 68 class Food:
 69     def __init__(self, surface):
 70         self.surface = surface
 71         self.x = random.randint(0, surface.get_width())
 72         self.y = random.randint(0, surface.get_height())
 73         self.color = 255, 255, 255
 74
 75     def draw(self):
 76         pygame.draw.rect(self.surface, self.color, (self.x, self.y, 3, 3), 0)
 77
 78     def erase(self):
 79         pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, 3, 3), 0)
 80
 81     def check(self, x, y):
 82         if x < self.x or x > self.x + 3:
 83             return False
 84         elif y < self.y or y > self.y + 3:
 85             return False
 86         else:
 87             return True
 88
 89 w = 500
 90 h = 500
 91
 92 screen = pygame.display.set_mode((w, h))
 93 clock = pygame.time.Clock()
 94
 95 pygame.mixer.init()
 96 chomp = pygame.mixer.Sound("chomp.wav")
 97
 98 score = 0
 99 worm = Worm(screen)
100 food = Food(screen)
101 running = True
102
103 while running:
104     #screen.fill((0, 0, 0))
105     worm.move()
106     worm.draw()
107     food.draw()
108
109     if worm.crashed:
110         running = False
111     elif worm.x <= 0 or worm.x >= w – 1:
112         running = False
113     elif worm.y <= 0 or worm.y >= h – 1:
114         running = False
115     elif food.check(worm.x, worm.y):
116         score += 1
117         worm.eat()
118         chomp.play()
119         print "Score: %d" % score
120         food.erase()
121         food = Food(screen)
122     
123     for event in pygame.event.get():
124         if event.type == pygame.QUIT:
125             running = False
126         elif event.type == pygame.KEYDOWN:
127             worm.event(event)
128
129     pygame.display.flip()
130     clock.tick(240)

Exercises

1. If you play the game for a while you will notice that from time to time the food gets placed in a location where you cannot reach it. This happens when the food is placed at the right side or the bottom of the game screen. Why is that? What modification do you have to make to the Food constructor to prevent this from happening?

2. Modify the keyboard handler to only use the left and right keys. Pressing right should change the mouse direction clock-wise. Left will change the direction counter-clockwise.

3. Give the game a different background than black.

4. Notice how Food.erase() can result in the worm being “cut”. Change the game so that any time the food is eaten, after the call to Food.erase() the entire worm gets redrawn. You may want to have add an optional flag argument to Worm.draw(). If the flag is true, the entire worm is redrawn. Otherwise only the first and last pixels are drawn.

5. Add a sound when the worm crashes.

6. We increased the size of the food by drawing rectangles instead of pixels. Could the same be applied to the worm? Experiment with this and see what the issues are, if any.

2008.02.27

Pygame tutorial #8: the worm game

Filed under: Pygame Tutorial — Tags: , , , — Lorenzo E. Danielsson @ 11:46

After the little detour I took with tutorial #7, we are going back to writing our little worm game. I am going to write a first, (barely) playable version here, and then we are gradually going to improve upon it.

As always, I am taking things really slow. That way I hope everybody can keep up. There are loads of other, more advanced pygame tutorials floating around for those who feel I’m moving too slowly. Many of them are really good.

Rules of the game

The idea behind the game is very simple. You control a worm that move around the screen looking for food. When the worm gets the food, you get some points and the worms grows longer. Crashing onto yourself or the borders ends the game.

I first played the worm game on a VIC-20, which means this was one of the first games I ever played.

The Worm class

We will expand a little upon the worm class the we developed earlier. I’m going to take it method by method. You will find the whole program at the end of the tutorial.

Let’s look at the constructor first. Here we just set up some information.


 1 def __init__(self, surface):
 2     self.surface = surface
 3     self.x = surface.get_width() / 2
 4     self.y = surface.get_height() / 2
 5     self.length = 1
 6     self.grow_to = 50
 7     self.vx = 0
 8     self.vy = -1
 9     self.body = []
10     self.crashed = False
11     self.color = 255, 255, 0

This constructor only takes the surface as an argument. We position to worm in the center of the screen initially (remember the x and y attributes hold the position of the worm’s head). The worm’s initial length is 1. I have added a new attribute, grow_to. As long as length is less than grow_to our worm hasn’t reached full length yet. This will allow the worm to grow, something that is important in the game.

Apart from that, most things should be straight-forward. The vx (horizontal velocity) and vy attributes control the direction of the worm. Each one of these can be -1, 0 or 1. Depending on the direction of motion. The color attribute should be obvious. The body attribute is an array holding all the positions of the worm’s body. Crashed is a flag that gets set to true if the worm bumps into itself.

The worm’s event() method is hopefully clear now. It looks like this:

 1 def event(self, event):
 2     """ Handle keyboard events. """
 3     if event.key == pygame.K_UP:
 4         self.vx = 0
 5         self.vy = -1
 6     elif event.key == pygame.K_DOWN:
 7         self.vx = 0
 8         self.vy = 1
 9     elif event.key == pygame.K_LEFT:
10         self.vx = -1
11         self.vy = 0
12     elif event.key == pygame.K_RIGHT:
13         self.vx = 1
14         self.vy = 0
15

The move() method has changed slightly. Let us look at what it does.


 1 def move(self):
 2     """ Move the worm. """
 3     self.x += self.vx
 4     self.y += self.vy
 5
 6     if (self.x, self.y) in self.body:
 7         self.crashed = True
 8
 9     self.body.insert(0, (self.x, self.y))
10
11     if (self.grow_to > self.length):
12         self.length += 1
13
14     if len(self.body) > self.length:
15         self.body.pop()
16

First we move the worm’s head to its new position. Where this is will depend on the values of vx and vy (note that exactly one of these will be non-zero). Next we check if the new position of the worm’s head exists in the worm’s body array. If it does, we have crashed onto ourselves and set the crashed flag.

The new position then gets inserted into the worm’s body array. We then check if the worm is growing. If it is, simply increase the worm’s length by 1. Finally, if we have reached full length, then we need to remove the last item of the body array. Make sure you understand how the different parts of this method work together to make the worm move and grow. See if you can figure out a few ways to improve it (it shouldn’t be too difficult once you’ve got the point of the code).

The draw() method should be familiar by now. It just plots out each one of the points in the worm’s body array.

1 def draw(self):
2     for x, y in self.body:
3         self.surface.set_at((x, y), self.color)
4
5

There is a new method that I’ve called position(). This will return the x and y-coordinate of the worm’s head. This is just there for convenience.


1 def position(self):
2     return self.x, self.y
3
4

Another new method is the eat() method. This just instructs the worm to start growing.


1 def eat(self):
2     self.grow_to += 25
3

That is how the Worm class works. That main thing that is new about it as compared to the one we did in tutorial #5 is that this worm has the ability to grow. In order to grow, this worm needs food, so let’s look at that next.

The Food class

The Food class needs a surface to draw itself onto, just like the Worm class. Apart from that it holds it x and y coordinates and color. It contains a method to draw itself and one to get its position in the form x, y. It looks as follows.


 1 class Food:
 2     def __init__(self, surface):
 3         self.surface = surface
 4         self.x = random.randint(0, surface.get_width())
 5         self.y = random.randint(0, surface.get_height())
 6         self.color = 255, 255, 255
 7
 8     def draw(self):
 9         self.surface.set_at((self.x, self.y), self.color)
10
11     def position(self):
12         return self.x, self.y
13

When a new Food is created, it places itself at a random location on the surface it gets passed.

The game

Putting our two classes to use, we come up with this:


  1 #! /usr/bin/env python
  2
  3 # A simple worm game.
  4
  5 import pygame
  6 import random
  7
  8 class Worm:
  9     def __init__(self, surface):
 10         self.surface = surface
 11         self.x = surface.get_width() / 2
 12         self.y = surface.get_height() / 2
 13         self.length = 1
 14         self.grow_to = 50
 15         self.vx = 0
 16         self.vy = -1
 17         self.body = []
 18         self.crashed = False
 19         self.color = 255, 255, 0
 20
 21     def eat(self):
 22         self.grow_to += 25
 23
 24     def event(self, event):
 25         """ Handle keyboard events. """
 26         if event.key == pygame.K_UP:
 27             self.vx = 0
 28             self.vy = -1
 29         elif event.key == pygame.K_DOWN:
 30             self.vx = 0
 31             self.vy = 1
 32         elif event.key == pygame.K_LEFT:
 33             self.vx = -1
 34             self.vy = 0
 35         elif event.key == pygame.K_RIGHT:
 36             self.vx = 1
 37             self.vy = 0
 38
 39     def move(self):
 40         """ Move the worm. """
 41         self.x += self.vx
 42         self.y += self.vy
 43
 44         if (self.x, self.y) in self.body:
 45             self.crashed = True
 46
 47         self.body.insert(0, (self.x, self.y))
 48
 49         if (self.grow_to > self.length):
 50             self.length += 1
 51
 52         if len(self.body) > self.length:
 53             self.body.pop()
 54
 55     def draw(self):
 56         for x, y in self.body:
 57             self.surface.set_at((x, y), self.color)
 58
 59     def position(self):
 60         return self.x, self.y
 61
 62 class Food:
 63     def __init__(self, surface):
 64         self.surface = surface
 65         self.x = random.randint(0, surface.get_width())
 66         self.y = random.randint(0, surface.get_height())
 67         self.color = 255, 255, 255
 68
 69     def draw(self):
 70         self.surface.set_at((self.x, self.y), self.color)
 71
 72     def position(self):
 73         return self.x, self.y
 74
 75 w = 500
 76 h = 500
 77
 78 screen = pygame.display.set_mode((w, h))
 79 clock = pygame.time.Clock()
 80
 81 score = 0
 82 worm = Worm(screen)
 83 food = Food(screen)
 84 running = True
 85
 86 while running:
 87     screen.fill((0, 0, 0))
 88     worm.move()
 89     worm.draw()
 90     food.draw()
 91
 92     if worm.crashed:
 93         running = False
 94     elif worm.x <= 0 or worm.x >= w – 1:
 95         running = False
 96     elif worm.y <= 0 or worm.y >= h – 1:
 97         running = False
 98     elif worm.position() == food.position():
 99         score += 1
100         worm.eat()
101         print "Score: %d" % score
102         food = Food(screen)
103     
104     for event in pygame.event.get():
105         if event.type == pygame.QUIT:
106             running = False
107         elif event.type == pygame.KEYDOWN:
108             worm.event(event)
109
110     pygame.display.flip()
111     clock.tick(240)

We initialize a counter called score to 0 (unless you want to cheat of course). We create a our worm and an initial food item.

In the game loop we need to make sure we draw not only the worm but the food item as well. This is done with food.draw().

In addition to checking if the worm has crashed with itself or the borders, we check if the worm has eaten the food. This is where the position() methods in the Worm and Food classes comes in handy. If the worm’s head is at the same location as the food the worm has taken the food. We then increase the score and print the new score to the terminal (which sucks, I know, but don’t worry one of our improvements will be to get the score onto the game screen). We also need to signal the worm to start growing. We do that by calling worm.eat(). Finally, since the food item has been eaten, we need to generate a new one.

The rest of the program should be familiar by now.

Running it

The one thing you will notice quickly with this game is how annoyingly difficult it is to eat the food (unless you have really good eye-sight which I don’t). This will be the first improvement we have to make to the game.

Exercises

1. Ew! There are like magic numbers, EVERYWHERE! Find each one and change it. It’s often useful to create attributes. For instance the worm is hard-coded to grow 25 pixels longer each time it eats. You could change that to an attribute called grow_by (or whatever you like). That means it can be manipulated from outside the class.

2. Play around with the initial worm size and the amount that the worm grows by when it eats. By modifying these you can affect how difficult the game is when it first starts and how much more difficult it gets over time.

3. Currently the program ends as soon as the worm crashes. Change it so that the user gets asked if they want to play again.

4. Build upon exercise 2 to keep a track of the high score.

5. Imagine you wanted to turn this into a two-player game. What would you need to change in order to make the Worm class re-usable?

6. Add the concept of lives to the game. The player should start off with three lives. Each time they crash the lose a life. When they run out, the game is over.

7. Let the worm start at a random position.

8. Suppose you wanted to implement difficulty levels. What things would you vary in order to make the game more or less difficult?

Conclusion

Now that we have a functional game, we need to make it properly playable as well. The first thing we need to do is to make the food item a little larger so that our worm stands a reasonable chance of getting it. It would be nice if the worm could make a “chomp” sound when it takes a bite as well. That will be the topic for the next tutorial.

2008.02.19

Pygame tutorial #7: more on lines

Filed under: Pygame, Pygame Tutorial — Tags: , , — Lorenzo E. Danielsson @ 23:52

This tutorial is a bit of a special. I’ve heard from several people who have had difficulty with Drawing lines – Exercise 6 in tutorial #2. So I am going to go through how to solve it here.

Understand the problem

To understand the problem we are trying to solve we need to draw a bit so get out your pen and paper. If you have square ruled paper, it might make things a bit easier for you. Draw a horizontal and a vertical line as in the picture below.

net001.png

You can of course use more squares than I have, but this should be enough to demonstrate the principle. Next, draw a line from (0, -5) to (1, 0) as I have.

net002.png

In each image, I have used blue to indicate the newest line. It is not something you need to do in your drawing. Next line goes from (0, -4) to (2, 0).

net003.png

Next line: (0, -3) to (3, 0):

net004.png

Then (0, -2) to (4, 0):

net005.png

Draw a final line from (0, -1) to (5, 0):

net006.png

Now you have the principle behind the pattern in the exercise. To get the horizontal and vertical lines you could do (0, -6) to (0, 0) and (0, 0) to (6, 0). To sum it all up:

(0, -6) to (0, 0)
(0, -5) to (1, 0)
(0, -4) to (2, 0)
(0, -3) to (3, 0)
(0, -2) to (4, 0)
(0, -1) to (5, 0)
(0, 0) to (6, 0)

Translating to python

We obviously have two variables here. One is the y-coordinate of the first point in each line that varies from -6 to 0. The other one is the x-coordinate of the second point in each pair that goes from 0 to 6. The following Python snippet can achieve what we want.


1 for x, y in zip(range(-6, 0+1), range(0, 6+1)):
2     print "(0, %d) to (%d, 0)" % (x, y)
3

(Note that in python, range(a, b) will include all numbers from a to b-1. That is why I wrote 0+1 and 6+1. You could have written 1 and 7 respectively, but I just wanted to add clarity)

If we think about it, the two variables vary together. So we could select one of them, say x, and express y in terms of x. Since x goes from 0 to 6 at the same time as y goes from -6 to 0, we could say that for each pair of points:

y = x - 6

So we could, if we so choose, write the python code as follows instead:

1 for x in range(0, 6+1):
2     print "(0, %d) to (%d, 0)" % (x-6, x)
3

Got it so far? Good. 🙂

Preparing to put this on the screen

First of all, we need to make a slight modification to what we have said so far. On the screen we need some space between each point. So instead of the x-coordinates being 0, 1, 2, 3, 4, 5 and 6, we would want them to be something like 0, 10, 20, 30, 40, 50 and 60. We will of course draw more lines as well.

But there is a more serious issue. I have been using Cartesian coordinates up to now. But screen coordinates are different. The top left hand corner of your screen is the point (0,0). As you move down the screen, y increases. So the lines we need to draw thus look as follows:

(0, 6) to (0, 0)
(0, 5) to (1, 0)
(0, 4) to (2, 0)
(0, 3) to (3, 0)
(0, 2) to (4, 0)
(0, 1) to (5, 0)
(0, 0) to (6, 0)

This means that the relationship between x and y is:

y = 6 - x

Make sure you understand this before you go on. Take as much time as you need to (hurry is a bad word in my vocabulary).

Now on to pygame

Time to use what we have just learned to put together a little program. The following program will solve the first part of the exercise.


 1 import pygame
 2
 3 screen = pygame.display.set_mode((500, 500))
 4 clock = pygame.time.Clock()
 5 running = True
 6 size = 250
 7 step = 10
 8
 9 for x in range(0, size+1, step):
10     pygame.draw.line(screen, (255, 255, 255), (0, 250-x), (x, 0))
11
12 pygame.display.flip()
13
14 while running:
15     for event in pygame.event.get():
16         if event.type == pygame.QUIT:
17             running = False
18
19     clock.tick()
20

Drawing in all corners

The second part of the exercise was to draw this pattern in all four corners of the screen. I suggest you go back to use the pen and paper and draw each figure so you get a good idea of how the coordinates vary in each case.

Next you need to remember to convert all the lines into screen coordinates. You will need to remember that “nets” on the right side need their x-coordinates to be in relation to the right-most screen coordinate, which is the width of the screen – 1 (if the width of the screen is 501 pixels, the coordinates go from 0 to 500).

The same applies to the “nets” at the bottom of the screen. The y-coordinates need to be expressed in relation to the height of the screen instead.

I am going to let you work out the math for yourself. It is not too difficult if you take your time and solve it one step at a time. Just follow the principle I have outlined already. I will give you the solution in code. If you are having difficulties, you can look at the code and “go backwards” to get to the math.


 1 import pygame
 2
 3 w = h = 500
 4 screen = pygame.display.set_mode((w+1, h+1))
 5 clock = pygame.time.Clock()
 6 running = True
 7 size = 250
 8 step = 10
 9 color = 255, 255, 255
10
11 for x in range(0, size+1, step):
12     pygame.draw.line(screen, color, (0, size-x), (x, 0))
13     pygame.draw.line(screen, color, (w – (size-x), 0), (w, x))
14     pygame.draw.line(screen, color, (w, h – (size-x)), (w-x, h))
15     pygame.draw.line(screen, color, (250-x, h), (0, h-x))
16
17 pygame.display.flip()
18
19 while running:
20     for event in pygame.event.get():
21         if event.type == pygame.QUIT:
22             running = False
23
24     clock.tick()
25

Note that the screen width is actually w+1 and the height is h+1. This is acceptable in my code because I make the rules. 😉 On a serious note, it makes the draw_line statements a little simpler.

You can (and should) experiment with size, step and color. Don’t worry if something ends up looking differently than you expected. If that happens, just take the time to understand why the program produces the results it does. This can be done with pen paper and a few debug statements (you could get the program to print out the coordinates of each line).

Animating it

Okay, let’s do something that is a little more interesting.


 1 #! /usr/bin/env python
 2
 3 import pygame
 4
 5 w = h = 500
 6 size = 250
 7 step = 10
 8 lines = []
 9 pos = 0
10 maxlines = 40
11
12 for x in range(0, size+1, step):
13     lines.append((0, size-x, x, 0))
14
15 for x in range(0, size+1, step):
16     lines.append((w – (size-x), 0, w, x))
17
18 for x in range(0, size+1, step):
19     lines.append((w, h – (size-x), w-x, h))
20
21 for x in range(0, size+1, step):
22     lines.append((size-x, h, 0, h-x))
23
24 screen = pygame.display.set_mode((w+1, h+1))
25 clock = pygame.time.Clock()
26 running = True
27
28 while running:
29     for event in pygame.event.get():
30         if event.type == pygame.QUIT:
31             running = False
32
33     screen.fill((0, 0, 0))
34     col = 0
35     cur = pos
36
37     for i in range(maxlines):
38         x1, y1, x2, y2 = lines[cur]
39         pygame.draw.line(screen, (col, col, col), (x1, y1), (x2, y2))
40
41         cur += 1
42         if cur >= len(lines): cur = 0
43         col += 240 / maxlines
44         
45     pos += 1
46     if pos >= len(lines): pos = 0
47
48     pygame.display.flip()
49     clock.tick(40)

I am not going to explain how it works here. Rather the following exercises are all related to this code. Solve the exercises as a guide to help you build up an understanding of how the program works.

Remember that as a programmer, one of the skills you will need to have is the ability to read code and make sense of it. That is the purpose of this. Just take it all one step at a time and you will be okay (I promise). If you are not sure of what a particular line does, comment it out or modify it in some way and see how it affects the result. That is an excellent and fun way to learn more.

Once you have been able to understand how the program works, see how you can improve upon it. There are several things you could do. You could look at optimizing it, making it more readable, cut out unnecessary variables, etc. Feel free to post your code.

Exercises

1. What date does the lines array hold?

2. What is the purpose of the pos variable?

3. What is the purpose of the maxlines variable? What happens when you change maxlines to 10? Or to 60? Try with different values for maxlines and see how it affects the program.

4. Why is it that I use four for loops to populate the lines array? What would happen if you used a single for loop containing all four appends? Is there any way you could re-write this part to use a single for loop without the lines ending up in the wrong order?

5. Why have I created the cur variable. It seems to always be initialized to the value of pos. Would it be possible to do away with cur and only use pos?

6. What on earth goes on in the loop that starts with for i in range(maxlines)?

7. What is it that causes the lines to “fade”?

8. Play around with the expression that changes the color value (col += 240 / maxlines)

9. Modify the program to draw red, green, blue or yellow lines. Better yet, make it use any color you prefer. Make sure that the lines still fade properly.

10. What is the purpose of this code?

pos += 1
if pos >= len(lines): pos = 0

11. Change the “tick” value. How does it affect the speed of the animation when you increase or decrease the value?

12. Can you reverse the direction of the animation?

Conclusion

Okay, that’s it for this time. Hope you enjoyed it. Don’t forget that curiosity is a good thing. Try things out. Modify the code. Any time you are uncertain about what something does, play around with it. You won’t damage your computer or your OS by messing around with the code.

Also, as the beginning of this tutorial was meant to show, there are times when pen and paper are a programmer’s best friend (sorry, Matz).

Most importantly, don’t give up! If I’m able to work things like this out, so can you.

2007.12.18

Pygame tutorial #6: from pixel to worm

Filed under: Pygame Tutorial, Python Programming — Tags: , , , — Lorenzo E. Danielsson @ 18:03

In the previous tutorial we learned how to plot individual pixels. We wrote a simple program where you move a pixel around the screen. Today we are going to turn our single pixel into a worm.

Two classic games that could easily be built from what we will look at today are “the worm game” and Tron. In this tutorial we will learn how to create the worm itself, and in the next tutorial we will turn it all into a little game that we can actually play.

A first try

Our worm will move across the screen. We can control it with the cursor keys. There are two things are worm must avoid at all costs: the screen borders and itself.

The following code implements just that.


 1 #! /usr/bin/env python
 2
 3 # Move a worm across the screen. Beware of borders and self!
 4
 5 import pygame
 6
 7 class Worm:
 8     """ A worm. """
 9
10     def __init__(self, surface, x, y, length):
11         self.surface = surface
12         self.x = x
13         self.y = y
14         self.length = length
15         self.dir_x = 0
16         self.dir_y = -1
17         self.body = []
18         self.crashed = False
19
20     def key_event(self, event):
21         """ Handle key events that affect the worm. """
22         if event.key == pygame.K_UP:
23             self.dir_x = 0
24             self.dir_y = -1
25         elif event.key == pygame.K_DOWN:
26             self.dir_x = 0
27             self.dir_y = 1
28         elif event.key == pygame.K_LEFT:
29             self.dir_x = -1
30             self.dir_y = 0
31         elif event.key == pygame.K_RIGHT:
32             self.dir_x = 1
33             self.dir_y = 0
34
35     def move(self):
36         """ Move the worm. """
37         self.x += self.dir_x
38         self.y += self.dir_y
39
40         if (self.x, self.y) in self.body:
41             self.crashed = True
42
43         self.body.insert(0, (self.x, self.y))
44
45         if len(self.body) > self.length:
46             self.body.pop()
47         
48     def draw(self):
49         for x, y in self.body:
50             self.surface.set_at((x, y), (255, 255, 255))
51
52 # Dimensions.
53 width = 640
54 height = 400
55
56 screen = pygame.display.set_mode((width, height))
57 clock = pygame.time.Clock()
58 running = True
59
60 # Our worm.
61 w = Worm(screen, width/2, height/2, 200)
62
63 while running:
64     screen.fill((0, 0, 0))
65     w.move()
66     w.draw()
67
68     if w.crashed or w.x <= 0 or w.x >= width-1 or w.y <= 0 or w.y >= height-1:
69         print "Crash!"
70         running = False
71
72     for event in pygame.event.get():
73         if event.type == pygame.QUIT:
74             running = False
75         elif event.type == pygame.KEYDOWN:
76             w.key_event(event)
77
78     pygame.display.flip()
79     clock.tick(240)

The first thing we do a define what a worm is. Notice how the functionality of the worm is contained within the class. The worm knows how to move itself, draw itself, and it handles the events that relate to it. (Note: if you have done Java programming before, you may wonder where your accessors and mutators are. Don’t worry about them. You don’t need them.)

The body of the worm is stored in a list. Each time we move one step, we insert the new position of the body into the beginning of the list and pop off the last list item. That keeps our worm constant in length. Actually, that is not entirely true. Our worm starts off being only a single pixel long. We have a bit of extra code that allows the worm to grow to its full length. A little bit of code analysis should make that clear.

The move() method also contains a check to see if the worm has crashed onto itself. What we do is see if the position of the worm’s head (self.x, self.y) happens to be at the same place as any part of the worm’s body, by comparing (self.x, self.y) with each position in the body list.

The draw() method simply plots all the points held in the body list. The main program should be fairly straight-forward by now. Notice that in addition to checking if the worm has crashed onto any of the borders, the worm object is also asked if it has crashed with itself.

Exercises

  1. Let the worm grow over time. You could for instance have a counter that decreases every frame, or every time the worm turns or whatever. When the counter reaches zero, you increase the length of the worm.
  2. Create a program that displays two worms. What changes do you need to make to the Worm class in order for this to work?
  3. Modify the worm to have a gradient effect. The head of the mouse should be white. Each body segment should gradually become darker and darker shades of gray.

Collision detection

In our first example, we checked if the worm had collided with itself by comparing the position of the worm’s head with all the positions of the body. That worked fine, but there are instances where it’s not practical to keep a lot of positions in a list. An example would be the Tron game that I referred to earlier. The more you move, the longer the list would be, and the longer it would take to determine whether or the object has collided with its own body.

We will try a slightly different approach, by using Surface.get_at, which tells us the color of the pixel at the given position. It actually returns (red, green, blue, alpha), but we won’t worry about the alpha part just yet.

The new code is below:


 1 #! /usr/bin/env python
 2
 3 # Move a worm across the screen. Beware of borders and self!
 4
 5 import pygame
 6
 7 class Worm:
 8     def __init__(self, surface, x, y, length):
 9         self.surface = surface
10         self.x = x
11         self.y = y
12         self.length = length
13         self.dir_x = 0
14         self.dir_y = -1
15         self.body = []
16         self.crashed = False
17
18     def key_event(self, event):
19         """ Handle keyboard events that affect the worm. """
20         if event.key == pygame.K_UP:
21             self.dir_x = 0
22             self.dir_y = -1
23         elif event.key == pygame.K_DOWN:
24             self.dir_x = 0
25             self.dir_y = 1
26         elif event.key == pygame.K_LEFT:
27             self.dir_x = -1
28             self.dir_y = 0
29         elif event.key == pygame.K_RIGHT:
30             self.dir_x = 1
31             self.dir_y = 0
32
33     def move(self):
34         """ Move the mouse. """
35         self.x += self.dir_x
36         self.y += self.dir_y
37
38         r, g, b, a = self.surface.get_at((self.x, self.y))
39         if (r, g, b) != (0, 0, 0):
40             self.crashed = True
41
42         self.body.insert(0, (self.x, self.y))
43
44         if len(self.body) > self.length:
45             self.body.pop()
46
47     def draw(self):
48         """ Draw the worm. """
49         for x, y in self.body:
50             self.surface.set_at((x, y), (255, 255, 255))
51
52 # Window dimensions.
53 width = 640
54 height = 400
55
56 screen = pygame.display.set_mode((width, height))
57 clock = pygame.time.Clock()
58 running = True
59
60 # Our worm.
61 w = Worm(screen, width/2, height/2, 200)
62
63 while running:
64     screen.fill((0, 0, 0))
65     w.draw()
66     w.move()
67
68     if w.crashed or w.x <= 0 or w.x >= width-1 or w.y <= 0 or w.y >= height-1:
69         print "Crash!!!"
70         running = False
71
72     for event in pygame.event.get():
73         if event.type == pygame.QUIT:
74             running = False
75         elif event.type == pygame.KEYDOWN:
76             w.key_event(event)
77
78     pygame.display.flip()
79     clock.tick(240)
80

As you can see, the new version is similar to the previous one for most part. Where it does differ is in the part where we detect whether or not the worm has collided with itself. We no longer need to compare the position of the head with each position of the body. Instead we simply compare the color of the pixel where the head is with the color of the background. If they don’t match then we have a collision, since the worm is the only object on the screen.

There is one other difference between the programs. In the event loop, look at the order in which the w.draw() and w.move() are in each program. Notice the difference? In order for the second program to be able to work at all, it is vital that we draw that worm before we move it. Think about it for a while and you will understand why.

Exercises

  1. Change the program to compare the pixel color at the worm’s head with the worm’s body color instead of the background color. If the color of the pixel is the same as that of the body it is a crash.
  2. Add obstacles around the screen. These should be randomly positioned pixels. Use a different color than the worm’s color. Add collision detection to determine if the worm has crashed onto one of the obstacles. If so, the program should exit.
  3. Give the worm food to eat. Food should be a pixel randomly positioned on the screen. Make it a different color from both obstacles and the worm itself. Any time the worm gets the food, the message “yummy” should be printed to the console and a new food should be generated. Keep a counter that increases every time the worm grabs a food item.

Optimizing

As a game developer, one thing you will have to get used to is spending hours with your code looking for places to optimize. What I’m going to do here is very trivial, but then again, we are working with a very trivial example.

One thing that we do that is really wasteful is “clear” the surface each frame. This wouldn’t be necessary if we were to instead plot a single pixel, the color of the background, at the very tail of the worm. The following program does that:


 1 #! /usr/bin/env python
 2
 3 # Move a worm around the screen. Beware of borders and self!
 4
 5 import pygame
 6
 7 class Worm:
 8     """ A worm. """
 9
10     def __init__(self, surface, x, y, length):
11         self.surface = surface
12         self.x = x
13         self.y = y
14         self.dx = 0
15         self.dy = -1
16         self.length = length
17         self.body = []
18         self.last = (0, 0)
19         self.crashed = False
20
21     def key_event(self, event):
22         """ Handle keyboard events that affect the worm. """
23         if event.key == pygame.K_UP:
24             self.dx = 0
25             self.dy = -1
26         elif event.key == pygame.K_DOWN:
27             self.dx = 0
28             self.dy = 1
29         elif event.key == pygame.K_LEFT:
30             self.dx = -1
31             self.dy = 0
32         elif event.key == pygame.K_RIGHT:
33             self.dx = 1
34             self.dy = 0
35
36     def move(self):
37         """ Move the mouse. """
38         self.body.insert(0, (self.x, self.y))
39         self.x += self.dx
40         self.y += self.dy
41
42         r, g, b, a = self.surface.get_at((self.x, self.y))
43
44         if (r, g, b) != (0, 0, 0):
45             self.crashed = True
46
47         if len(self.body) >= self.length:
48             self.last = self.body.pop()
49         else:
50             self.last = self.body[-1]
51
52     def draw(self):
53         """ Draw the worm """
54         self.surface.set_at((self.x, self.y), (255, 255, 255))
55         self.surface.set_at(self.last, (0, 0, 0))
56
57 # Window dimensions.
58 width = 640
59 height = 400
60
61 screen = pygame.display.set_mode((width, height))
62 screen.fill((0, 0, 0))
63 pygame.display.flip()
64
65 clock = pygame.time.Clock()
66 running = True
67
68 # Our worm.
69 w = Worm(screen, width/2, height/2, 200)
70
71 while running:
72     w.draw()
73     w.move()
74
75     if w.crashed or w.x <= 0 or w.x >= width-1 or w.y <= 0 or w.y >= height-1:
76         print "Crashed!"
77         running = False
78
79     for event in pygame.event.get():
80         if event.type == pygame.QUIT:
81             running = False
82         elif event.type == pygame.KEYDOWN:
83             w.key_event(event)
84
85     pygame.display.flip()
86     clock.tick(240)
87

Study the move() and draw() methods and compare them to the ones in the previous version. Also notice that there is no longer any screen.fill() call inside the event loop.

I have rearranged some of the code in the move() method. Is this necessary? Figure it out. You can always try rearranging lines to see what happens. You will learn much by experimenting with the code.

Exercises

  1. Analyze the last version of the worm program. How does it differ from the previous one? How does it work? In what ways can this said to be “optimized” as compared to the previous versions?
  2. Extend the worm program that draws food for the worm to eat in such a way that each time the worm eats food it grows a little longer.
  3. Extend the program in the previous exercise so that there is “poison mushroom” as well as food on the screen. If the worm eats one of these, it reduces in length. If the length goes below a certain minimum, the program should end.

Conclusion

That brings to end another pygame tutorial. I hope you enjoyed it. I know that I am moving way too slow for some people. There are loads of other good tutorials out there if you are in a hurry to learn. Many (all?) of them are far superior to my own.

Please do try and solve at least some of the exercises. If you want to send me your solutions or questions, you can find my email if you look around a bit. 😉

2007.12.16

Pygame tutorial #5: pixels

Filed under: Pygame Tutorial — Tags: , , — Lorenzo E. Danielsson @ 21:45

Welcome to pygame tutorial #5. This time we’ll look at how to plot individual pixels. We will also look at a keyboard event that may prove useful when we get to the stage of actually being able to write games.

Setting a pixel

You can use Surface.set_at() to plot a single pixel on your surface. You will need to provide the pixel coordinates as well as the color you want to use. An example will (hopefully) make this clear.


 1 #! /usr/bin/env python
 2
 3 # Plot random pixels on the screen.
 4
 5 import pygame
 6 import random
 7
 8 # Window dimensions
 9 width = 640
10 height = 400
11
12 screen = pygame.display.set_mode((width, height))
13 clock = pygame.time.Clock()
14 running = True
15
16 while running:
17     x = random.randint(0, width-1)
18     y = random.randint(0, height-1)
19     red = random.randint(0, 255)
20     green = random.randint(0, 255)
21     blue = random.randint(0, 255)
22
23     screen.set_at((x, y), (red, green, blue))
24
25     for event in pygame.event.get():
26         if event.type == pygame.QUIT:
27             running = False
28
29     pygame.display.flip()
30     clock.tick(240)
31

This program just plots random pixels until the window is closed. It should be fairly straight-forward.

Exercises

  1. Modify the program above to take the window dimensions as command line arguments.
  2. Modify the program to start with a different background color.
  3. Write a program that contains a list of 16 preset colors. You can choose any colors you want, but red, green, blue, yellow, brown and purple are a few good ones. Create a program that plots pixels and random coordinates and uses random colors out of the preset list of colors only.
  4. Write a program that plots a pixel and the mouse position when the user presses the left mouse button.

Keydown events

You have learned how to deal with the QUIT event as well as MOUSEMOVE, MOUSEBUTTONDOWN and MOUSEBUTTONUP. This time let’s look at the KEYDOWN event. This even occurs when the user presses a key down.

When we recieve a KEYDOWN event, the event object will hold the code of the key that was pressed in an attribute called ‘key’. So by comparing event.key to whichever key we are interested in, we can find out if that was the actual key that was pressed. As usual, since I’m not good at explaining things, I hope the code below will make it clear.


 1 #! /usr/bin/env python
 2
 3 # Move a single pixel around the screen without crashing against the borders.
 4
 5 import pygame
 6
 7 # Window dimensions.
 8 width = 640
 9 height = 400
10
11 # Position of the pixel.
12 x = width / 2
13 y = height / 2
14
15 # Direction of the pixel.
16 dir_x = 0
17 dir_y = -1
18
19 screen = pygame.display.set_mode((width, height))
20 clock = pygame.time.Clock()
21 running = True
22
23 while running:
24     x += dir_x
25     y += dir_y
26
27     if x <= 0 or x >= width or y <= 0 or y >= height:
28         print "Crash!"
29         running = False
30
31     screen.fill((0, 0, 0))
32     screen.set_at((x, y), (255, 255, 255))
33     
34     for event in pygame.event.get():
35         if event.type == pygame.QUIT:
36             running = False
37         elif event.type == pygame.KEYDOWN:
38             if event.key == pygame.K_UP:
39                 dir_x = 0
40                 dir_y = -1
41             elif event.key == pygame.K_DOWN:
42                 dir_x = 0
43                 dir_y = 1
44             elif event.key == pygame.K_LEFT:
45                 dir_x = -1
46                 dir_y = 0
47             elif event.key == pygame.K_RIGHT:
48                 dir_x = 1
49                 dir_y = 0
50
51     pygame.display.flip()
52     clock.tick(120)

This program moves a pixel along the screen. Using the cursor keys you can change the direction of the pixel. If the pixel hits any of the borders the program terminates. Notice that keep a track not only of the x and y coordinates, but also the horizontal and vertical direction. When you need to keep track of several attributes of a single “thing” like this, that thing is definitely a candidate for being turned into a class.

In order to get used to moving “things” out into being classes, let’s take a look at an example of this. The following example looks and behaves just like the previous one, but there is a difference. We now create a class called MovingPixel. This class is responsible for keeping its own internal state (it’s current position, horizontal and vertical direction, etc). We could have added many more attributes to the class, but for the time being we keep things as simple as possible.

The class also contains a few methods in order for the main program to communicate to the class to perform certain actions, such as change direction, draw itself onto the specified surface and so on.

All in all, our program has grown slightly longer. But notice that the main program has grown a little less complex because we moved some things that are internal to the pixel into the MovingPixel class.

This example is obviouly very simplistic and I don’t necessarily recommend that you create classes to represent pixels (although to be fair, ours is a moving pixel). But the principle can be used on more complex objects that maybe require many drawing operations to draw. We will see more on these in future tutorials.


 1 #! /usr/bin/env python
 2
 3 # Move a single pixel around the screen without crashing against the borders.
 4
 5 import pygame
 6
 7 # These are used for directions.
 8 UP = (0, -1)
 9 DOWN = (0, 1)
10 LEFT = (-1, 0)
11 RIGHT = (1, 0)
12
13 class MovingPixel:
14     """ A moving pixel class. """
15
16     def __init__(self, x, y):
17         """ Creates a moving pixel. """
18         self.x = x
19         self.y = y
20         self.hdir = 0
21         self.vdir = -1
22
23     def direction(self, dir):
24         """ Changes the pixels direction. """
25         self.hdir, self.vdir = dir
26
27     def move(self):
28         """ Moves the pixel. """
29         self.x += self.hdir
30         self.y += self.vdir
31
32     def draw(self, surface):
33         surface.set_at((self.x, self.y), (255, 255, 255))
34
35 # Window dimensions.
36 width = 640
37 height = 400
38
39 screen = pygame.display.set_mode((width, height))
40 clock = pygame.time.Clock()
41 running = True
42
43 # Create a moving pixel.
44 pix = MovingPixel(width/2, height/2)
45
46 while running:
47     pix.move()
48
49     if pix.x <= 0 or pix.x >= width or pix.y <= 0 or pix.y >= height:
50         print "Crash!"
51         running = False
52
53     screen.fill((0, 0, 0))
54     pix.draw(screen)
55     
56     for event in pygame.event.get():
57         if event.type == pygame.QUIT:
58             running = False
59         elif event.type == pygame.KEYDOWN:
60             if event.key == pygame.K_UP:
61                 pix.direction(UP)
62             elif event.key == pygame.K_DOWN:
63                 pix.direction(DOWN)
64             elif event.key == pygame.K_LEFT:
65                 pix.direction(LEFT)
66             elif event.key == pygame.K_RIGHT:
67                 pix.direction(RIGHT)
68
69     pygame.display.flip()
70     clock.tick(120)

Exercises

  1. Modify the above program so that the pixel can move diagonally as well. You could use q (K_q) for up/left, e (K_e) for up/right, y (K_y) for down/left and c (K_c) for down/right.
  2. See if you can figure out how to make the pixel leave a trail.
  3. See if you can figure out how to turn the pixel into a worm. Choose a length for the worm and make sure the body always follows the head of the worm (as it does in all those worm games.

Conclusion

We have reached the end of another tutorial. This one is maybe a little shorter than some of the previous ones. I think its better to keep each tutorial short and instead try to write them more frequently. What do you think? Please let me know.

We can have a whole lot of fun with pixels. In the next tutorial we will continue with pixels a bit more but also combine them with a few other drawing methods. We will look at the simplest form of collision detection which allows you to determine if two objects are in contact with each other. That could represent a crash, or the fact that the player has been “caught” or whatever.

2007.12.09

Pygame tutorial #4: more on events

Filed under: Pygame Tutorial — Lorenzo E. Danielsson @ 20:27

I have received loads of requests for this, but $LIFE took a serious down-turn for a while. Oh well, let’s not waste time with that. On with the show!

The trouble with poll

So far, we have been using pygame.event.poll() to get event information. I have already hinted that there are alternatives. But before we do that let us try to see what the issue at hand is.

To begin with run any of the pygame programs that we have written as a part of this tutorial series so far, apart from the very first example from tutorial #1, which didn’t even have an event loop. Before you start the program start up some utility that lets you see the CPU usage. I personally prefer top, but you could use one of those Gnome or KDE thingies as well. If you are under Windows, the Task Manager should do the job.

Notice how sharply the CPU usage shoots up when your program starts. A part of that is inevitable, but you should be able to write pygame applications that behave far better than that. On my system, even the simplest program with just an empty window averages over 80% CPU usage.

Why is that? Well, we can try to add a small print statement to one of our earliest examples and see what results we get.


 1 #! /usr/bin/env python
 2
 3 import pygame
 4  
 5 screen = pygame.display.set_mode((640, 400))
 6 running = 1
 7  
 8 while running:
 9     event = pygame.event.poll()
10     if event.type == pygame.QUIT:
11         running = 0
12     else:
13         print event.type

Now run this and study the output. (Warning: depending on your hardware this can really slow down your machine.) As you can see, the program spits out loads of zeros to the terminal. If you place the mouse over the window and wiggle it a bit, you will see some other number pass by once in a while, but still most of the numbers being printed are zeros.

This is because the poll() method returns an event of type NOEVENT if there is no event in the event queue. This is rather wasteful. We need a better way of writing our event loop. We will try out pygame.event.get() instead. Below is the modified code:


 1 #! /usr/bin/env python
 2
 3 import pygame
 4
 5 screen = pygame.display.set_mode((640, 480))
 6 running = 1
 7
 8 while running:
 9     for event in pygame.event.get():
10         if event.type == pygame.QUIT:
11             running = 0
12         else:
13             print event.type
14

Notice how we now have a little for loop instead of just the call to poll(). This loop will go through all the acive events on the queue. There is a big difference. Run the program again and notice the output. No zeros! Which means, pygame.event.get() is not generating any NOEVENTs. The only time you will see numbers appearing on the screen is when you move and/or click the mouse over the window. The numbers you see correspond to the event types.

That takes care of part of the problem. However, if you go back to studying your CPU usage you will see that it is probably still high. It may be slightly lower, however. But we can still clearly do something about our CPU usage.

What is happening is that our program is so busy running through the event loop that it puts a lot of demand on the CPU. Imagine if we could get the program to take a pause once in a while, so that the CPU gets a chance to breathe out (and the operating system a chance to deal with other issues). That is exactly what we are going to do with a pygame.time.Clock().

For the time being it may be helpful to understand this as that we have a clock that we use to regularly tell our pygame program to “take a rest”. It will then signal to the rest of the system that, “hey, I’m idle at the moment, so do whatever it is you have to do”.

Creating the Clock() is only the first part, we also need to let it work. This is done inside the event loop. Simply call the tick() method on the Clock object. You can optionally pass in a number that I won’t go into too much because it will spoil one of the exercises. 😉

The final version of our program then looks as follows:


 1 #! /usr/bin/env python
 2
 3 import pygame
 4
 5 screen = pygame.display.set_mode((640, 480))
 6 clock = pygame.time.Clock()
 7 running = 1
 8
 9
10 while running:
11     for event in pygame.event.get():
12         if event.type == pygame.QUIT:
13             running = 0
14
15     clock.tick(20)

Notice that I have used clock.tick(20). In this case there is nothing happening on the screen, so the value I place there is rather irrelevant. But when you add “things” to your screen you will notice that the value you use will make a difference. Without going into too much detail, there is a trade-off between getting your program to run as fast as possible, and your resource usage spiking to the point where the computer becomes very slow.

What I have taught you here obviously means that you have to “unlearn” some of the things I taught you from the beginning. Sorry about that. But then again, it won’t be the last time.

I personally don’t like tutorials that say “do like this, don’t do like that”. I prefer to see things for myself. I hope that I have managed to get you to understand why you should write your programs in a certain way. My long-term goal is not just to let you be able to put together pygame programs, but to know exactly what you are doing as well.

That’s it for now. But this time you won’t have to wait as long for the next tutorial, I promise.

Exercises

Just a single exercise this time, lucky you!

1. Rewrite every single application you have written so far as a part of the tutorial series, both the examples I have given as well as the exercise you have solved (because you have been doing the exercises, haven’t you?). In each case, re-write it to use pygame.event.get() instead of pygame.event.poll(). Also include a Clock().

For each program, compare CPU usage and responsiveness between the original version and your new version. Also, experiment with different values in the clock.tick() method call. Try a really large number. Then try a really small number. In each case, look at CPU usage. See how fast or slow the program runs. Based on what you see, what can you say about x in clock.tick(x)? How does a low or a high value of x affect responsiveness and speed of the application as well as CPU usage? Finally try to find a value that keeps your pygame application running as smoothly as possible but without making the rest of your computer too slow.

2007.05.30

Pygame tutorial #3: mouse events

Filed under: Graphics Programming, Pygame Tutorial, Python Programming — Lorenzo E. Danielsson @ 10:17

In the last tutorial, you learned how to draw lines. This time we will deal with mouse events. As usual we will keep it simple.

Already in the first tutorial you learned about the QUIT event. Now let’s add a little bit of code to track the position of the mouse on our window. This is done by the MOUSEMOTION event. The code looks as follows:

 1 #! /usr/bin/env python
 2 
 3 import pygame
 4 
 5 x = y = 0
 6 running = 1
 7 screen = pygame.display.set_mode((640, 400))
 8 
 9 while running:
10     event = pygame.event.poll()
11     if event.type == pygame.QUIT:
12         running = 0
13     elif event.type == pygame.MOUSEMOTION:
14         print "mouse at (%d, %d)" % event.pos
15 
16     screen.fill((0, 0, 0))
17     pygame.display.flip()

Most of the code should look familiar by now. What is new is the check to see if we have an event of the type MOUSEMOTION (line 13). Also notice that if the event is a mouse motion event we can get a little bit more information out of the Event object. In the next line (14), we use call event.pos to get the current coordinates of the mouse pointer. This method returns a pair of values representing the x-position and y-position of the mouse pointer.

Note that the values returned by event.pos are relative to the top left-hand corner of the window, not the entire screen (unless, of course, the window covers the entire screen). You probably already know that the top-left hand corner is (0, 0) in screen coordinates.

Exercises

  1. Write a program that prints the position of the mouse pointer using Cartesian coordinates. To begin with assume that the x-axis is at half the height of the window and the the y-axis is located at half the width of the window. You already know how to draw lines, so you might as well draw the axes. Take care with the difference in direction along the y-axis between screen coordinates and Cartesian coordinates.
  2. Replace MOUSEMOTION in the example above with MOUSEBUTTONDOWN. Run the program. Move the mouse over the window and press any mouse button. What happens?
  3. Imagine that your window consists of tiles, each one 32×32 pixels. Write a program that detects the current mouse position, translates the screen coordinates into tile coordinates and prints this.
  4. Write a program that calculates the distance of the mouse pointer from the center of the window. Remember that there is an imaginary straight line from the center point to the point where the mouse pointer is located. Just calculate the length of the straight line.

Seeing screen coordinates dumped onto you terminal window is exciting for just about 0.02 seconds. Let’s do something else. Since what we have learned so far is drawing lines we will stick to that. Here is a program that draws lines that cut the mouse pointer’s coordinates.

 1 #! /usr/bin/env python
 2 
 3 import pygame
 4 
 5 bgcolor = 0, 0, 0
 6 linecolor = 255, 255, 255
 7 x = y = 0
 8 running = 1
 9 screen = pygame.display.set_mode((640, 400))
10 
11 while running:
12     event = pygame.event.poll()
13     if event.type == pygame.QUIT:
14         running = 0
15     elif event.type == pygame.MOUSEMOTION:
16         x, y = event.pos
17 
18     screen.fill(bgcolor)
19     pygame.draw.line(screen, linecolor, (x, 0), (x, 399))
20     pygame.draw.line(screen, linecolor, (0, y), (639, y))
21     pygame.display.flip()

There is really nothing new in this code so I don’t think you will have any difficult understanding it. We have already learned how to set the color of the line we are drawing, so let’s extend the program slightly:

 1 #! /usr/bin/env python
 2 
 3 import pygame
 4 
 5 bgcolor = 0, 0, 0
 6 blueval = 0
 7 bluedir = 1
 8 x = y = 0
 9 running = 1
10 screen = pygame.display.set_mode((640, 400))
11 
12 while running:
13     event = pygame.event.poll()
14     if event.type == pygame.QUIT:
15         running = 0
16     elif event.type == pygame.MOUSEMOTION:
17         x, y = event.pos
18 
19     screen.fill(bgcolor)
20     pygame.draw.line(screen, (0, 0, blueval), (x, 0), (x, 399))
21     pygame.draw.line(screen, (0, 0, blueval), (0, y), (639, y))
22     blueval += bluedir
23     if blueval == 255 or blueval == 0: bluedir *= -1
24     pygame.display.flip()

Exercises

  1. In the examples above, the program draws lines that extend the full width and the height of the window. Create a program that draws a much smaller ‘+’, say from 10 pixels to the left/top of the mouse pointer to 10 pixels to the right/below the pointer.
  2. Write a program that draws a horizontal line at the y-coordinate of the mouse pointer. The color of the line should vary according to the following: divide the window into four quadrants. Check which of the quadrants the pointer is in and set the line color to red if it is in the first quadrant, green if it is in the second quadrant, blue if in the third quadrant or white if the mouse pointer is in the last quadrant.
  3. Write a program that tracks the positon of the mouse pointer and draws one line from the bottom right right-hand corner of the window to the current mouse position as well as one line from the bottom left-hand corner of the window to the position of the mouse pointer.

Mouse buttons

If you have been doing the exercises so far then you already know how to deal with a mouse button being pressed. Let’s look at an example.

 1 #! /usr/bin/env python
 2 
 3 import pygame
 4 
 5 LEFT = 1
 6 
 7 running = 1
 8 screen = pygame.display.set_mode((320, 200))
 9 
10 while running:
11     event = pygame.event.poll()
12     if event.type == pygame.QUIT:
13         running = 0
14     elif event.type == pygame.MOUSEBUTTONDOWN and event.button == LEFT:
15         print "You pressed the left mouse button at (%d, %d)" % event.pos
16     elif event.type == pygame.MOUSEBUTTONUP and event.button == LEFT:
17         print "You released the left mouse button at (%d, %d)" % event.pos
18 
19     screen.fill((0, 0, 0))
20     pygame.display.flip()

In this example we handle the two events MOUSEBUTTONDOWN and MOUSEBUTTONUP. Run the program and try this. Press the left mouse button down. While holding the button down, move it to a different position, the release the mouse button. Good, now you know how that works.

Exercises

  1. Write a program that draws a horizontal line and a vertical line that both intersect the position of the mouse pointer. The color of the lines should change every time the left button is clicked. The vertical line should be set to a new random color. After generating a random color value, make sure that it is different from the current color. The horizontal line should be set to the previous color of the vertical line.
  2. Write a program that draws a ‘+’ surrounding the mouse pointer when it is pressed down. The size of the cross should be 20 pixels in each direction. When the mouse button is released the cross should disappear.

Conclusion

In this tutorial you have learned how to deal with mouse motion and mouse button state. I think you are now comfortable enough with what events that we can look into them into some detail. Up to now we have been using poll() to get an event off the event queue, for the sake of simplicity. In the next tutorial you will see why that is not really a good idea and what you should do instead. Don’t worry, it won’t really become more complicated.

We will also learn a few more drawing methods.

2007.05.27

Pygame tutorial #2: drawing lines

Filed under: Graphics Programming, Pygame, Pygame Tutorial, Python Programming — Lorenzo E. Danielsson @ 02:46

Welcome to part 2 in my pygame tutorial. This time we will build upon what you learned last time as we start drawing onto surfaces. We will continue to move very slowly so that everybody gets a chance to learn.

Drawing a line

You draw a line by using pygame.draw.line. You can also use pygame.draw.aaline which draws an anti-aliased line. Using anti-aliasing can make the line appear less jagged in some cases at the expense of the function call being much slower.

To draw a blue line from (0, 0) to (200, 100) (note: we are measuring in pixels) onto the surface screen you do:


pygame.draw.line(screen, (0, 0, 255), (0, 0), (200, 100))

You could also do this:


blue = 0, 0, 255
point1 = 0, 0
point2 = 200, 100
pygame.draw.line(screen, blue, point1, point2)

This is will do exactly the same thing as the previous example, but is (possibly) more readable. Let’s try to put this together to a little program. We will draw two diagonal lines: one going from the top left-hand corner to the bottom right-hand corner and one from the top right-hand corner to the bottom left-hand corner. We will use pygame.draw.line for one of the lines and pygame.draw.aaline for the other.

 1 #! /usr/bin/env python
 2 
 3 import pygame
 4 
 5 screen = pygame.display.set_mode((640, 480))
 6 running = 1
 7 
 8 while running:
 9     event = pygame.event.poll()
10     if event.type == pygame.QUIT:
11         running = 0
12 
13     screen.fill((0, 0, 0))
14     pygame.draw.line(screen, (0, 0, 255), (0, 0), (639, 479))
15     pygame.draw.aaline(screen, (0, 0, 255), (639, 0), (0, 479))
16     pygame.display.flip()

I hope you recognize most of the code from the first tutorial. If you run the program you should see a big blue ‘x’ across the window.

Exercises

  1. Improve the program by getting rid of “magic numbers”. Declare two variables width and height and initialize them to the width and the height of the window. Then modify the rest of the program to use these variables instead of actual numbers. To verify that you have made all the required changes, try setting width and height to different values and confirm that you still get two diagonal lines and that each line’s end points are at one of the corners of the window.
  2. Improve the program further by declaring variables called linecolor, topleft, bottomright and so on. Modify the program to make use of the variables.
  3. Modify the program to draw the two lines in different colors.
  4. Write a program that instead draws one horizontal and one vertical line. The lines should both be centered.
  5. Write a program that draws four lines: the diagonal lines as in the example as well has the horizontal and vertical lines as in the last exercise. Each line should be in a different color. The program should take the height and width of the window as command-line arguments.
  6. Write a program that draws the following pattern:
    fishnet.png
    Once you have solved it for the upper left hand corner, repeat it so that the same pattern is drawn in all four corners of the screen. You should be able to draw all in all corners with a single loop and four calls to pygame.draw.line.

Moving things around

Now that we know how to draw lines on the screen with pygame, let’s start moving them around. Moving lines around is simple. we store the line coordinates in variables. Inside the event loop we modify the values of those variables. Let’s draw a line that jumps up and down.

 1 #! /usr/bin/env python
 2 
 3 import pygame
 4 
 5 y = 0
 6 dir = 1
 7 running = 1
 8 width = 800
 9 height = 600
10 screen = pygame.display.set_mode((width, height))
11 linecolor = 255, 0, 0
12 bgcolor = 0, 0, 0
13 
14 while running:
15     event = pygame.event.poll()
16     if event.type == pygame.QUIT:
17         running = 0
18 
19     screen.fill(bgcolor)
20     pygame.draw.line(screen, linecolor, (0, y), (width-1, y))
21 
22     y += dir
23     if y == 0 or y == height-1: dir *= -1
24 
25     pygame.display.flip()

There we go. The y-position of the line is determined by the variable y which increased by dir in each iteration of the event loop. The value of dir is 1 when the line is moving downwards, or -1 when the line is moving upwards. Simple, isn’t it?

Exercises

  1. Comment out the line screen.fill(bgcolor) and run the program. What happens?
  2. Extend the program to also include a vertical line that moves across the screen.

Drawing a color bar

Our final example for today will draw a single color bar (“copper bar” if you’ve ever owned an Amiga). Just like our last example, this one will also jump.

 1 #! /usr/bin/env python
 2 
 3 import pygame
 4 
 5 y = 0
 6 dir = 1
 7 running = 1
 8 barheight = 124
 9 screen = pygame.display.set_mode((800, 600));
10 
11 barcolor = []
12 for i in range(1, 63):
13     barcolor.append((0, 0, i*4))
14 for i in range(1, 63):
15     barcolor.append((0, 0, 255 - i*4))
16 
17 while running:
18     event = pygame.event.poll()
19     if event.type == pygame.QUIT:
20         running = 0
21 
22     screen.fill((0, 0, 0))
23     for i in range(0, barheight):
24         pygame.draw.line(screen, barcolor[i], (0, y+i), (799, y+i))
25 
26     y += dir
27     if y + barheight > 599 or y < 0:
28         dir *= -1
29 
30     pygame.display.flip()

In this example I create an array called colorbar (for the lack of a better name). This will hold values for the colorbar as it shifts from black to bright blue and back to black. Keep in mind that a color is composed of red, green and blue. Each one of these can be a value between 0 and 255.

If I change the blue value by one for each new line, I would get a really smooth gradient bar. But, the height would be 256 pixels from black to blue and another 256 pixels from blue back to black = 512 pixels, which is too high. There would hardly be enough space in our window to see the bar moving up and down.

So I have to decide on a bar height that I find acceptable. I settled on 124, which means I have 62 pixels from the black to blue gradient and another 62 for the blue to black gradient. This also means that I have to move from a blue value of 1 to a blue value of 255 in 62 pixels. The change in blue per line must be 4 per line.

I use two for loops to populate the barcolor array. The first loop pushes increasing values of blue to the array and the second one decreasing values of blue. It is important to notice that this array is nothing other than a lookup. It doesn’t contain the bar as you see it on the screen. It just contains color values.

Run it and see what it looks like. Once you’ve seen it run, analyze the code and make sure you understand exactly how it works. Then start experimenting with it.

Exercises

  1. The color bar example code is horrible. Full of “magic numbers” everywhere! Fix it.
  2. Try using pygame.draw.aaline for drawing the lines. Run the program. Do you notice any difference in speed?
  3. Change the color of the color bar.
  4. Write a program that draws three color bars in different colors. Take note of the height per bar, so that all three can fit and have some space for movement.

Conclusion

In this tutorial you have learned to draw lines. We will be staying with methods of drawing to the screen a little while longer, before we move on to other interesting things like loading and displaying images. In the next tutorial you will also learn a bit about mouse events.


2007.12.11:Added explanation of what the colorbar array does in the last example. Thanks to reader Sergy for pointing out that it needed some explanation.

2007.05.25

Pygame tutorial #1: getting started

Filed under: Game Programming, Graphics Programming, Pygame, Pygame Tutorial, Python Programming — Lorenzo E. Danielsson @ 02:22

This is the first part of my pygame tutorial, aimed at beginners. I will keep it small and simple, meaning I will proceed very slowly. I am not going to go into too much detail. I will explain just about as much as is necessary to get you started. You will need to have the pygame documentation available. Always keep it open while you are going through this tutorial.

One little thing before we begin. I am chronically lazy. It is amazing that I even manage to get out of bed every day. So please don’t expect this to be a regular thing. I will write tutorials as and when my eyes are open.

What you will need

  • Python: I am using Python 2.4 which is the default Python version on Debian Etch. Other versions will work as well.
  • Pygame: There really isn’t much point in programming in pygame unless you have it installed.
  • A text editor: I recommend Vim a powerful editor with an unfortunately bad reputation for being complex. Don’t believe the rumors. They are not true.

If you are on Debian or one of its derivates, try this (as root):

# aptitude install python-pygame

That will install pygame and all dependencies for you. Other distros and operating systems: both Python and pygame are most likely available in your repositories. Figure out how to install them and you are ready to go.

What you should know already

It is a good idea if you at least have some basic knowledge of Python. I guess you could be learning it as you go along, but I will assume that you know how to program in Python already.

Getting started

First of all, let us look at creating a pygame application that does absolutely nothing. Well, nearly absolutely nothing. It actually does display a window. It also does some very rudimentary event handling. You can see this as a template for the next few programs that we will write.

Here is the code:

1 #! /usr/bin/env python
2 
3 import pygame
4 
5 screen = pygame.display.set_mode((640, 400))
6 
7 while 1:
8     pass

First of all, to use pygame we have to import it. Then we create a Surface that is 640 pixels wide and 400 pixels high. There are a lot more things you can do with set_mode, but we will keep it simple at this point.

Next we enter into an infinite loop. We need this because otherwise the window will
If you run this program it should display a 640×400 window. That wasn’t too difficult was it. If you try to close this application by clicking on the window’s close button, you will notice that it does nothing. You will have to go activate the terminal that you started the program from and
hit CTRL+C to stop the program. We will fix that soon.

Exercises

  1. Create a window that is 320 pixels wide and 200 pixels high.
  2. Create a program where the user can specify the width and the height as command line arguments.
  3. Create a program that asks the users for the width and the height and then displays the window.
  4. Write a program that calls pygame.display.set_mode twice with different sizes. What do you expect should happen? Run the program. What actually happens? Can you explain why?

Adding an event loop

Our first example was maybe a little too simple. Staring at a completely blank window soon gets boring. Also, having to go to the terminal and hit CTRL+C to close the window seems a little awkward. Let’s add a bit of code!

Here is an updated version of the first program:

 1 #! /usr/bin/env python
 2 
 3 import pygame
 4 
 5 screen = pygame.display.set_mode((640, 400))
 6 running = 1
 7 
 8 while running:
 9     event = pygame.event.poll()
10     if event.type == pygame.QUIT:
11         running = 0
12 

What is new is that I have added a simple event loop. The loop is controlled by a flag called running. As long as the flag is set the loop keeps running. Inside the loop we use the poll method to grab the next event from the event queue. There are other ways of doing this, but polling works just fine for now.

There are several different event types that pygame knows about and can respond to. One of these is QUIT, which gets triggered when the user clicks on the window’s close button. All we do if we get an event of this type is clear the running flag, which will exit the loop and cause the program to terminate. Still simple, isn’t it.

Exercises

  1. Adapt each of the programs you wrote for the exercises in the previous section to use an event loop.
  2. Rewrite the program to do away with the running flag. Make sure that the program still jumps out of the event loop on the QUIT event.

Finishing touches to the template

As a final step before we start doing real things, let’s add just a little bit more so that we have a complete template for what follows. We will paint the our Surface and we will learn how to do a bit of screen flipping. First the code:

 1 #! /usr/bin/env python
 2 
 3 import pygame
 4 
 5 screen = pygame.display.set_mode((640, 400))
 6 running = 1
 7 
 8 while running:
 9     event = pygame.event.poll()
10     if event.type == pygame.QUIT:
11         running = 0
12     screen.fill((0, 0, 0))
13     pygame.display.flip()
14 

We have added just two lines of code. The first one sets a background color for the Surface. We have passed in a sequence of three values: red, green and blue. Each one can be a value between 0 and 255. Since we set all to zero, we get a black screen. You should experiment with different values for red, green and blue.

The next thing that is new is that we call pygame.display.flip. Drawing directly to the screen is usually a very bad idea. Instead, we have a invisible buffer that we draw onto. When we have finished drawing we make the buffer visible. That way we get flicker-free animations (when we get to that).

Exercises

  1. Create a window with a white background color.
  2. Create a window with a red background color.
  3. Experiment with setting different background colors. If you are not familiar with RGB values then spend a little extra time to figure out how to get colors like yellow, brown, cyan etc.
  4. Create a program that asks the user to specify the values for red, green and blue. Check that the values are in the valid range (0-255) and then use these for the background color.
  5. Create a program that upon start-up checks the time of the day and sets the brightness of the background color accordingly. Use a blue scale. If it is midnight, the screen should be black. If it is midday, the screen should be bright blue. Any other time the background should be something in between corresponding to the brightness outside.

Conclusion

So what have you learned so far? Not an awful lot, by the looks of it. But in a way you have. You have acquired some basics that you will need to understand the next tutorial, which will come soon. In the meantime, try your hands on a few of the exercises. If you complete all you can make up a few of your own.

Blog at WordPress.com.