Table of Contents
Step 016 - LayeredUpdates and Parallax scrolling
The source code example below is not much of a game, but it demonstrates the uses of layers for sprites and Parallax_scrolling (the mountains). Also, you can hide penguins behind blocks and mountains. Additionally, you can aim with the mouse pointer and press P to print out information about the layers (at the text console).
According to wikipedia, Parallax scrolling was first introduced to computer games by the arcade game Moon_Patrol 1982. The mountains (and in higher levels, futuristic buildings) moved at different speed to create the illusion of a side-scrolling computer game.
proposals to tinker:
- Change the layer of the three types of class.moutains. IF you switch -3 with -1, the view effect will be like looking at a rotating disc.
- Not enough action ? Apply the thingls learned at the pevious step, let the Birds change
_layerby random and explode them by mouseclick !
Do you remember the sprite groups from the last step ? Sprite groups are basically containers where the sprites “live” in. As soon as a sprite is killed (by it's
kill() functoin) the sprite is not also removed from the screen but also from all containers holding it. In the last two steps, i had two uses for sprite groups:
allgroup(every sprite was member of this group), to clear, draw, and update all sprites from within the main loop
- other groups like
birdgroup, used to check each sprite in this group for collision detection (crashgroup)
You will note that there exist different variants of sprite groups:
- the old spritegroup
- the newer LayeredUpdate group
pygame.sprite.Layeredupdate() instead of
pygame.sprite.Group() you can give each sprite a variable
_layer as well as a variable
groups to influence the drawing order of the sprite. In the previous step those variables were defined outside the sprite class in the mainloop. It makes more sense to define those variables inside the sprite class itself. The ideal place to do so is the __init__(self) method of each sprite class. Note two things:
- The sprite groups must exist (be defined in the mainloop) before you can assign sprites to the groups. That means, inside the mainloop, before you create a Bird sprite or assign images to the Bird class, you must define the spritegroups.
- Inside the class, you must assign groups and _layer before you call pygame.sprite.Sprite.__init__(self, *groups):
class Bird(pygame.sprite.Sprite): #... def __init__(self, startpos=screen.get_rect().center): self.groups = birdgroup, allgroup # assign groups BEFORE calling pygame.sprite.Sprite.__init__ self._layer = 7 # assign _layer BEFORE calling pygame.sprite.Sprite.__init__ pygame.sprite.Sprite.__init__(self, self.groups ) #call parent class. NEVER FORGET ! self.pos = starpos # ... # define sprite groups birdgroup = bargroup = pygame.sprite.Group() # simple group for collision detection allgroup = pygame.sprite.LayeredUpdates() # more sophisticated and modern group # assign images to the Bird class or create a Bird sprite Bird()
please see the official pygame doumentation at: http://www.pygame.org/docs/ref/sprite.html#pygame.sprite.LayeredUpdates
The cool things about layers is: you can change them even at runtime, to place sprites more in the foreground or more in the background (see code example below). The change_layer method does exaclty that. Important: you need only to change the layer of the LayeredUpdates-group that actually draws the sprites on the screen. In the code example below, this is the allgroup. (Special thanks to Gummbum for helping me out here). Because i want only to change the layer of the Bird sprites and their Lifebar sprites i loop over all sprites in the groups birdgroup and bargroup. Each Bird sprite is a member of the allgroup as well as of the birdgroup. Each Lifebar sprite is a member of the allgroup as well as of the bargroup.
if pygame.mouse.get_pressed(): if birdlayer < 10: birdlayer += 1 cooldowntime = .5 # seconds cry.play() for bird in birdgroup: allgroup.change_layer(bird, birdlayer) # allgroup draws the sprite for bar in bargroup: allgroup.change_layer(bar, birdlayer) # allgroup draws the sprite
Up to now, i blitted all text to the screen or to the background with the write function. In this example, you will see a new class called Text, also printing a msg to the screen. The Text sprite has no advantages over blitting directly to the background yet, but you can use it later to change the _layer of the Text sprite or if you want the Text sprite to move around. Note that the class Text has it's own method newmsg to update the displaying text string.
def newmsg(self, birdlayer): self.image = write("current Bird _layer = %i" % birdlayer) self.rect = self.image.get_rect() self.rect.center = (screen.get_width()/2,10)
Take a look at the Bird class. Something is new ! There is a new class variable Bird.waittime indicating how long a Bird should stay “invisible”. Instead of messing around with drawing and not drawing, during his “invisible” waittime, each bird is simply teleported to the position (-100,-100), that is outside your screen. If the waittime is over, the Bird sprite is teleported to his Bird.pos position and act like a normal Bird sprite - speeding around, crashing into walls and other birds, exploding.
Class Bird(pygame.sprite.Sprite): waittime = 1.0 #seconds #... def update(self, seconds): #---make Bird only visible after waiting time self.lifetime += seconds if self.lifetime > (self.waittime) and self.waiting: self.newspeed() self.waiting = False self.rect.centerx = round(self.pos,0) self.rect.centery = round(self.pos,0) if self.waiting: self.rect.center = (-100,-100) else: # speedcheck #.. all the other things
But why the waittime ? The answer is a modification to the Fragment class - A bird gets now Fragment when killed (red) and also when appearing (blue). Before you look at the Fragment class, check out the __init__ method of the Bird class:
def __init__(self, layer=4): #... Lifebar(self.number) #create a Lifebar for this Bird. # starting implosion of blue fragments for _ in range(8): Fragment(self.pos, True)
See the for loop ? I simply wanted 8 (blue) Fragments, so i do not use an variable like a or b or x inside the for loop but simply an underscore. The True parameter in the Fragment class (see below) indicate a blue Fragment instead of the “normal” red Fragment.
The fragment class get now a paramter (bluefrag) into it's __init__ method indicating if this is a “born” (blue) or a “kill” (red) Fragment. Then the __init__ method splits using a if-else construct. The new part is only valid for “bluefrag” Fragments. After choosing per random from where (screen edge) the Fragment start, self.dx and self.dy are calculated… The Fragment should fly from the screen edge toward the position of the Bird sprite (self.target). But how fast does the fragment fly ? Because in the update method of the fragment class dx and dy is multiplied by the amount of seconds passed (at a Framerate of 30 FPS, this would be 1/30 of a second), the speed unit of a sprite is measured in pixel per second. Meaning each blue sprite will need exactly one second to travel from it's origin to the Bird sprite position.
But what if the waittime for each Birdsprite is 2 seconds ? or just a half second ? For this reason, dx and dy are multiplied by the factor ( 1.0 / Bird.waittime). So if the Bird waits 2 seconds before appearing, the blue Fragments have more time and should travel slower: 1.0 / 2.0 = 0.5, the speed is reduced. On the other hand, if the waittime is shorter (say 0.5 seconds) the Fragments should fly faster: ( 1.0 / 0.5 = 2); dx and dy is doubled.
For aesthetic reasons, i allow the blue Fragments to live up to a half second after reaching their target by adding to self.lifetime a value between 0 and 0.5:
random.random() * 0.5. Random.random() creates a float value between 0 and 1.
class Fragment(pygame.sprite.Sprite): #... def __init__(self, pos, bluefrag = False): #... self.bluefrag = bluefrag self.pos=[0.0,0.0] self.target = pos if self.bluefrag: # blue frament implodes from screen edge toward Bird self.color = (0,0,random.randint(25,255)) # blue self.side = random.randint(1,4) if self.side == 1: # left side self.pos = 0 self.pos = random.randint(0,screen.get_height()) elif self.side == 2: # top self.pos = random.randint(0,screen.get_width()) #... # calculating flytime for one second.. Bird.waittime should be 1.0 self.dx = (self.target - self.pos) * 1.0 / Bird.waittime self.dy = (self.target - self.pos) * 1.0 / Bird.waittime self.lifetime = Bird.waittime + random.random() * .5 # a bit more livetime after the Bird appears else: # red fragment explodes from the bird toward screen edge #... all the stuff for red Fragments def update(self, seconds): # ... self.pos += self.dx * seconds self.pos += self.dy * seconds # ... self.rect.centerx = round(self.pos,0) self.rect.centery = round(self.pos,0)
You will notice that at the start of the game, the moutains “walk” in from right to left. All moutnains are instances of the same mountain class. The different types of moutains each have their own
type parameter (making blue, red or pink mountains).
Most of the work in this class is done in it's
class Mountain(pygame.sprite.Sprite): #... def __init__(self, type): self.type = type if self.type == 1: self._layer = -1 self.dx = -100 self.color = (0,0,255) # blue mountains, close elif self.type == 2: self._layer = -2 self.color = (200,0,255) # pink mountains, middle self.dx = -75 else: self._layer = -3 self.dx = -35 self.color = (255,0,0) # red mountains, far away self.groups = allgroup, mountaingroup pygame.sprite.Sprite.__init__(self, self.groups) # THE Line #...
Maybe the most interesting part here is the creation of the actual mountain. This is done with the help of the random.random() function (creates an decimal number between 0.0 and 1.0) and the
pygame.draw.polygon-method. On each mountain surface, a filled triangle (polygon) is created from the lower left corner to a corner in the middle (x/2) and at a random height (the moutain peak) and back to the lower right corner. The syntax for pygame.draw.poligon is:
pygame.draw.polygon(surface, color, pointlist, width=0)
class Mountain(pygame.sprite.Sprite): #... def __init__(self, type): #... self.dy = 0 x = 100 * self.type * 1.5 y = screen.get_height() / 2 + 50 * (self.type -1) self.image = pygame.Surface((x,y)) self.image.set_colorkey((0,0,0)) # black is transparent pygame.draw.polygon(self.image, self.color, ((0,y), (0,y-10*self.type), (x/2, int(random.random()*y/2)), (x,y-10*self.type), (x,y), (9,y)),0) # width=0 fills the polygon self.image.convert_alpha() self.rect = self.image.get_rect()
Each mountain has a
.parent attribute, set to False in the
__ini__-method. In the
update-method of the moutain class, it is checked if the mountain' center is visible. If yes, a new mountain of the same
type is created and placed directly to it's right side (yet invisible because outside the screen border). If a mountain travels too far to the left side, it is killed.
class Mountain(pygame.sprite.Sprite): #... def update(self, time): if self.rect.centerx + self.rect.width/2+10 < 0: self.kill() # create new mountains if necessary if not self.parent: if self.rect.centerx < screen.get_width(): self.parent = True Mountain(self.type) # new Mountain coming from the right side
source code on github
To run this example you need:
|016_layers.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/016_layers.py
click reload in your browser if you see no code here: