Table of Contents
Step 015 - Explosions, healthbars and gravity
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).
#...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 - bird.pos bird.dy -= crashbird.pos - bird.pos
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.
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:
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
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 = pos self.pos = pos #... self.dx = random.randint(-self.fragmentmaxspeed,self.fragmentmaxspeed) self.dy = random.randint(-self.fragmentmaxspeed,self.fragmentmaxspeed) def update(self, seconds): #... self.pos += self.dx * seconds self.pos += self.dy * seconds if Fragment.gravity: self.dy += FORCE_OF_GRAVITY # gravity suck fragments down self.rect.centerx = round(self.pos,0) self.rect.centery = round(self.pos,0)
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
drawcommands, one for each sprite group:
- use the
pygame.sprite-LayeredUpdatesgroup 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).
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:
|015_more_sprites.py|| || Download the whole Archive with all files from Github:
| babytux.png ||
| babytux_neg.png ||
| claws.ogg |
from Battle of Wesnoth
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: