Translations of this page:

Step 015 - Explosions, healthbars and gravity

code discussion

screenshot from part2step015

In the code example below (see source code) you can create and kill sprites by mouse click. For each kill there is an explosion effect: a sound plays and a random amount of fragments is flying around. The fragments are subject to a gravity force.

Also note: Each Bird sprite has it's own livebar sprite (a green bar to display the remaining hitpoints). Each bird create it's own hitbar inside the init method !

class Bird(pygame.sprite.Sprite):
   # ..
   def __init__(self, pos):
      #..
      Livebar(self) #create a Livebar for this Bird. self is the Bird sprite as parameter

Each Livebar get his “boss” as parameter when created:

class Livebar(pygame.sprite.Sprite):
        #..
        def __init__(self, boss):
            #.. 
            self.boss = boss # the boss is the bird sprite

The main purpose of the code example is to demonstrate the difference between a bad and a good sprite collision detection. As each Bird1) sprite moves around the screen, it is checked (in the mainloop) for collision with another Bird sprite. (A even more clever way will be demonstrated in the next steps).

good

The efficient way to do so was friendly showed me by Gummbum from the pygame mailing list

#...before mainloop...
othergroup =  []  #create empty list
 
#...inside mainloop...
while mainloop:
    # ... the usual mainloop code
 
    # test if a bird collides with another bird
    for bird in birdgroup:
        othergroup[:] = birdgroup.sprites() # This is the correct code, no garbage collection
        othergroup.remove(bird) # remove the actual bird, only all other birds remain
        if pygame.sprite.spritecollideany(bird, othergroup): 
            bird.crashing = True
            crashgroup = pygame.sprite.spritecollide(bird, othergroup, False )
            for crashbird in crashgroup:
                bird.dx -= crashbird.pos[0] - bird.pos[0]
                bird.dy -= crashbird.pos[1] - bird.pos[1]

wrong

If you write instead of the othergroup[:] = …. line this code:

othergroup = birdgroup.copy() # WRONG ! THIS CODE MAKES UGLY TIME-CONSUMING GARBAGE COLLECTION !

you will notice longer delays between 2 frames as soon as many objects are on the screen.

The .copy() command, excecuted each frame, each up huge blocks of memory. This block of memory has to be cleaned again from time to time and while pygame does so, you can notice a huge pause…the game “hangs”. This effect will only become visible if you have many sprites moving around. Watch the max(ms) display in the pygame title or the length of the green Timebar bars.

In the source code example at the end of this page you can toggle efficient and inefficient coding by pressing B.

clever

Instead of creating another group for all Birds without the actual Bird you can also compare with the whole birdgroup and check the Bird's number attribute to make sure there is a collision between two different sprites:

# very clever coding
crashgroup = pygame.sprite.spritecollide(bird, birdgroup, False) # the actual Bird is also in birdgroup
for crashbird in crashgroup: 
    if crashbird.number != bird.number: #avoid collision with itself

In the source code example at the end of this page you can toggle clever coding by pressing C

overwriting a class method

Take a look at the code of the def kill(self) method of the (huge) Bird sprite class. Other classes like the Timebar sprite class use the self.kill() command, but you will find no def kill(self): function in the Timebar sprite class. Pygame knows what to to because the pygame.sprite.Sprite class provides a def kill(self) method. As the name suggest, self.kill() removes the actual sprite from the screen and from all groups.

class Timebar(pygame.sprite.Sprite):
   #..
   def update(self, time):
        self.rect.centery = self.rect.centery - 7 # each timebar kill itself when it leaves the screen
        if self.rect.centery < 0:
            self.kill() # remove the sprite from screen and from all groups

over writing kill

However, the more complicated Bird sprite class has it's own def kill(self): function. That is because I want to do some extra stuff before killing the sprite, like playing a sound effect and shattering fragments around the screen. Also in this case I want to remove the Bird sprite from a special dictionary where I store each Bird sprite and its individual number. Therefore, I overwrite the def kill(self): function, do my special things and finally call pygame's kill method directly: pygame.sprite.Sprite.kill(self)

Big Badda Boom!

class Bird(pygame.sprite.Sprite):
   #..
   def kill(self):
        """because i want to do some special effects (sound, dictionary etc.)
        before killing the Bird sprite i have to write my own kill(self)
        function and finally call pygame.sprite.Sprite.kill(self) 
        to do the 'real' killing"""
        cry.play()   #play sound effect
        for _ in range(random.randint(3,15)):
            Fragment(self.pos) # create Fragment sprites
        Bird.birds[self.number] = None # kill Bird in sprite dictionary
        pygame.sprite.Sprite.kill(self) # kill the actual Bird 

Exploding fragments

The Fragment class does not need much input: just the start position of the Fragment. A Fragment Sprite will be generated and a random amount for dx and dy is generated to make the Fragment fly away from the point of the “explosion”2). Note that you can toggle gravity by pressing G during the game. The effect of the gravity is handled in the update method of the Fragment class:

class Fragment(pygame.sprite.Sprite):
    """a fragment of an exploding Bird"""
    gravity = True # fragments fall down ?
    def __init__(self, pos):
            pygame.sprite.Sprite.__init__(self, self.groups)
            self.pos = [0.0,0.0]
            self.pos[0] = pos[0]
            self.pos[1] = pos[1]
            #...
            self.dx = random.randint(-self.fragmentmaxspeed,self.fragmentmaxspeed)
            self.dy = random.randint(-self.fragmentmaxspeed,self.fragmentmaxspeed)
 
    def update(self, seconds):
            #...
            self.pos[0] += self.dx * seconds
            self.pos[1] += self.dy * seconds
            if Fragment.gravity:
                self.dy += FORCE_OF_GRAVITY # gravity suck fragments down
            self.rect.centerx = round(self.pos[0],0)
            self.rect.centery = round(self.pos[1],0)

Layers

By default, pygame will blit the sprites in the order the sprites are added. IF you prefer precise order of drawing the sprites (like the mouse pointer always before all other sprites) you can do two things:

  • use several clear, update and draw commands, one for each sprite group:
  • use the pygame.sprite-LayeredUpdates group instead of a sprite group and set a default layer for each group, like in the code example below:
    # LayeredUpdates instead of group to draw in correct order
    allgroup = pygame.sprite.LayeredUpdates() # important
    #assign default groups to each sprite class
    Livebar.groups =  bargroup, allgroup 
    Timebar.groups = bargroup, allgroup
    Bird.groups =  birdgroup, allgroup
    Fragment.groups = fragmentgroup, allgroup
    BirdCatcher.groups = stuffgroup, allgroup
    #assign default layer for each sprite (lower numer is background)
    BirdCatcher._layer = 5 # top foreground
    Fragment._layer = 4
    Timebar._layer = 3
    Bird._layer = 2
    Livebar._layer = 1 #background

More about layers in the next step.

putting the whole game inside a function

You may have noticed that the whole game sits inside a function called def game():. This is useful when we later make a game menu that starts the game. Because it will be sensible to split the game code into a menu.py and a startgame.py file. The menu.py file will import the startgame and call it when the user chooses the menu option. To be more precise, the menu.py file will start a single function inside the startgame.py file. (If there is no meaningful function defined inside startgame.py the menu file would start startgame.py right after importing it, before displaying a menu and waiting for the user's choice).

python modules

We have no menu.py (yet) but we can prepare for it by stuffing all interesting parts (shooting up penguins etc) inside a function - let's call the function game() (see below). But how do we start the game directly? For that, we check the internal python variable __name__. In this variable, python stores the name of the python module that imported the actual python program. If we started the actual python program directly (from the terminal or from the python editor), then this variable gets the value __main__ from python. Therefore you will find in many python modules3) those lines:

def game():
  #.. all the interesting stuff
if __name__ == "__main__":
    print "i was started directly and will start game()"
    game()  # start the interesting stuff 
else:
    print "i am imported by",__name__, "and will do nothing at the moment"

source code on github

To run this example you need:

file in folder download
015_more_sprites.py pygame Download the whole Archive with all files from Github:
https://github.com/horstjens/ThePythonGameBook/archives/master
babytux.png
babytux.png
pygame/data
babytux_neg.png
babytux_neg.png
pygame/data
claws.ogg
from Battle of Wesnoth
pygame/data

View/Edit/Download the file directly in Github: https://github.com/horstjens/ThePythonGameBook/blob/master/pygame/015_more_sprites.py

click reload in your browser if you see no code here:

comment this page

1) class names should begin with an upper case letter
2) The random.randint function could generate Fragments with the values Zero for dx and dy. In this case, you would have a static hovering fragment. Let's assume such a Fragment is flying directly toward the viewer.
3) every python file is a module!

en/pygame/step015.txt · Last modified: 2014/01/09 11:07 (external edit)