ThePythonGameBook

learn Python. Create Games

User Tools

Site Tools


en:pygame:step016

Step 016 - LayeredUpdates and Parallax scrolling

gameplay

screenshot of layers

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 <key>p</key> to print out information about the layers (at the text console).

Trivia:

screenshot of moon-patrol. Picture from http://www.arcade-museum.com/game_detail.php?game_id=8747

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 _layer by random and explode them by mouseclick !

code discussion

layers

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:

  1. The allgroup (every sprite was member of this group), to clear, draw, and update all sprites from within the main loop
  2. 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

When using 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:

  1. 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.
  2. Inside the class, you must assign groups and _layer before you call pygame.sprite.Sprite.__init__(self, *groups):

<note>#… means that i let away some not so important code lines</note>

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()

pygame.sprite.LayeredUpdates

please see the official pygame doumentation at: http://www.pygame.org/docs/ref/sprite.html#pygame.sprite.LayeredUpdates


changing layers

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()[0]:
  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 

Textsprite

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)

waiting Birds

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],0)
           self.rect.centery = round(self.pos[1],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.

dual-use Fragments

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] = 0   
               self.pos[1] = random.randint(0,screen.get_height())
          elif self.side == 2: # top
               self.pos[0] = random.randint(0,screen.get_width()) 
               #...
          # calculating flytime for one second.. Bird.waittime should be 1.0
          self.dx = (self.target[0] - self.pos[0]) * 1.0 / Bird.waittime
          self.dy = (self.target[1] - self.pos[1]) * 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[0] += self.dx * seconds
       self.pos[1] += self.dy * seconds
       # ...
       self.rect.centerx = round(self.pos[0],0)
       self.rect.centery = round(self.pos[1],0)

scrolling mountains

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 __ini__-method:

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:

file in folder download
016_layers.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/016_layers.py

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

comment this page

~~DISQUS~~

/var/www/horst/thepythongamebook.com/data/pages/en/pygame/step016.txt · Last modified: 2014/01/09 11:07 (external edit)