One of Pygame's most useful classes is the pygame.sprite
class. A close comparison to this class
would be sprites you have seen in Snap! Within this class we will be working closely with two of its
sub-classes:
pygame.sprite.Sprite
and
pygame.sprite.Group
.
To understand what these classes are and how they work, we will look at Obstacle.py
as an example.
But before we do that, let's look at some of the capabilities of the pygame.sprite.Sprite
and
pygame.sprite.Group
classes that make pygame.sprite
so special.
# Sprite class in pygame. This class can be used to initialize objects that move and appear on the screen. In this game, it will be used to implement car obstacles and the frog that the player controls
pygame.sprite.Sprite()
# a sprite update function like the update function described in the ttt class, this one will update the x and y position of the obstacle and frog based on how much it is supposed to move
pygame.sprite.Sprite().update()
# This function removes the sprite from all groups that it has been added to
pygame.sprite.Sprite().kill()
# This attribute the user to upload an image and use that to represent the sprite on the screen
pygame.sprite.Sprite().image
# This attribute, when used in conjunction with the Sprite.image attribute is responsible for the size of the sprite and its location on the screen
pygame.sprite.Sprite().rect
In pygame, an image can be loaded and rendered on top of a pygame.Rect object to represent things on the screen. The Sprite class is able to combine the Rect object, the image, and a load of other functions into one, easy to use class. Sprite even has a built-in update function to update any data for a particular object that the game may need to know about. In the case of the Obstacle class and the Frog class, the update function will include setting the position of the coordinates of the Sprite().rect class to new ones if it is moved from its previous position during the course of a game. The kill function is used to remove a sprite from groups it may be a part of. To understand what that means, let's look at the Group class.
# Group class that contains different sprite objects. Sprite groups can then be used to streamline things that may usually be done manually like drawing every sprite or checking to see if one sprite collides with any sprite in a a sprite group
pygame.sprite.Group()
# Adds a sprite to a sprite group
pygame.sprite.Group().add(sprite)
# Draws the image attribute of all sprites in a sprite class, location is given by the sprite's rect attribute
pygame.sprite.Group().draw()
# a function of pygame.Sprite that checks if a given sprite has collided with any thing in a sprite group
pygame.sprite.spritecollideany(sprite, group)
It is important to note that while all of these sprite and group functions can be created manually with simpler pygame building blocks, the advantages of these classes is that they have abstracted much of that work away for you. If the group class did not exist, then users would have to draw each obstacle sprite individually and check every obstacle sprite to see if it has collided with the frog sprite manually. While this is certainly possible to do. using the group class allows to perform these long checks with a very small amount of code.
If you haven't already, open up Obstacle.py and give it a look. Some of it might not make sense right away, and some of it might look completely foreign to you. That's ok! In this section we will talk about a few of the specific class attributes and functions in depth, others we will leave you to figure out by reading the comments in the start files. If you have questions, please go to lab or office hours to get them answered!
class Obstacle(pg.sprite.Sprite):
def __init__(self, lane_num, screen_width, screen_height, direction, speed, img_path, car_size):
pg.sprite.Sprite.__init__(self)
self.lane_num = lane_num
self.width = car_size[0]
self.height = car_size[1]
self.screen_width = screen_width
self.screen_height = screen_height
self.finished = False
self.direction = direction
if self.direction == 1:
self.image = pg.transform.scale(pg.image.load(img_path), car_size)
else:
self.image = pg.transform.flip(pg.transform.scale(pg.image.load(img_path), car_size), True, False)
self.rect = self.image.get_rect()
self.move_x = 0
self.speed = speed
The first thing to notice about this Obstacles class is that it is a sub class of the pygame.sprite.Sprite
class. If you remember back to the OOP lab, this means that the Obstacles class will have all the attributes of
the pygame.sprite.Sprite class, in addition to a few useful functions and attributes that we will add. Another
line of code that looks particularly interesting is the pg.sprite.Sprite.__init__(self)
code
underneath the def __init__(self ...)
line. What is happening here is that the init function we
defined will initialize all the attributes that we have created as part of the Obstacles subclass, but the
attributes in the original class has not yet been initialized. The line of code calling the init function fo the
Sprite class initializes all the attributes of that class, giving the Sprite class what it needs to function.
Some notable attributes that have been mentioned before are self.rect and self.image. If you take a look at the
image that we are using to represent pygame obstacles, you may notice that the size of it is quite large. First
off,to get an image into pygame, we must use the function pygame.image.load
. This loads the image
froma file path and returns a pygame.image
object. From there we can use pygame's transform
functions to scale, flip, or apply other transformations to the image.
For the sprite rect, we will use the image.get_rect function to give our sprite its dimensions and location.
These dimensions
will be based off the image dimensions and the location will be set to x = 0 and y = 0 by default. To change the
x, y coordinates of the rect, we have tp set self.rect.x
and self.rect.y
to our desired coordniates
Looking further down the code, we see two functions called move
and update
. Let's take
a closer look at these two fucntions.
def move(self):
self.move_x += self.speed * self.direction
if (self.rect.x > self.screen_width and self.direction == 1) or (self.rect.x < 0 and self.direction == -1):
self.finished = True
def update(self):
self.rect.x += self.move_x
self.move_x = 0
The purpose of the move function is to simply move the obstacle along its lane, towards the end of the screen.
The direction of and speed of this movement is determined by instance attributes that are assigned during
initialization. What you might notice is that Obstacle.move()
does not actually move the x
coordinate in self.rect.x
, but it changes another instance attribute called
self.move_x
. This attribute describes how much self.rect.x
will change by when the
update function is called.
In the self.update()
function, the position of the sprite is actually changed by changing
self.rect.x
. The attribute self.move_x
is set to 0 so that self.move()
will operate correctly in the next call of the function.