Using Sprites in Pygame

Sprites are one of the most useful, but least understood, parts of Pygame. This document will, hopefully, teach you enough about sprites to simplify your code tremendously.

This document assumes that you're familiar with Pygame's Rect and Surface objects. Concepts about Rects and Surface will be given only cursory mention here, so be sure to check the documentation if you don't understand something. I believe strongly in code examples and analysis as a way to learn things, so most of this tutorial will be Python, not English.

(This document applies to Pygame 1.5.x and Pygame 1.6. After that, the sprite library has changed in some compatible and subtle but powerful ways. This tutorial will be updated to reflect those changes when the next version of Pygame is released.)

What is a Sprite?

A "sprite", at least in Pygame terms, is an object that contains both an image (a Surface), and a location at which to draw that image (a Rect). The term "sprite" is actually a holdover from older display systems that did such manipulations directly in hardware. Computers today are fast enough to not need this, but it is still convenient to work with this abstraction.

Sprites are especially good in object-oriented languages like Python, where you can have a standard sprite interface, and classes that extend that interface as specific sprites. Which is exactly what Pygame does.

pygame.sprite.Sprite

pygame.sprite.Sprite is the class that Pygame uses to represent sprites. However, you will rarely instantiate this class directly. Instead, you'll extend it with your own user-created class, which in turn acts like whatever kind of object you want.

Sprites have two important instance variables, self.image and self.rect. self.image is a Surface, which is the current image that will be displayed. self.rect is the location at which this image will be displayed when the sprite is drawn to the screen.

Sprites also have one important instance method, self.update. This method can take any arguments, but (for reasons which will be explained when we get to sprite groups) the more sprite subclasses you have that use the same arguments to update, the better.

This is probably a little confusing to just read, so the following is a step by step example of making a sprite.

Making a Simple Sprite

This sprite is very simple; it creates a 15x15 box and fills it with a color.

Box: boxes.py

import pygame

class Box(pygame.sprite.Sprite):
    def __init__(self, color, initial_position):

        # All sprite classes should extend pygame.sprite.Sprite. This
        # gives you several important internal methods that you probably
        # don't need or want to write yourself. Even if you do rewrite
        # the internal methods, you should extend Sprite, so things like
        # isinstance(obj, pygame.sprite.Sprite) return true on it.
        pygame.sprite.Sprite.__init__(self)
      
        # Create the image that will be displayed and fill it with the
        # right color.
        self.image = pygame.Surface([15, 15])
        self.image.fill(color)

        # Make our top-left corner the passed-in location.
        self.rect = self.image.get_rect()
        self.rect.topleft = initial_position

So now we've got a sprite. The following code creates an instance of that sprite, and displays it on the screen.

showbox.py

A red box
import pygame
from pygame.locals import *
from boxes import Box

pygame.init()
screen = pygame.display.set_mode([150, 150])
b = Box([255, 0, 0], [0, 0]) # Make the box red in the top left
screen.blit(b.image, b.rect)
pygame.display.update()
while pygame.event.poll().type != KEYDOWN: pygame.time.delay(10)

At the moment sprites probably aren't looking too great; it's a lot more code than just using surfaces directly. That's true. However, sprites start to shine when you plan to use a lot of them:

showboxes.py

Three boxes
import pygame
from pygame.locals import *
from boxes import Box

pygame.init()
boxes = []
for color, location in [([255, 0, 0], [0, 0]),
                        ([0, 255, 0], [0, 60]),
                        ([0, 0, 255], [0, 120])]:
    boxes.append(Box(color, location))

screen = pygame.display.set_mode([150, 150])
for b in boxes: screen.blit(b.image, b.rect)
pygame.display.update()
while pygame.event.poll().type != KEYDOWN: pygame.time.delay(10)

We've gotten a good deal of code reuse out of our Box object now, and our screen update is a one-line loop, despite it updating three things. In fact, it can be made even more efficient with:

rectlist = [screen.blit(b.image, b.rect) for b in boxes]
pygame.display.update(rectlist)

The loop itself won't be any slower than the for loop, and we only update the parts of the screen that were changed. Since updating the screen surface can take a long time, this is a big win.

While this can be done with surfaces, the advantages of sprites are beginning to emerge even in this simple example.

Animation

Now for something that would be a complete pain for surfaces to do — moving the boxes up and down. This will introduce the purpose of the update method.

UpDownBox: boxes.py

import pygame

class UpDownBox(pygame.sprite.Sprite):
    def __init__(self, color, initial_position):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface([15, 15])
        self.image.fill(color)
        self.rect = self.image.get_rect()
        self.rect.topleft = initial_position
        self.going_down = True # Start going downwards
        self.next_update_time = 0 # update() hasn't been called yet.

    def update(self, current_time, bottom):
        # Update every 10 milliseconds = 1/100th of a second.
        if self.next_update_time < current_time:

            # If we're at the top or bottom of the screen, switch directions.
            if self.rect.bottom == bottom - 1: self.going_down = False
            elif self.rect.top == 0: self.going_down = True
     
            # Move our position up or down by one pixel
            if self.going_down: self.rect.top += 1
            else: self.rect.top -= 1

            self.next_update_time = current_time + 10

This is the new sprite object. Now, rather than just sitting there, we call the update method every cycle, with the current time and the bottom of the screen (which is its vertical size).

showmovingboxes.py

Moving boxes
import pygame
from pygame.locals import *
from boxes import UpDownBox

pygame.init()
boxes = []
for color, location in [([255, 0, 0], [0, 0]),
                        ([0, 255, 0], [60, 60]),
                        ([0, 0, 255], [120, 120])]:
    boxes.append(UpDownBox(color, location))

screen = pygame.display.set_mode([150, 150])
while pygame.event.poll().type != KEYDOWN:
    screen.fill([0, 0, 0]) # blank the screen.

    # Save time by only calling this once
    time = pygame.time.get_ticks() 
    for b in boxes:
      b.update(time, 150)
      screen.blit(b.image, b.rect)

    pygame.display.update()

Running this, you'll see the boxes start moving up and down the screen. Trying to do this directly with surfaces and rectangles would be much more confusing --- the most convenient way would be a list of (image, rectangle) tuples and a function to update the data in the rectangle based on the time. But at that point, you've just reimplemented sprites in a less object-oriented, harder to read, and probably slower manner.

Sprite Groups

As shown above, the benefits of sprites appear when you've got a lot of them. In the examples, the sprites are stored in a list, which is convenient, but not really optimal. In addition to sprites, Pygame offers a set of classes known as sprite groups, which are easy to use lists of sprites. Sprite groups have 5 important methods --- add, remove, draw, update, and sprites.

add and remove add or remove a sprite, list of sprites, or all the sprites in another sprite group, to the group. So, you can do add(sprite1), add([sprite1, sprite2]), or add(group_with_1and2).

update takes any arguments, and passes them to the update method of each sprite in it. This is why it's a good idea to have your sprites take the same arguments to update, because then you can take advantage of the group's update rather than call each individual sprite's method.

sprites returns a list of sprites in the sprite group. In Pygame 1.7 and greater, sprite groups are iterators themselves, so you can use for sprite in group directly, rather than for sprite in group.sprites().

In addition, there's a new function of the Sprite object that's important to mention here — kill (which takes no arguments) will remove a sprite from all the sprite groups it's in.

Some of the more advanced sprite groups have a draw function that takes a single surface as an argument, and draws all the sprites in it to that surface, at the location specified by their rectangles.

Rewriting the moving box example in terms of sprite groups:

movingwithgroups.py

import pygame
from pygame.locals import *
from boxes import UpDownBox

pygame.init()
boxes = pygame.sprite.Group()
for color, location in [([255, 0, 0], [0, 0]),
                        ([0, 255, 0], [60, 60]),
                        ([0, 0, 255], [120, 120])]:
    boxes.add(UpDownBox(color, location))

screen = pygame.display.set_mode([150, 150])
while pygame.event.poll().type != KEYDOWN:
    screen.fill([0, 0, 0]) # blank the screen.
    boxes.update(pygame.time.get_ticks(), 150)
    for b in boxes.sprites(): screen.blit(b.image, b.rect)
    pygame.display.update()

Note how:

The sprite group code is also highly optimized, so it is often faster than a list. Insertions and deletions are both constant-time operations, and the code avoids memory dereferences as much as possible in the inner loops. This comes at the expensive of some readability, but you don't need to read sprite.py to use it.

I mentioned before that it's a good idea to have as many sprites as possible take the same arguments to their update method, and now you can see why. If sprites share the same update method, they can share the same sprite group, and use a single update call. While this doesn't make the code anything faster, it makes it a lot shorter.

RenderUpdates

If you're clever, you're starting to consider the potential of sprite groups right now. There are two key inefficiencies in the above example — first, we clear the whole screen, and second we update the whole screen. But since we have information about rectangles and surfaces, we should be able to only calculate the changed areas, right?

Pygame offers many different sprite groups. Group, shown above, is the simplest one. We'll skip all the ones in the middle (you should check them out later, though!) and head straight for RenderUpdates, the most featureful. RenderUpdates does exactly what is described above: It lets you update just the areas on the screen that are changed. It's also capable of clearing the screen (via blitting a background image in place of the sprites) in a similar manner.

renderupdates.py

import pygame
from pygame.locals import *
from boxes import UpDownBox

pygame.init()
boxes = pygame.sprite.RenderUpdates()
for color, location in [([255, 0, 0], [0, 0]),
		        ([0, 255, 0], [60, 60]),
                        ([0, 0, 255], [120, 120])]:
    boxes.add(UpDownBox(color, location))

screen = pygame.display.set_mode([150, 150])
background = pygame.Surface([150, 150])
background.fill([0, 0, 0])
screen.blit(background, [0, 0])
while pygame.event.poll().type != KEYDOWN:
    boxes.update(pygame.time.get_ticks(), 150)
    rectlist = boxes.draw(screen)
    pygame.display.update(rectlist)
    pygame.time.delay(10)
    boxes.clear(screen, background)

Now, the screen will be cleared automatically every cycle, and only the necessary areas of the screen will be updated. Although in this example we clear to a plain black background, the clear method can take any surface as its second argument, and so can clear to any image you want.

Droppings from a forgotten clear()

Remember to call clear, otherwise your sprites will leave droppings behind.

Like the rest of the sprite groups, RenderUpdates is highly optimized. It will automatically calculate overlapping rectangles in draw and clear and only update them once.

Ordering Sprites

One important thing to remember about sprite groups is that they are not ordered. Sprites will be drawn to the screen in no particular order. If you need ordering, you have two choices. First, if you only need partial ordering (e.g. sprites A and B must always be under C and D, but the order of A and B doesn't matter), you can use multiple sprite groups, and then draw them in order from lowest to highest (e.g. group_ab, then group_cd). Secondly, you can implement your own sprite group that has ordering. An example of how to do this is presented later in this tutorial.

Thinking in Sprites

The hardest part of sprites, like the hardest part of any program, isn't just the API and syntax. It's designing your program around them, in a manner that makes it easy to write, debug, and extend later. Unfortunately it's hard to show exactly how to do this in a tutorial. But some general advice that I've found useful is below.

Use Sprites..

Don't Use Sprites..

Sprite Tricks

Singleton Images

Often you'll want to have a lot of sprites of the same class on the screen at the same time, using an image that comes from a file. What you don't want to do is this:

class MySprite(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load("image.png")

If you do this, every copy of your sprite will load the image from the file, and you'll have a copy of the image for each sprite, eating up memory and loading time. Instead, you'll be better off with this:

class MySprite(pygame.sprite.Sprite):
    image = None

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)

        if MySprite.image is None:
            # This is the first time this class has been instantiated.
            # So, load the image for this and all subsequence instances.
            MySprite.image = pygame.image.load("image.png")

        self.image = MySprite.image

This loads the image once the first time the class is instantiated, and every other instance uses the same image as the first.

Even if all your sprites use the same base image, but change it later, you can use this trick to avoid loading the image from disk and decompressing it each time you make a new sprite.

class MySprite(pygame.sprite.Sprite):
    image = None

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        if MySprite.image is None:
            MySprite.image = pygame.image.load("image.png")

        # Converting an image to its own depth and masks gives us the
        # same image. Some preliminary benchmarks show that using
        # convert is actually faster than making a new surface and
        # blitting to it in separate steps. You might want to verify
        # this yourself.
        self.image = MySprite.image.convert(MySprite.image)

Internals

Sprites

As mentioned before, Sprites are meant to be extended. As they stand, they're pretty useless, so you'll need to wrap them in something a bit more useful. However, thanks to good design, you don't need to know much about the internals of Sprites to extend the class. Instead, you just need to override __init__ and update. However, if you do need to do low-level work with sprites, there are a few more details.

The two important functions are add_internal and remove_internal. These are called when the sprite is added or removed from a sprite group, with one argument, being the sprite group. Your sprites will need to track what groups they are in, so when self.kill() is called it can tell the groups to remove them.

You shouldn't find yourself rewriting the sprite class internals. Sprite is simple enough that you can build everything you need off of it, rather than parallel to it.

Groups

Sprite groups are more complicated. For speed reasons (and some lack of foresight), groups have some internal abstraction violations that make them a little harder to extend. But it's possible, and it's not too terrible.

Sprite groups (with the exception of GroupSingle) use a dictionary. self._spritedict, to keep track of all the sprites in them. This means that addition and deletion of sprites are fast (constant-time), but the tradeoff is that there's no way to order the sprites. Like sprites, groups need add_internal and remove_internal functions, that take a single arguing, being a sprite. The key difference between add and remove and add_internal and remove_internal is that the former can take lists or sprite groups, while the latter will only take a single sprite.

Real Examples

All of the above is probably best explained by examples. We're going to construct a sprite and a sprite group as an example, from the ground up.

AnimatedSprite

The Idea

The code above shows examples of a sprite's rect attribute changing, but just as often you might want to change the picture. The following sprite subclass cycles through a list of frames, at a given rate.

The Code

Pretty straightfoward. Dividing 1000 by the fps gives us milliseconds per frame, which we can then use to figure out how long to wait between frame switches. Once we hit the end of the frames, go back to the first one.

animatedsprite.py

import pygame

class AnimatedSprite(pygame.sprite.Sprite):
    def __init__(self, images, fps = 10):
        pygame.sprite.Sprite.__init__(self)
        self._images = images

        # Track the time we started, and the time between updates.
        # Then we can figure out when we have to switch the image.
        self._start = pygame.time.get_ticks()
        self._delay = 1000 / fps
        self._last_update = 0
        self._frame = 0

        # Call update to set our first image.
        self.update(pygame.time.get_ticks())

    def update(self, t):
        # Note that this doesn't work if it's been more that self._delay
        # time between calls to update(); we only update the image once
        # then, but it really should be updated twice.

        if t - self._last_update > self._delay:
            self._frame += 1
            if self._frame >= len(self._images): self._frame = 0
            self.image = self._images[self._frame]
            self._last_update = t

OrderedRenderUpdates

The Idea

Sprite groups normally aren't ordered (although using multiple groups you can order them a little). This can be really annoying in some cases. The following sprite group will draw sprites in the order they were added to the group (so more recently added sprites are on top).

The code is basically the same as RenderUpdates, but keeps a list as well as a dictionary of sprites. Ostensibly, insertions are constant-time, deletions are linear time, and drawing is linear time. In reality, sometimes insertions will be linear time too, as Python has to extend the array's memory buffer.

The Code

This code is close to the RenderUpdates code in Pygame, but unoptimized for readability. Because it's from RenderClear, there's some stuff that might not make sense at first, like the lostsprites list. If you're curious about that, check out the code for RenderClear. The main point of this example is show how to use the add and remove functions.

oru.py

from pygame.sprite import RenderClear

# This class keeps an ordered list of sprites in addition to the dict,
# so we can draw in the order the sprites were added.
class OrderedRenderUpdates(RenderClear):
  def __init__(self, group = ()):
    self.spritelist = []
    RenderClear.__init__(self, group)

  # Some quick benchmarks show that [:] is the fastest way to get a
  # shallow copy of a list.
  def sprites(self):
    return self.spritelist[:]

  # This is kind of a wart -- the actual RenderUpdates class doesn't
  # use add_internal in its add method, so just overriding
  # add_internal won't work.
  def add(self, sprite):
    if hasattr(sprite, '_spritegroup'):
      for sprite in sprite.sprites():
        if sprite not in self.spritedict:
          self.add_internal(sprite)
          sprite.add_internal(self) 
    else:
      try: len(sprite)
      except (TypeError, AttributeError):
        if sprite not in self.spritedict:
          self.add_internal(sprite)
          sprite.add_internal(self) 
      else:
        for sprite in sprite:
          if sprite not in self.spritedict:
            self.add_internal(sprite)
            sprite.add_internal(self) 

  def add_internal(self, sprite):
    RenderClear.add_internal(self, sprite)
    self.spritelist.append(sprite)

  def remove_internal(self, sprite):
    RenderClear.remove_internal(self, sprite)
    self.spritelist.remove(sprite)

  def draw(self, surface):
    spritelist = self.spritelist
    spritedict = self.spritedict
    surface_blit = surface.blit
    dirty = self.lostsprites
    self.lostsprites = []
    dirty_append = dirty.append
    for s in spritelist:
      r = spritedict[s]
      newrect = surface_blit(s.image, s.rect)
      if r is 0:
        dirty_append(newrect)
      else:
        if newrect.colliderect(r):
          dirty_append(newrect.union(r))
        else:
          dirty_append(newrect)
      spritedict[s] = newrect
    return dirty

DirtySprite and DirtyUpdates

The Idea

RenderUpdates is great, but not perfect. It doesn't know anything about how each sprite works, so although it can optimize the drawing areas, it doesn't know if a sprite has changed or not. If it knew this, it could avoid drawing the sprites that haven't changed.

The bad news is, we can't do this with Sprite as it stands. The good news is, the changes needed to make Sprite allow this are small.

The Code

I split the code up into two sections, DirtySprite and RenderDirty. The first is a new sprite class, with a new attribute, self.dirty. Whenever you change the image or rectangle, you should set self.dirty to True. The draw method of RenderDirty then only needs to draw the sprites that have changed.

This particular implemention of RenderDirty is from Pete Shinners, the author of pygame. I undid some of the optimization, to make it more readable.

This code doesn't totally work. If two sprites are overlapping, and one has updated but the other hasn't, this can give in strange results.

dirty.py

from pygame.sprite import Sprite, RenderUpdates

class DirtySprite(Sprite):
  def __init__(self):
    Sprite.__init__(self)
    self.dirty = True

class RenderDirty(RenderUpdates):
  def draw(self, surface):
    dirty = self.lostsprites
    self.lostsprites = []
    for s, r in self.spritedict.items():
      if not s.dirty:
        continue

      s.dirty = False
      newrect = surface.blit(s.image, s.rect)
      if r is 0:
        dirty.append(newrect)
      else:
        if newrect.colliderect(r):
          dirty.append(newrect.union(r))
        else:
          dirty.append(newrect)
          dirty.append(r)
      spritedict[s] = newrect
    return dirty

TODO

Further Reading

Feedback

If you have advice for this tutorial, or didn't understand something, contact me at piman (at) sacredchao.net.

Thanks

License

Copyright ©2004 Joe Wreschnig. This document is released under the GNU GPL version 2, as published by the Free Software Foundation. The code examples provided are released into the public domain.