Creating Visuals - Beginning Python Games Development With Pygame (2015)

Beginning Python Games Development With Pygame (2015)

CHAPTER 4

image

Creating Visuals

Computer games are very visual in nature, and game developers spend a lot of time working on manipulating graphics and refining the visuals to create the most entertaining experience for the player. This chapter gives you a strong foundation in generating visuals for computer games.

Proceeding through this chapter, we will use examples written in Python and using the Pygame module to illustrate our point. You are not required to understand these programs in full, but you’re encouraged to try!

Using Pixel Power

If you peer closely at your computer screen, you should be able to make out that it is composed of rows and columns of colored dots. These dots are packed so tightly together that when you view the screen at a comfortable distance they merge to form a single image. An individual dot in the display is called a picture element, or pixel. Because computer games are primarily visual in nature, pixels are very much the tools of the trade for game programmers.

Let’s write a small Python script to generate an image containing every possible color. Listing 4-1 uses Pygame to create a large image with every possible color value. The script takes a couple of minutes to run, but when it is finished it will have saved an image file called allcolors.bmp, which you can open in an image viewer or web browser. Don’t worry about the details of Listing 4-1; we will cover the unfamiliar code in this chapter.

Listing 4-1. Generating an Image Containing Every Color

import pygame
pygame.init()

screen = pygame.display.set_mode((640, 480))

all_colors = pygame.Surface((4096,4096), depth=24)

for r in range(256):
print(r+1, "out of 256")
x = (r&15)*256
y = (r>>4)*256
for g in range(256):
for b in range(256):
all_colors.set_at((x+g, y+b), (r, g, b))

pygame.image.save(all_colors, "allcolors.bmp")
pygame.quit()

Listing 4-1 is unusual for a Pygame script because it is not interactive. When it runs you will see it count from 1 to 256 and then exit after saving the bitmap file in the same location as the script. Don’t worry that it is slow—generating bitmaps one pixel at a time is something you should never need to do in a game!

Working with Color

You are probably familiar with how colors are created with paint. If you have a pot of blue paint and a pot of yellow paint, then you can create shades of green by mixing the two together. In fact, you can produce any color of paint by mixing the primary colors red, yellow, and blue in various proportions. Computer color works in a similar way, but the “primary” colors are red, green, and blue. To understand the difference we need to cover the science behind color— don’t worry, it’s not complicated.

To see a color, light from the sun or a bulb has to bounce off something and pass through the lens in your eye. Sunlight or artificial light from a bulb may appear white, but it actually contains all the colors of the rainbow mixed together. When light hits a surface, some of the colors in it are absorbed and the remainder is reflected. It’s this reflected light that enters your eye and is perceived as color. When colors are created in this way, it is called color subtraction. Computer screens work differently. Instead of reflecting light, they generate their own and create color by adding together red, green, and blue light (a process known as color addition).

That’s enough science for the moment. For now we need to know how to represent color in a Python program, because it is impractical to think of names for all 16.7 million of them!

Representing Color in Pygame

When Pygame requires a color, you pass it in as a tuple of three integers, one for each color component in red, green, and blue order. The value of each component should be in the range 0 to 255, where 255 is full intensity and 0 means that the component doesn’t contribute anything to the final color. Table 4-1 lists the colors you can create by using components set to either off or full intensity.

Table 4-1. Color Table

Table4-1

Some early computers were limited to just these gaudy colors; fortunately you can create more subtle hues nowadays!

It’s well worth experimenting with different values so that you have an intuitive feel for computer-generated color. With a little practice you should find that you can look at the three color values and make an educated guess at what the color looks like. Let’s write a script to help us to do this. When you run Listing 4-2, you will see a screen split into two halves. At the top of the screen are three scales—one for each of the red, green, and blue components—and a circle to represent the currently selected value. If you click anywhere on one of the scales, it will modify the component and change the resulting color, which is displayed on the lower half of the screen.

Try adjusting the sliders to (96, 130, 51), which gives a convincing shade of zombie green, or (221, 99, 20) for a pleasing fireball orange.

Listing 4-2. Script for Tweaking Colors

import pygame
from pygame.locals import *
from sys import exit

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

# Creates images with smooth gradients
def create_scales(height):
red_scale_surface = pygame.surface.Surface((640, height))
green_scale_surface = pygame.surface.Surface((640, height))
blue_scale_surface = pygame.surface.Surface((640, height))
for x in range(640):
c = int((x/639.)*255.)
red = (c, 0, 0)
green = (0, c, 0)
blue = (0, 0, c)
line_rect = Rect(x, 0, 1, height)
pygame.draw.rect(red_scale_surface, red, line_rect)
pygame.draw.rect(green_scale_surface, green, line_rect)
pygame.draw.rect(blue_scale_surface, blue, line_rect)
return red_scale_surface, green_scale_surface, blue_scale_surface

red_scale, green_scale, blue_scale = create_scales(80)

color = [127, 127, 127]

while True:

for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()

screen.fill((0, 0, 0))

# Draw the scales to the screen
screen.blit(red_scale, (0, 00))
screen.blit(green_scale, (0, 80))
screen.blit(blue_scale, (0, 160))

x, y = pygame.mouse.get_pos()

# If the mouse was pressed on one of the sliders, adjust the color component
if pygame.mouse.get_pressed()[0]:
for component in range(3):
if y > component*80 and y < (component+1)*80:
color[component] = int((x/639.)*255.)
pygame.display.set_caption("PyGame Color Test - "+str(tuple(color)))

# Draw a circle for each slider to represent the current setting
for component in range(3):
pos = ( int((color[component]/255.)*639), component*80+40 )
pygame.draw.circle(screen, (255, 255, 255), pos, 20)

pygame.draw.rect(screen, tuple(color), (0, 240, 640, 240))

pygame.display.update()

Listing 4-2 introduces the pygame.draw module, which is used to draw lines, rectangles, circles, and other shapes on the screen. We will cover this module in more detail later in this chapter.

Once you have a color, there are a number of things you may want to do to it. Let’s say we have a soldier in a space game that has the misfortune to be caught in a meteor shower without an umbrella. We could use “fireball orange” for the meteors as they streak through the atmosphere, but when they hit the ground they would gradually fade to black. How do we find the darker colors?

Scaling Colors

To make a color darker, you simply multiply each of the components by a value between 0 and 1. If you take fireball orange (221, 99, 20) and multiply each component by 0.5 (in other words, decrease them by one-half), then you get (110.5, 49.5, 10). But because color components are integers we need to drop the fractional part to get (110, 49, 10). If you use Listing 4-2 to create this color, you should see that it is indeed a darker shade of fireball orange. We don’t want to have to do the math in our head every time, so let’s write a function to do it for us. Listing 4-3 is a function that takes a color tuple, multiplies each number by a float value, and returns a new tuple.

Listing 4-3. Function for Scaling a Color

def scale_color(color, scale):
red, green, blue = color
red = int(red*scale)
green = int(green*scale)
blue = int(blue*scale)
return red, green, blue

fireball_orange = (221, 99, 20)
print(fireball_orange)
print(scale_color(fireball_orange, .5))

If you run Listing 4-3, it will display the color tuple for fireball orange and the darker version:

(221, 99, 20)
(110, 49, 10)

Multiplying each of the components by a value between 0 to 1 makes a color darker, but what if you multiply by a value that is greater than 1? It will make the color brighter, but there is something you have to watch out for. Let’s use a scale value of 2 to make a really bright fireball orange. Add the following line to Listing 4-3 to see what happens to the color:

print(scale_color(fireball_orange, 2.))

This adds an additional color tuple to the output:

(442, 198, 40)

The first (red) component is 442—which is a problem because color components must be a value between 0 and 255! If you use this color tuple in Pygame it will throw a TypeError exception, so it is important that we “fix” it before using it to draw anything. All we can do is check each component and if it goes over 255, set it back to 255—a process known as saturating the color. Listing 4-4 is a function that performs color saturation.

Listing 4-4. Function for Saturating a Color

def saturate_color(color):
red, green, blue = color
red = min(red, 255)
green = min(green, 255)
blue = min(blue, 255)
return red, green, blue

Listing 4-4 uses the built-in function min, which returns the lower of the two values. If the component is in the correct range, it is returned unchanged. But if it is greater than 255, it returns 255 (which is exactly the effect we need).

If we saturate the extra bright fireball orange after scaling it, we get the following output, which Pygame will happily accept:

(255, 198, 40)

With color components saturated at 255, the color will be brighter but may not be exactly the same hue. And if you keep scaling a color, it may eventually change to (255, 255, 255), which is bright white. It is usually better to select the brightest shade of the color you want and scale it downward (using a factor less than 1).

We now know what scaling does when using a value that is greater than zero. But what if it is less than zero—that is, negative? Using negative values when scaling a color produces negative color components, which don’t make sense because you can’t have less than zero red, green, or blue in a color. Avoid scaling colors by a negative value!

Blending Colors

Something else you may want to do with colors is blend one color gradually into another. Let’s say we have a zombie in a horror game that is normally a sickly shade of zombie green but has recently emerged from a lava pit and is currently glowing a bright shade of fireball orange. Over time the zombie will cool down and return to its usual color. But how do we calculate the intermediate colors to make the transition look smooth?

We can use something called linear interpolation, which is a fancy term for moving from one value to another in a straight line. It is such a mouthful that game programmers prefer to use the acronym lerp. To lerp between two values, you find the difference between the second and the first value, multiply it by a factor between 0 and 1, and then add that to the first value. A factor of 0 or 1 will result in the first or second values, but a factor of .5 gives a value that is halfway between the first and second. Any other factors will result in a proportional value between the two end points. Let’s see an example in Python code to make it clearer. Listing 4-5 defines a function lerp that takes two values and a factor and returns a blended value.

Listing 4-5. Simple Lerping Example

def lerp(value1, value2, factor):
return value1+(value2-value1)*factor

print(lerp(100, 200, 0.))
print(lerp(100, 200, 1.))
print(lerp(100, 200, .5))
print(lerp(100, 200, .25))

This results in the following output. Try to predict what the result of lerp(100, 200, .75) will be.

100.0
200.0
150.0
125.0

To lerp between colors, you simply lerp between each of the components to produce a new color. If you vary the factor over time, it will result in a smooth color transition. Listing 4-6 contains the function blend_color, which does color lerping.

Listing 4-6. Blending Colors by Lerping

import pygame
from pygame.locals import *
from sys import exit

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

color1 = (221, 99, 20)
color2 = (96, 130, 51)
factor = 0.

def blend_color(color1, color2, blend_factor):
red1, green1, blue1 = color1
red2, green2, blue2 = color2
red = red1+(red2-red1)*blend_factor
green = green1+(green2-green1)*blend_factor
blue = blue1+(blue2-blue1)*blend_factor
return int(red), int(green), int(blue)

while True:

for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()

screen.fill((255, 255, 255))

tri = [ (0,120), (639,100), (639, 140) ]
pygame.draw.polygon(screen, (0,255,0), tri)
pygame.draw.circle(screen, (0,0,0), (int(factor*639.), 120), 10)

x, y = pygame.mouse.get_pos()
if pygame.mouse.get_pressed()[0]:
factor = x / 639.
pygame.display.set_caption("PyGame Color Blend Test - %.3f"%factor)

color = blend_color(color1, color2, factor)
pygame.draw.rect(screen, color, (0, 240, 640, 240))

pygame.display.update()

If you run Listing 4-6, you will see a slider at the top of the screen. Initially it will be at the far left, representing a factor of 0 (fireball orange). If you click and drag toward the right of the screen, you can smoothly change the blending factor toward 1 (zombie green). The resulting color is displayed in the lower half of the screen.

You can experiment with blending between other colors by changing the values of color1 and color2 at the top of the script. Try blending between completely contrasting colors and shades of similar colors.

Using Images

Images are an essential part of most games. The display is typically assembled from a collection of images stored on the hard drive (or CD, DVD, or other media device). In a 2D game, the images may represent background, text, player characters, or artificial intelligence (AI) opponents. In a 3D game, images are typically used as textures to create 3D scenes.

Computers store images as a grid of colors. The way these colors are stored varies depending on how many are needed to reproduce the image. Photographs require the full range of colors, but diagrams or black and white images may be stored differently. Some images also store extra information for each pixel. In addition to the usual red, green, and blue components, there may be an alpha component. The alpha value of a color is most often used to represent translucency so that when drawn on top of another image, parts of the background can show through. We used an image with an alpha channel in Hello World Redux (Listing 3-1). Without an alpha channel, the image of a fish would be drawn inside an ugly rectangle.

Creating Images with an Alpha Channel

If you have taken a photo with a digital camera or drawn it with some graphics software, then it probably won’t have an alpha channel. Adding an alpha channel to an image usually involves a little work with graphics software. To add an alpha channel to the image of a fish, I used Photoshop, but you can also use other software such as GIMP (http://www.gimp.org) to do this. For an introduction to GIMP, see Akkana Peck’s Beginning GIMP: From Novice to Professional (Apress, 2006).

An alternative to adding an alpha channel to an existing image is to create an image with a 3D rendering package such as Autodesk’s 3ds Max or the free alternative, Blender (http://www.blender.org). With this kind of software you can directly output an image with an invisible background (you can also create several frames of animation or views from different angles). This may produce the best results for a slick-looking game, but you can do a lot with the manual alpha channel technique. Try taking a picture of your cat, dog, or goldfish and making a game out of it!

Storing Images

There are a variety of ways to store an image on your hard drive. Over the years, many formats for image files have been developed, each with pros and cons. Fortunately a small number have emerged as being the most useful, two in particular: JPEG and PNG. Both are well supported in image-editing software and you will probably not have to use other formats for storing images in a game.

· JPEG (Joint Photographic Expert Group) JPEG image files typically have the extension jpg or sometimes .jpeg. If you use a digital camera, the files it produces will probably be JPEGs, because they were specifically designed for storing photographs. They use a process known as lossy compression, which is very good at shrinking the file size. The downside of lossy compression is that it can reduce the quality of the image, but usually it is so subtle that you will not notice the difference. The amount of compression can also be adjusted to compromise between visual quality and compression. They may be great for photos, but JPEGs are bad for anything with hard edges, such as fonts or diagrams, because the lossy compression tends to distort these kinds of images. If you have any of these kinds of images, PNG is probably a better choice.

· PNG (Portable Network Graphics) PNG files are probably the most versatile of images formats because they can store a wide variety of image types and still compress very well. They also support alpha channels, which is a real boon for game developers. The compression that PNGs use is lossless, which means that images stored as PNG files will be exactly the same as the originals. The downside is that even with good compression they will probably be larger than JPEGs.

In addition to JPEG and PNG, Pygame supports reading the following formats:

· GIF (non-animated)

· BMP

· PCX

· TGA (uncompressed only)

· TIF

· LBM (and PBM)

· PBM (and PGM, PPM)

· XPM

As a rule of thumb, use JPEG only for large images with lots of color variation; otherwise, use PNGs.

Working with Surface Objects

Loading images into Pygame is done with a simple one-liner; pygame.image.load takes the filename of the image you want to load and returns a surface object, which is a container for an image. Surfaces can represent many types of images, but Pygame hides most of these details from us so we can treat them in much the same way. Once you have a surface in memory, you can draw on it, transform it, or copy it to another surface to build up an image. Even the screen is represented as a surface object. The initial call to pygame.display.set_mode returns a surface object that represents the display.

Creating Surfaces

A call to pygame.image.load is one way to create a surface. It creates a surface that matches the colors and dimensions of the image file, but you can also create blank surfaces of any size that you need (assuming there is enough memory to store it). To create a blank surface, call thepygame.Surface constructor with a tuple containing the required dimensions. The following line creates a surface that is 256 by 256 pixels:

blank_surface = pygame.Surface((256, 256))

Without any other parameters, this creates a surface with the same number of colors as the display. This is usually what you want, because it is faster to copy images when they have the same number of colors.

There is also a depth parameter for pygame.Surface that defines the color depth for the surface. This is similar to the depth parameter in pygame.display.set_mode and defines the maximum number of colors in the surface. Generally it is best not to set this parameter (or set it to 0), because Pygame selects a depth that matches the display—although if you want alpha information in the surface, you should set depth to 32. The following line creates a surface with alpha information:

blank_alpha_surface = pygame.Surface((256, 256), depth=32)

Converting Surfaces

When you use surface objects, you don’t have to worry about how the image information is stored in memory because Pygame will hide this detail from you. So most of the time the image format is something you don’t need to worry about, since your code will work regardless of what type of images you use. The only downside of this automatic conversion is that Pygame will have to do more work if you are using images with different formats, and that can potentially decrease game performance. The solution is to convert all your images to be the same format. Surface objects have a convert method for this purpose.

If you call convert without any parameters, the surface will be converted to the format of the display surface. This is useful because it is usually fastest to copy surfaces when the source and destination are the same type—and most images will be copied to the display eventually. It is a good idea to tack on .convert() to any calls to pygame.image.load to ensure your images is in the fastest format for the display. The exception is when your image has an alpha channel, because convert can discard it. Fortunately Pygame provides a convert_alpha method, which converts the surface to a fast format but preserve any alpha information in the image. We have used both methods in the previous chapter; the following two lines are taken from Listing 3-1:

background = pygame.image.load(background_image_filename).convert()
mouse_cursor = pygame.image.load(mouse_image_filename).convert_alpha()

The background is just a solid rectangle so we use convert. However, the mouse cursor has irregular edges and needs alpha information, so we call convert_alpha.

Both convert and convert_alpha can take another surface as a parameter. If a surface is supplied, the surface will be converted to match the other surface.

Keep in mind that, to use the preceding lines, you need a surface object already defined; otherwise you receive an error because no video mode has been specified.

Rectangle Objects

Pygame often requires you to give it a rectangle to define what part of the screen should be affected by a function call. For instance, you can restrict Pygame to drawing on a rectangular area of the screen by setting the clipping rectangle (covered in the next section). You can define a rectangle using a tuple that contains four values: the x and y coordinate of the top-left corner followed by the width and height of the rectangle. Alternatively, you can give the x and y coordinate as a single tuple followed by the width and height as another tuple. The following two lines define the rectangle with the same dimensions:

my_rect1 = (100, 100, 200, 150)
my_rect2 = ((100, 100), (200, 150))

You can use whichever method is most convenient at the time. For instance, you may already have the coordinate and size stored as a tuple, so it would be easier to use the second method.

In addition to defining rectangles, Pygame has a Rect class that stores the same information as a rectangle tuple but contains a number of convenient methods to work with them. Rect objects are used so often that they are included in pygame.locals—so if you have from pygame.locals import * at the top of your script, you don’t need to precede them with a module name.

To construct a Rect object, you use the same parameters as a rectangle tuple. The following lines construct the Rect objects equivalent to the two rectangle tuples:

from pygame import rect
my_rect3 = Rect(100, 100, 200, 150)
my_rect4 = Rect((100, 100), (200, 150))

Once you have a Rect object, you can adjust its position or size, detect whether a point is inside or outside, or find where other rectangles intersect. See the Pygame documentation for more details (http://www.pygame.org/docs/ref/rect.html).

Clipping

Often when you are building a screen for a game, you might want to draw only to a portion of the display. For instance, in a strategy command & conquer–type game, you might have the top of the screen as a scrollable map, and below it a panel that displays troop information. But when you start to draw the troop images to the screen, you don’t want to have them draw over the information panel. To solve this problem, surfaces have a clipping area, which is a rectangle that defines what part of the screen can be drawn to. To set the clipping area, call the set_clip method of a surface object with a Rect-style object. You can also retrieve the current clipping region by calling get_clip.

The following snippet shows how we might use clipping to construct the screen for a strategy game. The first call to clip sets the region so that the call to draw_map can only draw onto the top half of the screen. The second call to set_clip sets the clipping area to the remaining portion of the screen:

screen.set_clip(0, 0, 640, 300)
draw_map()
screen.set_clip(0, 300, 640, 180)
draw_panel()

Subsurfaces

A subsurface is a surface inside another surface. When you draw onto a subsurface, it also draws onto its parent surface. One use of subsurfaces is to draw graphical fonts. The pygame.font module produces nice, crisp-looking text in a single color, but some games require more graphically rich lettering. You could save an image file for each letter, but it would probably be easier to create a single image with all the letters on it, and then create 26 subsurfaces when you load the image into Pygame.

To create a subsurface, you call the subsurface method of Surface objects, which takes a rectangle that defines what portion of the parent it should cover. It will return a new Surface object that has the same color format as the parent. Here’s how we might load a font image and divide it into letter-sized portions:

my_font_image = Pygame.load("font.png")
letters = []
letters["a"] = my_font_image.subsurface((0,0), (80,80))
letters["b"] = my_font_image.subsurface((80,0), (80,80))

This creates two subsurfaces of my_font_image and stores them in a dictionary so that we can easily look up the subsurface for a given letter. Of course, we would need more than “a” and “b,” so the call to subsurface would probably be in a loop that repeats 26 times.

When you work with subsurfaces, it is important to remember that they have their own coordinate system. In other words, the point (0, 0) in a subsurface is always the top-left corner no matter where it sits inside its parent.

Filling Surfaces

When you create an image on the display, you should cover the entire screen; otherwise, parts of the previous screen will show through. If you don’t draw over every pixel, you will get an unpleasant strobing effect when you try to animate anything. The easiest way to avoid this is to clear the screen with a call to the fill method of surface objects, which takes a color. The following clears the screen to black:

screen.fill((0, 0, 0))

The fill function also takes an optional rectangle that defines the area to clear, which is a convenient way to draw solid rectangles.

Image Note If you draw over the entire screen with other methods, you won’t need to clear it with a call to fill.

Setting Pixels in a Surface

One of the most basic things you can do with a surface is set individual pixels, which has the effect of drawing a tiny dot. It is rarely necessary to draw pixels one at a time because there are more efficient ways of drawing images, but if you ever need to do any offline image manipulation it can be useful.

To draw a single pixel onto a surface, use the set_at method, which takes the coordinate of the pixel you want to set, followed by the color you want to set it to. We will test set_at by writing a script that draws random pixels. When you run Listing 4-7 you will see the screen slowlyfill up with random-colored dots; each one is an individual pixel.

Listing 4-7. Script That Draws Random Pixels (random.py)

import pygame
from pygame.locals import *
from sys import exit
from random import randint

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

while True:

for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()

rand_col = (randint(0, 255), randint(0, 255), randint(0, 255))
for _ in range(100):
rand_pos = (randint(0, 639), randint(0, 479))
screen.set_at(rand_pos, rand_col)

pygame.display.update()

Getting Pixels in a Surface

The complement of set_at is get_at, which returns the color of the pixel at a given coordinate. Getting pixels is occasionally necessary for collision detection so that the code can determine what the player character is standing on by looking at the color underneath it. If all platforms and obstacles are a certain color (or range of colors), this would work quite well. set_at takes just one parameter, which should be a tuple of the coordinates of the pixel you want to look at. The following line gets the pixel at coordinate (100, 100) in a surface called screen:

my_color = screen.get_at((100, 100))

Image Caution The get_at method can be very slow when reading from hardware surfaces. The display can be a hardware surface, especially if you are running full screen—so you should probably avoid getting the pixels of the display.

Locking Surfaces

Whenever Pygame draws onto a surface, it first has to be locked. When a surface is locked, Pygame has full control over the surface and no other process on the computer can use it until it is unlocked. Locking and unlocking happens automatically whenever you draw onto a surface, but it can become inefficient if Pygame has to do many locks and unlocks.

In Listing 4-7 there is a loop that calls set_at 100 times, which leads to Pygame locking and unlocking the screen surface 100 times. We can reduce the number of locks and unlocks and speed up the loop by doing the locking manually. Listing 4-8 is almost identical to the previous listing, but runs faster because there is a call to lock before drawing and a call to unlock after all the pixels have been drawn.

Image Caution There should be the same number of calls to lock as there are to unlock. If you forget to unlock a surface, Pygame may become unresponsive.

Listing 4-8. Random Pixels with Locking (randoml.py)

import pygame
from pygame.locals import *
from sys import exit
from random import randint

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

while True:

for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()

rand_col = (randint(0, 255), randint(0, 255), randint(0, 255))
screen.lock()
for _ in range(100):
rand_pos = (randint(0, 639), randint(0, 479))
screen.set_at(rand_pos, rand_col)
screen.unlock()
pygame.display.update()

Not all surfaces need to be locked. Hardware surfaces do (the screen is usually a hardware surface), but plain software surfaces do not. Pygame provides a mustlock method in surface objects that returns True if a surface requires locking. You could check the return value ofmustlock before you do any locking or unlocking, but there is no problem in locking a surface that doesn’t need it, so you may as well lock any surface that you plan on doing a lot of drawing to.

Blitting

The method of surface objects that you will probably use most often is blit, which is an acronym for bit block transfer. Blitting simply means copying image data from one surface to another. You will use it for drawing backgrounds, fonts, characters, and just about everything else in a game!

To blit a surface, you call blit from the destination surface object (often the display) and give it the source surface (your sprite, background, and so forth); followed by the coordinate you want to blit it to. You can also blit just a portion of the surface by adding a Rect-style object to the parameter that defines the source region. Here are two ways you use the blit method:

screen.blit(background, (0,0))

which blits a surface called background to the top-left corner of screen. If background has the same dimensions as screen, we won’t need to fill screen with a solid color.

The other way is

screen.blit(ogre, (300, 200), (100*frame_no, 0, 100, 100))

If we have an image containing several frames of an ogre walking, we could use something like this to blit it to the screen. By changing the value of frame_no, we can blit from a different area of the source surface.

Drawing with Pygame

We have used a few functions from the pygame.draw module in the preceding examples. The purpose of this module is to draw lines, circles, and other geometric shapes to the screen. You could use it to create an entire game without loading any images. The classic Atari game Asteroids is an example of a great game that just uses shapes drawn with lines. Even if you don’t use the pygame.draw module to create a complete game, you will find it useful for experimenting when you don’t want to go to the trouble of creating images. You could also use it to draw a debug overlay on top of your game when you need to visualize what is happening in your code.

The first two parameters for functions in pygame.draw are the surface you want to render to—which could be the screen (display surface) or a plain surface—followed by the color you want to draw in. Each draw function will also take at least one point, and possibly a list of points. A point should be given as a tuple containing the x and y coordinates, where (0, 0) is the top left of the screen.

The return value for these draw functions is a Rect object that gives the area of the screen that has been drawn to, which can be useful if we only want to refresh parts of the screen that have been changed. Table 4-2 lists the functions in the pygame.draw module, which we will cover in this chapter.

Table 4-2. The pygame.draw Module

Function

Purpose

rect

Draws a rectangle

polygon

Draws a polygon (shape with three or more sides)

circle

Draws a circle

ellipse

Draws an ellipse

arc

Draws an arc

line

Draws a line

lines

Draws several lines

aaline

Draws an antialiased (smooth) line

aalines

Draws several antialiased lines

pygame.draw.rect

This function draws a rectangle onto a surface. In addition to the destination surface and color, pygame.rect takes the dimensions of the rectangle you want to draw and the width of the line. If you set width to 0 or omit it, the rectangle will be filled with solid color; otherwise, just the edges will be drawn.

Let’s write a script to test Pygame’s rectangle-drawing capabilities. Listing 4-9 draws ten randomly filled rectangles in random positions and colors. It produces a strangely pretty, modern art–like effect.

Listing 4-9. Rectangle Test

import pygame
from pygame.locals import *
from sys import exit

from random import *

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

screen.lock()
for count in range(10):
random_color = (randint(0,255), randint(0,255), randint(0,255))
random_pos = (randint(0,639), randint(0,479))
random_size = (639-randint(random_pos[0],639), 479-randint(random_pos[1],479))
pygame.draw.rect(screen, random_color, Rect(random_pos, random_size))

screen.unlock()

pygame.display.update()

while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()

There is another way to draw filled rectangles on a surface. The fill method of surface objects takes a Rect-style object that defines what part of the surface to fill—and draws a perfect filled rectangle! In fact, fill can be faster than pygame.draw.rect; it can potentially be hardware accelerated (in other words, performed by the graphics card and not the main processor).

pygame.draw.polygon

A polygon is a many-sided shape, that is, anything from a triangle to a myriagon (10,000 sides—I looked it up!) and beyond. A call to pygame.draw.polygon takes a list of points and draws the shape between them. Like pygame.rect, it also takes an optional width value. Ifwidth is omitted or set to 0, the polygon will be filled; otherwise, only the edges will be drawn.

We test Pygame’s polygon-drawing capabilities with a simple script. Listing 4-10 keeps a list of points. Every time it gets a MOUSEBUTTONDOWN event, it adds the position of the mouse to the list of points. When it has at least three points, it will draw a polygon.

Try adding a width parameter to the call to pygame.draw.polygon to use nonfilled polygons.

Listing 4-10. Drawing Polygons with Pygame

import pygame
from pygame.locals import *
from sys import exit

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

points = []

while True:

for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == MOUSEBUTTONDOWN:
points.append(event.pos)

screen.fill((255,255,255))

if len(points) >= 3:
pygame.draw.polygon(screen, (0,255,0), points)
for point in points:
pygame.draw.circle(screen, (0,0,255), point, 5)

pygame.display.update()

pygame.draw.circle

The circle function draws a circle on a surface. It takes the center point and the radius of the circle (the radius is the distance from the center to the edge). Like the other draw functions, it also takes a value for the width of the line. If width is 0 or omitted, the circle will be drawn with a line; otherwise, it will be a solid circle. Listing 4-11 draws randomly filled circles on the screen, in a random color.

Listing 4-11. Random Circles

import pygame
from pygame.locals import *
from sys import exit

from random import *

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

for _ in range(25):
random_color = (randint(0,255), randint(0,255), randint(0,255))
random_pos = (randint(0,639), randint(0,479))
random_radius = randint(1,200)
pygame.draw.circle(screen, random_color, random_pos, random_radius)

pygame.display.update()

while True:

for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()

pygame.draw.ellipse

You can think of an ellipse as being a squashed circle. If you were to take a circle and stretch it to fit it into a rectangle, it would become an ellipse. In addition to surface and color, the ellipse function takes a Rect-style object that the ellipse should fit in to. It also takes a widthparameter, which is used just like rect and circle. Listing 4-12 draws an ellipse that fits in a rectangle stretching from the top-left corner of the screen to the current mouse position.

Listing 4-12. Drawing an Ellipse

import pygame
from pygame.locals import *
from sys import exit

from random import *

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

while True:

for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()

x, y = pygame.mouse.get_pos()
screen.fill((255,255,255))
pygame.draw.ellipse(screen, (0,255,0), (0,0,x,y))

pygame.display.update()

pygame.draw.arc

The arc function draws just a section of an ellipse, but only the edge; there is no fill option for arc. Like the ellipse function, it takes a Rect-style object that the arc would fit into (if it covered the entire ellipse). It also takes two angles in radians. The first angle is where the arc should start drawing, and the second is where it should stop. It also takes a width parameter for the line, which defaults to 1, but you can set it to greater values for a thicker line. Listing 4-13 draws a single arc that fits into the entire screen. The end angle is taken from the x coordinate of the mouse, so if you move the mouse left and right it will change the length of the arc.

Listing 4-13. Arc Test

import pygame
from pygame.locals import *
from sys import exit

from random import *
from math import pi

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

while True:

for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()

x, y = pygame.mouse.get_pos()
angle = (x/639.)*pi*2.
screen.fill((255,255,255))
pygame.draw.arc(screen, (0,0,0), (0,0,639,479), 0, angle)

pygame.display.update()

pygame.draw.line

A call to pygame.draw.line draws a line between two points. After the surface and color, it takes two points: the start point and the end point of the line you want to draw. There is also the optional width parameter, which works the same way as rect and circle. Listing 4-14draws several lines from the edges of the screen to the current mouse position.

Listing 4-14. Line Drawing (drawinglines.py)

import pygame
from pygame.locals import *
from sys import exit

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

while True:

for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()

screen.fill((255, 255, 255))

mouse_pos = pygame.mouse.get_pos()

for x in range(0,640,20):
pygame.draw.line(screen, (0, 0, 0), (x, 0), mouse_pos)
pygame.draw.line(screen, (0, 0, 0), (x, 479), mouse_pos)

for y in range(0,480,20):
pygame.draw.line(screen, (0, 0, 0), (0, y), mouse_pos)
pygame.draw.line(screen, (0, 0, 0), (639, y), mouse_pos)

pygame.display.update()

pygame.draw.lines

Often lines are drawn in sequence, so that each line begins where the previous one left off. The first parameter to pygame.draw.lines is a boolean that indicates whether the line is closed. If set to True an additional line will be drawn between the last point in the list and the first; otherwise, it will be left open. Following this value is a list of points to draw lines between and the usual width parameter.

Listing 4-15 uses pygame.draw.lines to draw a line from a list of points, which it gets from the mouse position. When there are more than 100 points in the list, it deletes the first one, so the line miraculously starts to “undraw” itself! This might be a good starting point for a wormgame.

Listing 4-15. Drawing Multiple Lines (multiplelines.py)

import pygame
from pygame.locals import *
from sys import exit

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

points = []

while True:

for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == MOUSEMOTION:
points.append(event.pos)
if len(points)>100:
del points[0]

screen.fill((255, 255, 255))

if len(points)>1:
pygame.draw.lines(screen, (0,255,0), False, points, 2)

pygame.display.update()

pygame.draw.aaline

You may have noticed from the previous line drawing functions that the lines have a jagged appearance. This is because a pixel can only be drawn at a coordinate on a grid, which may not lie directly underneath a line if it is not horizontal or vertical. This effect is called aliasing, something which computer scientists have put a lot of work into avoiding. Any technique that attempts to avoid or reduce aliasing is called antialiasing.

Pygame can draw antialiased lines that appear significantly smoother than the lines drawn by pygame.draw.line. The function pygame.draw.aaline has the same parameters as pygame.draw.line but draws smooth lines. The downside of antialiased lines is that they are slower to draw than regular lines, but only marginally so. Use aaline whenever visual quality is important.

To see the difference, replace the call to pygame.draw.line in the previous example code with the aaline version.

pygame.draw.aalines

Just like pygame.draw.line, there is an antialiased version of pygame.draw.lines. A call to pygame.draw.aalines uses the same parameters as pygame.draw.lines but draws smooth lines, so it is easy to switch between the two in code.

Summary

Colors are the most fundamental things in creating computer graphics. All images in a game are ultimately created by manipulating colors in some form or another. We’ve seen how Pygame stores colors and how to make new colors by combining existing ones. In the process of learning about color manipulation, we introduced lerping (linear interpolation), which we will use for various game tasks.

Surface objects are Pygame’s canvases, and can store all kinds of images. Fortunately we don’t have to worry about the details of how they are stored, because when you manipulate images through a surface they all appear to be the same type.

We covered the draw module in some detail, because it is handy for visually depicting additional information in your game. For instance, you could use it to draw a little arrow over your enemy character, indicating which way they are headed.

This chapter has covered all the ways that you can create visuals with Pygame. Armed with this information, you can create images of dungeons, alien worlds, and other game environments.

In the next chapter you will learn how to animate graphics over time.