Video games are all about things whizzing around on the screen, some of them controlled by the user, and others controlled by the computer. In game development terms, moving “things” are called sprites. Every sprite needs to have several features:
-
- Visual representation: You need to be able to see a sprite on screen so it must have some sort of graphical representation.
- Position and size: A sprite must have a size and position on the screen.
- The ability to move/animate: A sprite should be able to move (if needed).
- The ability to recognize collisions: Games often involve many sprites moving around, and they are bound to bump into each other. A sprite often needs to know when it’s hit something else.
- Dynamic birth (and death): The game must be able to create and destroy sprites as needed.
- Be self-contained: To simplify design, all of the qualities above should be organized into a separate code unit than the rest of the game code. This way, your main game loop can be as simple as possible, and focus on the interactions between sprites.
After reading the list above you should immediately be thinking Object-Oriented Programming! Using a class to represent a sprite is an ideal way to encapsulate the visual representation, position, and size (as attributes), the ability to move and detect collisions (as methods), and to dynamically create sprite objects (using an __init__() method). Moreover, OOP is a perfect way to organize all of these details within sprite objects so that our main game loop can focus on the core game dynamics. We can take all of this goodness to an even higher level by storing our Sprite class in a separate module!
Creating a Sprite Subclass
Pygame includes a special class called Sprite that includes all of the basic instance variables and methods that a sprite object needs. However, instantiating a Sprite object itself is not very useful. Instead, we will define a class for each type of sprite object in our game that inherits from the pygame.sprite.Sprite class. We will then add custom instance variables and methods to make each kind of sprite in our game unique.
For good design, we’ll create a separate module to store our custom Sprite subclasses. Add the following Box class to a module called mySprites.py.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import pygame class Box(pygame.sprite.Sprite): '''Our Box class inherits from the Sprite class''' def __init__(self, screen): '''Initializer to set the image, position, and direction for a Box Sprite.''' # Call the parent __init__() method pygame.sprite.Sprite.__init__(self) # Keep track of the screen so we can call get_width() self.window = screen # Define a red Surface for our Box Sprite self.image = pygame.Surface((25, 25)) self.image = self.image.convert() self.image.fill((255, 0, 0)) # Define the position of our Box using it's rect self.rect = self.image.get_rect() self.rect.left = 0 self.rect.top = 200 self.direction = 10 def update(self): '''Automatically called in the Refresh section to update our Box Sprite's position.''' self.rect.left += self.direction if (self.rect.left < 0) or (self.rect.right > self.window.get_width()): self.direction = -self.direction |
On line 1 we import pygame because we need access to the Sprite class from it. Line 3 defines our custom sprite class called Box that inherits from the pygame.sprite.Sprite class.
You’ll notice that the initializer takes a parameter called screen. This is will be a reference to the game window created in the Display (IDEA) section of our main code. We will need this later in the update() method of our Box class to detect the right edge of the window. On line 9 we call the __init__() method from the parent Sprite class, as we have done before. This ensures that any instance variables inherited from the Sprite class are initialized before we start making customizations.
The image Instance Variable
The Sprite class includes an important instance variable (that we inherit) called image that must refer to a Surface object to visually represent our sprite. In this case, I’m using a simple 25×25 pixel red square as our representation. I could have also used an image from a file by replacing these three lines with something like this:
1 2 |
self.image = pygame.image.load("mrrao.gif") self.image = self.image.convert() |
Notice that the image variable is actually self.image. This helps clarify that image is not simply a local variable, but an instance variable of the current object. Recall that the scope of a local variable would only be within this method, but an instance variable is conveniently accessible in any method of the same class.
The rect Instance Variable
Another inherited instance variable of the Sprite class is called rect. After a Surface object has been assigned to self.image, we can call the method self.image.get_rect() to return a reference to a special object called a Rect. The Rect object has many instance variables to help control the positioning and size of the Surface image for our Box sprite:
top, bottom, left, right
Are single value rect attributes specifying the x or y position of the four sides of the rectangle.
centerx, centery
Are the x and y values of the centre of the rectangle.
size
An (x,y) tuple representing the size of the rect in pixels.
height, width
Are the height and width of the rectangle, respectively (in pixels).
The Rect object also includes a couple of useful methods that we’ll use later!
colliderect(rect2)
This method determines if the current rect object has collided with rect2.
kill()
This method deletes the sprite object from memory (the opposite of instantiation).
Back to our example code: on line 20 we call the image.get_rect() method to obtain a reference to the Rect object for our sprite. Then, to position our sprite we directly set the left and top attributes for our sprite’s image. The direction instance variable is unique to our Box class and specifies how many pixels and in what direction (+ for right, – for left) our sprite will move.
Finally, the update() method defines how our Box sprite moves each frame (~ 30 times/second). This method will be called automatically in the Refresh (ALTER) section. On each frame, I reposition the left edge of the rect 10 pixels to the right or left depending on which direction the Box sprite is currently moving in. If we hit the left or right edge of the window then I reverse the self.direction of travel for future frames. Notice in this method that we access the get_width() method for our game window by using our self.window instance variable.
Using a Sprite and Sprite Group
Now let’s look at how to use our Box sprite with mainline logic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# I - Import and Initialize import pygame import mySprites pygame.init() def main(): '''This function defines the 'mainline logic' for our game.''' # Display screen = pygame.display.set_mode((640, 480)) pygame.display.set_caption("Basic Sprite Demo") # Entities background = pygame.Surface(screen.get_size()) background = background.convert() background.fill((255, 255, 0)) screen.blit(background, (0,0)) # create a Box sprite object from our mySprites module box = mySprites.Box(screen) # add our Box sprite to an OrderedUpdates Sprite Group to keep Refresh section simple allSprites = pygame.sprite.OrderedUpdates(box) # ACTION # Assign clock = pygame.time.Clock() keepGoing = True # Loop while keepGoing: # Time clock.tick(30) # Events for event in pygame.event.get(): if event.type == pygame.QUIT: keepGoing = False # Refresh screen allSprites.clear(screen, background) # The next line calls the update() method for any sprites in the allSprites group. allSprites.update() allSprites.draw(screen) pygame.display.flip() # Close the game window pygame.quit() # Call the main function main() |
On line 3 we import our mySprites module to have access to our Box sprite class. The rest of the code is similar to what we have seen before until the last few lines of the Entities (IDEA) section — sprites should always be instantiated in this section. On line 20 we instantiate a Box sprite object from our module. Remember that Python is case-sensitive, so the variable name box and class name Box are two different things. When creating a Box sprite object we pass the screen variable which refers to the game window. Again, this will allow our Box sprite code to use the get_width() method for our game window.
Sprite objects are not meant to act on their own. If you want a Pygame sprite to do anything, you have to put it in an OrderedUpdates Sprite Group. A Sprite Group is a special object (think of it as a list) that lets you control one or more sprites at the same time. Once you have instantiated all of your Sprite objects (in this example we only have one), you pass them as arguments to pygame.sprite.OrderedUpdates(), as shown on line 22. Once you have an OrderedUpdates sprite group, you won’t have to blit them to the screen individually as we have been doing with Surface objects up to this point.
Have a look at the Refresh (ALTER) section. Our allSprites group has a special method called clear() that erases all of the sprites that were drawn in the previous frame and replaces them with an associated part of the background. As shown on line 42 the clear() method requires a reference to the game window (screen) and background Surface as arguments.
The allSprites group also has a method called update() that calls the update() method of every sprite in the group. On line 44, this one line of code updates the positions of all sprites in our allSprites group! Finally, our allSprites group’s draw() method automatically blits each of the sprites in our group to the screen in the appropriate positions; i.e., according to each sprite’s rect instance variables. Note that we still need to flip() the display to tell pygame that you’re ready to update the actual graphics hardware.
As an object-oriented programmer, I’m sure you can appreciate the benefits of defining classes and objects for our sprites. At this point, however, the sprite version of our “red box” animation isn’t much shorter than the old version (without sprites). The real advantage of defining Sprite subclasses becomes clear when you have a lot of them zipping around and crashing into each other. You’ll appreciate this in your exercises today!
You Try!
- Start a new page in your Learning Journal titled “4-9 Sprites and Sprite Groups”. Carefully read the notes above and in your own words summarize the key ideas from each section.