Unit 9 - Introduction to Sprites

Our games need support for handling objects that collide. Balls bouncing off paddles, laser beams hitting aliens, or our favorite character collecting a coin. All these examples require collision detection.

The Pygame library has support for sprites. A sprite is a two dimensional image that is part of the larger graphical scene. Typically a sprite will be some kind of object in the scene that will be interacted with like a car, frog, or little plumber guy.

Originally, video game consoles had built-in hardware support for sprites. Now this specialized hardware support is no longer needed, but we still use the term “sprite.”

9.1 Basic Sprites and Collisions

Let's step through an example program that uses sprites. This example shows how to create a screen of black blocks, and collect them using a red block controlled by the mouse. The program keeps “score” on how many blocks have been collected.

The first few lines of our program start off like other games we've done:

import pygame

import random


# Define some colors

BLACK = ( 0, 0, 0)

WHITE = (255, 255, 255)

RED = (255, 0, 0)

The pygame library is imported for sprite support. The random library is imported for the random placement of blocks. The definition of colors is standard; there is nothing new in this example yet.

class Block(pygame.sprite.Sprite):

"""

This class represents the ball.

It derives from the "Sprite" class in Pygame.

"""

The first line starts the definition of the Block class. Note that this class is a child class of the Sprite class, the "parameter" being passed into the class. The pygame.sprite. specifies the library and package, which will be discussed later. All the default functionality of the Sprite class will now be a part of the Block class.

def __init__(self, color, width, height):

""" Constructor. Pass in the color of the block, and its x and y position. """


# Call the parent class (Sprite) constructor

pygame.sprite.Sprite.__init__(self)

The constructor for the Block class takes in a parameter for self just like any other constructor. It also takes in parameters that define the object's color, height, and width.

It is important to call the parent class constructor in Sprite to allow sprites to initialize. This is done in the __init__ of the class being created.

# Create an image of the block, and fill it with a color.

# This could also be an image loaded from the disk.

self.image = pygame.Surface([width, height])

self.image.fill(color)

The 2 lines above create the image that will eventually appear on the screen. The first of these creates a blank image. The next fills it with black (the value of "color"). If the program needs something other than a black square, these are the lines of code to modify.

For example, look at the code below:

def __init__(self, color, width, height):

"""

Ellipse Constructor. Pass in the color of the ellipse,

and its size

"""

# Call the parent class (Sprite) constructor

pygame.sprite.Sprite.__init__(self)


# Set the background color and set it to be transparent

self.image = pygame.Surface([width, height])

self.image.fill(WHITE)

self.image.set_colorkey(WHITE)


# Draw the ellipse to the surface created above

pygame.draw.ellipse(self.image, color, [0, 0, width, height])

If the code above was substituted, then everything would be in the form of ellipses. The last line draws the ellipse to the surface created in the lines above it.

def __init__(self):

""" Graphic Sprite Constructor. """


# Call the parent class (Sprite) constructor

pygame.sprite.Sprite.__init__(self)


# Load the image

self.image = pygame.image.load("player.png").convert()


# Set our transparent color

self.image.set_colorkey(WHITE)

If instead a bit-mapped graphic is desired, substituting the lines of code above will load a graphic and set white to the transparent background color. In this case, the dimensions of the sprite will automatically be set to the graphic dimensions, and it would no longer be necessary to pass them in.

There is one more important line that we need in our constructor, no matter what kind of sprite we have:

# Fetch the rectangle object that has the dimensions of the image.

# Update the position of this object by setting the values

# of rect.x and rect.y

self.rect = self.image.get_rect()

The attribute rect is a variable that is an instance of the Rect class that Pygame provides. The rectangle represents the dimensions of the sprite. This rectangle class has attributes for x and y that may be set. Pygame will draw the sprite where the x and y attributes are. So to move this sprite, a programmer needs to set mySpriteRef.rect.x and mySpriteRef.rect.y where mySpriteRef is the variable that points to the sprite.

We are done with the Block class. Time to move on to the initialization code.

# Initialize Pygame

pygame.init()


# Set the height and width of the screen

screen_width = 700

screen_height = 400

screen = pygame.display.set_mode([screen_width, screen_height])

The code above initializes Pygame and creates a window for the game. There is nothing new here from other Pygame programs.

# This is a list of 'sprites.' Each block in the program is

# added to this list.

# The list is managed by a class called 'Group.'

block_list = pygame.sprite.Group()


# This is a list of every sprite.

# All blocks and the player block as well.

all_sprites_list = pygame.sprite.Group()

A major advantage of working with sprites is the ability to work with them in groups. We can draw and move all the sprites with one command if they are in a group. We can also check for sprite collisions against an entire group.

The above code creates two lists. The variable all_sprites_list will contain every sprite in the game. This list will be used to draw all the sprites. The variable block_list holds each object that the player can collide with. In this example it will include every object in the game but the player. We don't want the player in this list because when we check for the player colliding with objects in the block_list, Pygame will go ahead and always return the player as colliding if it is part of that list.

for i in range(50):

# This represents a block

block = Block(BLACK, 20, 15)


# Set a random location for the block

block.rect.x = random.randrange(screen_width)

block.rect.y = random.randrange(screen_height)


# Add the block to the list of objects

block_list.add(block)

all_sprites_list.add(block)

The loop, starting on the first line, adds 50 black sprite blocks to the screen. Next, a new block is created, setting the color, the width, and the height. The next 2 lines set the coordinates for where this object will appear. Next I'm adding the block to the list of blocks the player can collide with. Finally, I add it to the list of all blocks.

# Create a RED player block

player = Block(RED, 20, 15)

all_sprites_list.add(player)

The previous lines set up the player for our game. It creates a red block that will eventually function as the player. This block is added to the all_sprites_list on the next line so it can be drawn, but not the block_list.

# Loop until the user clicks the close button.

done = False


# Used to manage how fast the screen updates

clock = pygame.time.Clock()


score = 0


# -------- Main Program Loop -----------

while not done:

for event in pygame.event.get():

if event.type == pygame.QUIT:

done = True


# Clear the screen

screen.fill(WHITE)

The code above is a standard program loop first introduced previously. Including initializing the score variable to 0.

# Get the current mouse position. This returns the position

# as a list of two numbers.

pos = pygame.mouse.get_pos()


# Fetch the x and y out of the list,

# just like we'd fetch letters out of a string.

# Set the player object to the mouse location

player.rect.x = pos[0]

player.rect.y = pos[1]

The code above fetches the mouse position similar to other Pygame programs discussed before. The important new part is where the rectangle containing the sprite is moved to a new location. Remember this rect was created in the __init__ and this code won't work without that line.

# See if the player block has collided with anything.

blocks_hit_list = pygame.sprite.spritecollide(player, block_list, True)

This line of code takes the sprite referenced by player and checks it against all sprites in block_list. The code returns a list of sprites that overlap. If there are no overlapping sprites, it returns an empty list. The boolean True will remove the colliding sprites from the list. If it is set to False the sprites will not be removed.

# Check the list of collisions.

for block in blocks_hit_list:

score +=1

print(score)

This loops for each sprite in the collision list. If there are sprites in that list, increase the score for each collision. Then print the score to the screen. Note that the print will not print the score to the main window with the sprites, but the console window instead.

# Draw all the spites

all_sprites_list.draw(screen)

The Group class that all_sprites_list is a member of has a method called draw. This method loops through each sprite in the list and calls that sprite's draw method. This means that with only one line of code, a program can cause every sprite in the all_sprites_list to draw.

# Limit to 60 frames per second

clock.tick(60)


# Go ahead and update the screen with what we've drawn.

pygame.display.flip()


pygame.quit()

The last few lines flip the screen, and calls the quit method when the main loop is done.

9.2 Moving Sprites

In the example so far, only the player sprite moves. How could a program cause all the sprites to move? This can be done easily; just two steps are required.

The first step is to add a new method to the Block class. This new method is called update. The update function will be called automatically when update is called for the entire list.

Put this in the sprite:

def update(self):

""" Called each frame. """


# Move block down one pixel

self.rect.y += 1

Put this in the main program loop:

# Call the update() method for all blocks in the block_list

block_list.update()

The code isn't perfect because the blocks fall off the screen and do not reappear. This code will improve the update function so that the blocks will reappear up top.

def update(self):

# Move the block down one pixel

self.rect.y += 1


if self.rect.y > screen_height:

self.rect.y = random.randrange(-100, -10)

self.rect.x = random.randrange(0, screen_width)

If the program should reset blocks that are collected to the top of the screen, the sprite can be changed with the following code:

def reset_pos(self):

""" Reset position to the top of the screen, at a random x location.

Called by update() or the main program loop if there is a collision.

"""

self.rect.y = random.randrange(-300, -20)

self.rect.x = random.randrange(0, screen_width)


def update(self):

""" Called each frame. """


# Move block down one pixel

self.rect.y += 1


# If block is too far down, reset to top of screen.

if self.rect.y > 410:

self.reset_pos()

Rather than destroying the blocks when the collision occurs, the program may instead call the reset_pos function and the block will move to the top of the screen ready to be collected.

# See if the player block has collided with anything.

# Note the "remove" parameter is set to False for this

blocks_hit_list = pygame.sprite.spritecollide(player, block_list, False)


# Check the list of collisions.

for block in blocks_hit_list:

score += 1

print(score)


# Reset block to the top of the screen to fall again.

block.reset_pos()

13.3 The Game Class

Previously we introduced functions. At that time we talked about an option to use a main function. As programs get large this technique helps us avoid problems that can come from having a lot of code to sort through. Our programs aren't quite that large yet. However I know some people like to organize things properly from the start.

For those people in that camp, here's another optional technique to organize your code. (If you aren't in that camp, you can skip this section and circle back later when your programs get too large.)