Chapter 6. Bitmap Graphics: The Orbiting Spaceship Demo

This chapter explains how to load and draw bitmaps using the pygame.Surface and pygame.image classes. We have already been using this class a bit and just taking it for granted up until now out of necessity. When the Pygame window is created with the call to pygame.display.set_mode(), a Surface object is returned, which we have called screen up to this point. Now we will learn more about this elusive Surface class and what its capabilities are between now and the following chapter, and really in every chapter from now on. Admittedly, we’ve done some interesting work with vector (line-based) graphics up to this point, but now it’s time to study bitmaps, which is where you want to go for a good-looking game.

In this chapter we learn:

  • How to load a bitmap

  • How to draw a bitmap

  • How to make a ship orbit a planet

  • How to point an image in the right direction

Examining the Orbiting Spaceship Demo

The Orbiting Spaceship Demo shows how to use some of the math functions in Python to cause a spaceship to rotate around a planet, like the NASA Space Shuttle and ISS (International Space Station) orbits the Earth. The calculations are not actual acceleration-versus-gravity in nature, but just rotation of a point around a center point based on radius, but the end result looks the same and is good enough for a game. See Figure 6.1.

The Orbiting Spaceship Demo.

Figure 6.1. The Orbiting Spaceship Demo.

Using Bitmaps

In Pygame, a bitmap is called a Surface. The “screen” object that we have been using until now with very little explanation is itself a Surface object (returned by the pygame.display.set_mode() function). Rather than demonstrate bitmap programming with several examples, we’ll just get started on The Orbiting Spaceship Demo from the start and add to it as we go along.

Loading a Bitmap

First, let’s learn how to load a bitmap, starting with the background image for the chapter demo. Pygame can handle quite a few bitmap file types via the pygame.image.load() function:

  • JPG

  • PNG

  • GIF

  • BMP

  • PCX

  • TGA

  • TIF

  • LBM, PBM, PGM, PPM, XPM

Our orbiting spaceship demo must have a background image of space, but I suppose just a black background would work too. Or, how about drawing random dots all over the background? You could do that with pygame.gfxdraw.pixel()! The pygame.gfxdraw module is based on the SDL drawing functions, which offer a few more shapes than pygame.draw has. Now let’s just load a bitmap:

space = pygame.image.load("space.png").convert()

The trailing convert() function converts the image into the native color depth of the program window as an optimization. This really is required without exception. If you don’t convert an image at load time, then it will be converted every time you draw it!

There’s another variation of the function called convert_alpha() that you will want to use when loading foreground objects that have to be drawn with transparency. A TGA or PNG file can have alpha channel transparency in it, but some formats don’t support it (like the older BMP format). If you just want to use convert_alpha() every time, even with images without transparency, there’s no harm in doing that and it would be a bit more consistent.

Note

Loading a Bitmap

Be sure to use Surface.convert_alpha() when loading a bitmap with an alpha channel to tell Pygame you want to preserve the transparency in the image.

Drawing the Background

Drawing a bitmap is done with the Surface object, usually called screen, but it could be another Surface in memory, like a back buffer. We haven’t covered double buffered drawing yet, but first things first, we’re just now learning to draw a bitmap for the first time! To draw, use the Surface object. The Surface class has a function called blit() that draws a bitmap. The function name is short for “bit block transfer”, a method of drawing by copying a chunk of memory from one location to another: from system memory to video memory, in this case. To draw the space bitmap starting at the upper-left corner:

    screen.blit(space, (0,0))

This is assuming the screen (i.e., window) has been initialized to a size that is large enough to hold the bitmap. I have used a size of 800,600. Here’s our demo at this point (see Figure 6.2).

Drawing the background bitmap.

Figure 6.2. Drawing the background bitmap.

import random, math, pygame
from pygame.locals import *

#main program begins
pygame.init()
screen = pygame.display.set_mode((800,600))
pygame.display.set_caption("Orbit Demo")

#load bitmaps
space = pygame.image.load("space.png").convert()

#repeating loop
while True:
    for event in pygame.event.get():
        if event.type == QUIT:
            sys.exit()
    keys = pygame.key.get_pressed()
    if keys[K_ESCAPE]:
        sys.exit()

    #draw background
    screen.blit(space, (0,0))

    pygame.display.update()

Note

Drawing the background bitmap.

The online reference manual for Pygame covers the Surface class in detail at this location: http://pygame.org/docs/ref/surface.html#pygame.Surface. I recommend keeping your web browser pointed here while learning bitmap programming as there will be some features here that you may find interesting but that are not covered in this chapter.

Drawing the Planet

Now we’ll load and draw the planet image. Note that the artwork for these examples is found in the resource files for this and every chapter—a fact that is important now that we’re relying on asset files that have to be loaded for our examples to work properly. First, let’s load the planet before the while loop:

planet = pygame.image.load("planet2.png").convert_alpha()

Now, to draw the planet in this demo, we want to make it centered in the game window. Since the image dimensions could change (by someone editing the bitmap file), we would prefer to get the image dimensions in order to center it with code. This is better than “hard coding” the size of the bitmap. First, get the width and height of the bitmap using Surface.get_size(). Optionally, the width and height can be retrieved separately with Surface.get_width() and Surface.get_height(), respectively.

    width,height = planet.get_size()
    screen.blit(planet, (400-width/2,300-height/2))

In the code here, I have hard-coded the screen center but not the image. The screen’s dimensions could change, but most likely this is something I’ve decided upon before working on the game. But, it is easy enough to get the center of the screen as well because it is also a Surface object. Figure 6.3 shows the planet.

Drawing the planet bitmap transparently over the background.

Figure 6.3. Drawing the planet bitmap transparently over the background.

Drawing the Spaceship

There are two spaceship bitmaps included with this chapter if you would like to use them for your own sci-fi themed games. The ships are quite nice looking, drawn by artist Ronald Conley for a game called Starflight—The Lost Colony. This game is free to download and play at www.starflightgame.com. The artwork is copyrighted but may be shared for non-commercial use. If you want to borrow any of the artwork from Starflight (or any other source!) for your own games, please give credit to the artist and source website to avoid legal problems. This is completely illegal with a commercial game, of course! Let’s load the ship bitmap:

ship = pygame.image.load("freelance.png").convert_alpha()

The next line draws it, and the output is shown in Figure 6.4. Uh oh, the ship image is gigantic!

Drawing the spaceship bitmap.

Figure 6.4. Drawing the spaceship bitmap.

    screen.blit(ship, (50,50))

We could edit the bitmap with a graphic editor like Microsoft Paint, Paint.net, Gimp, or another similar tool. But, let’s see if we can just shrink down the ship image with code instead. In order to do this, we have to sort of cheat. Surface has no means to change the scale of an image, so we have to shrink the spaceship by some other means. There is a class called pygame.sprite.Sprite that excels at drawing and manipulating images for use in a game, but that’s a bit premature at this stage.

Digging around in the Pygame docs (http://pygame.org/docs/ref/index.html), it turns out there is a module called pygame.transform that will meet our needs. This module has a bunch of helpful functions for working with images in creative ways, like scaling, flipping, and other things. First, let’s look at pygame.transform.scale()—a fast scaling function that produces a quick scaled image but the pixels will look kind of chunky. Let’s try it. This function is added right after the image is loaded. If you call this function inside the while loop it will just keep scaling the same image over and over until it’s too tiny to see or too large to fit on the screen!

ship = pygame.image.load("freelance.png").convert_alpha()
width,height = ship.get_size()
ship = pygame.transform.scale(ship, (width//2, height//2))

Do you remember what the double division sign does in Python? It still does division, but it performs integer division rather than floating-point division. The result of this code is shown in Figure 6.5. It works! But, admittedly, the image is not very good.

Drawing the spaceship bitmap scaled by 50%.

Figure 6.5. Drawing the spaceship bitmap scaled by 50%.

So, let’s try a better scaling function. There is a variation called pygame.transform.smoothscale(). This function takes more time to change the scale of the image because it over-samples the pixels and smoothes them out using one of two algorithms. For shrinking an image, like what we want to do, the pixels are averaged. For enlarging an image, a bilinear filter is used (a sort of blocky anti-aliasing technique). See Figure 6.6. The difference should be pretty clear even on the printed page, but if you want to really see clearly how the smooth version improves the appearance of the image, you’ll need to open up the source code and run the program, changing the function call to see the difference.

Scaling the spaceship bitmap with a better technique.

Figure 6.6. Scaling the spaceship bitmap with a better technique.

ship = pygame.transform.smoothscale(ship, (width//2,height//2))

Orbiting the Planet

We have learned how to do basic bitmap drawing now, so we can use this new knowledge to make our demo. As you may recall from Chapter 5, “Math and Graphics: The Analog Clock Demo,” the trigonometry functions sine and cosine are used to draw circles and calculate trajectories. There’s a third function we haven’t used yet, tangent, that is useful in a similar but tangential way: pointing things in a certain direction. So, here’s what we want to do: make the spaceship orbit around the planet with sine and cosine, and then have it rotate so the front is always pointing in the direction it’s moving as it goes around the planet.

Orbiting

Let’s work on just getting the ship to orbit around the planet first. It will look kind of funny at first going around without changing orientation, although that is exactly how spaceships orbit planets. Making the ship’s nose always point in the direction it’s moving in orbit is totally not necessary, and not even realistic! But, for a video game, the usual player has certain expectations, and this is one of them—make the ship point in the direction it’s going. In some sci-fi movies, you might have noticed another thing they always do—keep the rocket engines firing constantly! That is also not done. Spaceships travel ballistically. This word is related to shooting a gun or cannon. Literally, a ship is fired and then it coasts along its path, just like a bullet or cannon ball. But, it just doesn’t look cool. If you want realistic, watch the movie 2001: A Space Odyssey. Stanley Kubric got it right! Well, he had to, with the late great Sir Arthur C. Clark advising him on the making of the movie!

Based on the math code we learned in the previous chapter, we can cause the spaceship to move around any point on the screen at a certain radius. We’ll set that point at the center of the screen and rotate the ship around in a circle at a radius of 250 (based on a window size of 800,600). Now, there’s something we have to remember here, or else run into problems: the position is at the upper-left corner of the image, not at the center! So, when the ship is orbiting around the planet, we have to account for the ship size and adjust the position so that it is moving from the center of the ship image, not the upper-left corner.

Here’s a variation of the Point class introduced way back in Chapter 1 with some improvements: X and Y properties and an override of __str__() so the class data can be printed out with pre-coded formatting. Not familiar with Python properties? Well, this is a good time to learn how they work. Create a pair of “get” and “set” methods that return and set a private class variable. Then, using the desired name of the property (such as x or y), use the property() function to assign the “get” and “set” methods associated with that class variable. The benefit of a property over just using a global variable is the ability to control its bounds while keeping the code clean.

class Point(object):
    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    #X property
    def getx(self):
        return self.__x
    def setx(self, x):
        self.__x = x
    x = property(getx, setx)

    #Y property
    def gety(self):
        return self.__y
    def sety(self, y):
        self.__y = y
    y = property(gety, sety)

    def __str__(self):
        return "{X:" + "{:.0f}".format(self.__x) + 
            ",Y:" + "{:.0f}".format(self.__y) + "}"

Putting the Point class to work, we need two instances in our program:

pos = Point(0,0)
old_pos = Point(0,0)

Next, here’s how we’ll move the ship in its “orbit”:

    angle = wrap_angle(angle - 0.1)
    pos.x = math.sin( math.radians(angle) ) * radius
    pos.y = math.cos( math.radians(angle) ) * radius

Here’s the code to draw the ship, taking into account the image size:

    width,height = ship.get_size()
    screen.blit(ship, (400+pos.x-width//2,300+pos.y-height//2))

The current version of the demo now with the ship orbiting is shown in Figure 6.7.

The spaceship is now rotating around the planet.

Figure 6.7. The spaceship is now rotating around the planet.

Rotating

So far, so good! The ship now will need to be rotated so that it’s pointing in the direction it’s moving around the planet. This is going to be a little tricky. There’s a little-known math function that’s pure magic! It’s called math.atan2(), and is a function that calculates arc-tangent with two parameters. We pass to this function two parameters: delta_y and delta_x. These delta values represent the difference between the X and Y properties of two coordinates on the screen. Almost as if by magic, the resulting value returned by math.atan2() is the angle to the target! All we do after that is rotate the image to that target angle and it will appear to point in the direction it’s moving.

Now for the tricky part. How do we know where the spaceship image is going to be in the next frame while the demo is running? By making a prediction! We can write code to predict where the ship will be in the future! Here’s the magic algorithm: Keep track of the last position of the ship; then use math.atan2() using the current and last position; then add 180 degrees to the resulting angle returned by math.atan2(). Do you see how that works? We get the angle to the previous position of the ship moments ago, and rotate the ship to that angle, but flip it around 180 degrees, completely backward from that angle, and presto, that is where the ship is heading! This is another one of those phenomenally awesome functions that is used all the time in game development for all sorts of things!

Let’s put math.atan2() to work. We’ll need a

    delta_x = ( pos.x - old_pos.x )
    delta_y = ( pos.y - old_pos.y )
    rangle = math.atan2(delta_y, delta_x)
    rangled = wrap_angle( -math.degrees(rangle) )

I’ve used the rangle variable to represent the radian angle calculated by math.atan2(), and the rangled variable is the angle converted to degrees and wrapped. Once the angle is available, then we can rotate the ship image to the desired angle. This requires the pygame.transform module again. It’s a pretty useful module, as you can see! The function we need is pygame.transform.rotate(), with the source image and desired rotation angle as parameters, and a new image returned. A scratch variable is used for the new image.

    scratch_ship = pygame.transform.rotate(ship, rangled)

Now we can draw the ship. But we can’t use the original ship image that hasn’t changed; we have to use the new image called scratch_ship, for both the position calculation and drawing. Note in the code that follows that the scratch_ship image is used to get the width and height. Surface.get_size() calculates the width and height of the rotated image in this case.

    width,height = scratch_ship.get_size()
    x = 400+pos.x-width//2
    y = 300+pos.y-height//2
    screen.blit(scratch_ship, (x,y))

After everything else, all we have to do is “remember” the position of the ship for use next time through the while loop (also called “the next frame” in game parlance).

    old_pos.x = pos.x
    old_pos.y = pos.y

Figure 6.8 shows the finished program. The complete code listing follows for reference (less the Point class, which was already shown in its entirety).

The spaceship rotates as it orbits the planet.

Figure 6.8. The spaceship rotates as it orbits the planet.

import sys, random, math, pygame
from pygame.locals import *

#Point class definition goes here . . .

#print_text function
def print_text(font, x, y, text, color=(255,255,255)):
    imgText = font.render(text, True, color)
    screen.blit(imgText, (x,y))

#wrap_angle function
def wrap_angle(angle):
    return angle % 360

#main program begins
pygame.init()
screen = pygame.display.set_mode((800,600))
pygame.display.set_caption("Orbit Demo")
font = pygame.font.Font(None, 18)

#load bitmaps
space = pygame.image.load("space.png").convert_alpha()
planet = pygame.image.load("planet2.png").convert_alpha()
ship = pygame.image.load("freelance.png").convert_alpha()
width,height = ship.get_size()
ship = pygame.transform.smoothscale(ship, (width//2, height//2))

radius = 250
angle = 0.0
pos = Point(0,0)
old_pos = Point(0,0)

#repeating loop
while True:
    for event in pygame.event.get():
        if event.type == QUIT:
            sys.exit()
    keys = pygame.key.get_pressed()
    if keys[K_ESCAPE]:
        sys.exit()

    #draw background
    screen.blit(space, (0,0))

    #draw planet
    width,height = planet.get_size()
    screen.blit(planet, (400-width/2,300-height/2))

    #move the ship
    angle = wrap_angle(angle - 0.1)
    pos.x = math.sin( math.radians(angle) ) * radius
    pos.y = math.cos( math.radians(angle) ) * radius

    #rotate the ship
    delta_x = ( pos.x - old_pos.x )
    delta_y = ( pos.y - old_pos.y )
    rangle = math.atan2(delta_y, delta_x)
    rangled = wrap_angle( -math.degrees(rangle) )
    scratch_ship = pygame.transform.rotate(ship, rangled)

    #draw the ship
    width,height = scratch_ship.get_size()
    x = 400+pos.x-width//2
    y = 300+pos.y-height//2
    screen.blit(scratch_ship, (x,y))

    print_text(font, 0, 0, "Orbit: " + "{:.0f}".format(angle))
    print_text(font, 0, 20, "Rotation: " + "{:.2f}".format(rangle))
    print_text(font, 0, 40, "Position: " + str(pos))
    print_text(font, 0, 60, "Old Pos: " + str(old_pos))

    pygame.display.update()

    #remember position
    old_pos.x = pos.x
    old_pos.y = pos.y

Summary

This chapter was a fun romp through more rocket science and the addition of the bitmap features of Pygame! There is just no comparison between vector shapes and bitmap graphics. As The Orbiting Spaceship Demo in this chapter demonstrated, we can do a lot with bitmaps and some interesting math functions, and we haven’t even touched upon sprite programming yet! That’s coming up in the very next chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.141.115