Pygame – creating a scrolling background

This article explains how to create a scrolling background in Pygame.

Scrolling backgrounds, while not suitable in all types of games can add a very realistic touch to the game they are implemented in. Common examples of games which use scrolling backgrounds are typically side scroller games like flappy bird, space invaders and car racing games.

Today, in this article we’ll be using one of our previous games, from our Pygame Tutorial. It’s a simple side scroller, where you have to dodge the incoming cars. If you want to learn more about the code and how the game works, follow the link to the tutorial.


The concepts behind scrolling backgrounds

Now obviously there are many different methods that can be used to create the scrolling background effect in Pygame. The method we’ll be following is what I consider to be one of the best. It easily bypasses many common issues that arise, while also having the ability to scroll vertically as well as horizontally (with a few tweaks).

The common principle behind background scrolling is simply to move the background in the direction you want it to move. However, the problem is that moving the background 10 pixels in any direction will result in a gap of 10 pixels in that very spot.

The solution that our approach takes is to create two copies of the background, with no overlap. This way, if the first background has been moved half way down, the other background which will be right behind it and will cover that 50% gap.

If this doesn’t make sense, take a good long look at the code we’ll be showing below. If you examine each line of the code carefully, you’ll understand how background scrolling works.

A horizontal scrolling version of our code is available at the very end of this article.


Our Code

Another advantage of our approach is that it’s class based. 90% of the work is all done before the game loop even begins. If you want to skip the explanation and simply implement the scrolling background feature, copy over the code for the Background class, create an object and call the two methods shown below in your game loop. You’ll also have to change the file path of the image to be used.

background_object.update()
background_object.render()

For the actual explanation, keep reading.

Below is simply the code for the Background class. The full source code for the game can be found at the bottom of this article. We’ll begin by explaining each method of the Background class.

class Background():
      def __init__(self):
            self.bgimage = pygame.image.load('AnimatedStreet.png')
            self.rectBGimg = self.bgimage.get_rect()

            self.bgY1 = 0
            self.bgX1 = 0

            self.bgY2 = self.rectBGimg.height
            self.bgX2 = 0

            self.moving_speed = 5
        
      def update(self):
        self.bgY1 -= self.moving_speed
        self.bgY2 -= self.moving_speed
        if self.bgY1 <= -self.rectBGimg.height:
            self.bgY1 = self.rectBGimg.height
        if self.bgY2 <= -self.rectBGimg.height:
            self.bgY2 = self.rectBGimg.height
            
      def render(self):
         DISPLAYSURF.blit(self.bgimage, (self.bgX1, self.bgY1))
         DISPLAYSURF.blit(self.bgimage, (self.bgX2, self.bgY2))

Explanation

def __init__(self):
      self.bgimage = pygame.image.load('AnimatedStreet.png')
      self.rectBGimg = self.bgimage.get_rect()

      self.bgY1 = 0
      self.bgX1 = 0

      self.bgY2 = self.rectBGimg.height
      self.bgX2 = 0

      self.moving_speed = 5

This is the initializing function for the Background class. First we load an image and then create a rect object based of it.

Next we define 2 sets of points. The first set starts at the origin point, the (top left corner) and the second initializes at the bottom of the screen, just out of sight.

Finally we define the moving speed of the background. The faster you want the background the move, the higher the value should be. 5 is a good average though.

def update(self):
      self.bgY1 -= self.moving_speed
      self.bgY2 -= self.moving_speed
      if self.bgY1 <= -self.rectBGimg.height:
            self.bgY1 = self.rectBGimg.height
      if self.bgY2 <= -self.rectBGimg.height:
            self.bgY2 = self.rectBGimg.height

The update() method is what handles all the movement of the background. Every time it is called, it decrements self.bgY1 and self.bgY2, thus changing the co-ordinates to which the background is drawn.

The if statements are there to make sure that the values of the two variables do not exceed the height of the screen itself. If this occurs, it will reset it back to it’s original position at the top.

def render(self):
      DISPLAYSURF.blit(self.bgimage, (self.bgX1, self.bgY1))
      DISPLAYSURF.blit(self.bgimage, (self.bgX2, self.bgY2))

The render() method is the method used to finally draw the background to the screen. Normally, we would only have one blit() function here. However, this time we draw two backgrounds using both pairs of co-ordinates we defined earlier.

Here’s a short video showing the scrolling background effect in our game.

If you want to see how the above code will be implemented in the actual program, you can look the source code for the whole project below. A section on a horizontal version of the Background class is also present at the end of this article.


The Full Game + Code

The code for the whole game with the scrolling background implemented is shown below. If you want the source code and the images used in the game, follow the link to the main tutorial.

#Imports
import pygame, sys
from pygame.locals import *
import random, time

#Initializing 
pygame.init()

#Setting up FPS 
FPS = 60
FramePerSec = pygame.time.Clock()

#Creating colors
BLUE  = (0, 0, 255)
RED   = (255, 0, 0)
GREEN = (0, 255, 0)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

#Other Variables for use in the program
SCREEN_WIDTH = 400
SCREEN_HEIGHT = 600
SPEED = 5
SCORE = 0

#Setting up Fonts
font = pygame.font.SysFont("Verdana", 60)
font_small = pygame.font.SysFont("Verdana", 20)
game_over = font.render("Game Over", True, BLACK)

#Create a white screen 
DISPLAYSURF = pygame.display.set_mode((400,600))
DISPLAYSURF.fill(WHITE)
pygame.display.set_caption("Game")


class Enemy(pygame.sprite.Sprite):
      def __init__(self):
        super().__init__() 
        self.image = pygame.image.load("Enemy.png")
        self.surf = pygame.Surface((42, 70))
        self.rect = self.surf.get_rect(center = (random.randint(40,SCREEN_WIDTH-40)
                                                 , 0))

      def move(self):
        global SCORE
        self.rect.move_ip(0,SPEED)
        if (self.rect.top > 600):
            SCORE += 1
            self.rect.top = 0
            self.rect.center = (random.randint(40, SCREEN_WIDTH - 40), 0)


class Player(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__() 
        self.image = pygame.image.load("Player.png")
        self.surf = pygame.Surface((40, 75))
        self.rect = self.surf.get_rect(center = (160, 520))
       
    def move(self):
        pressed_keys = pygame.key.get_pressed()
       #if pressed_keys[K_UP]:
            #self.rect.move_ip(0, -5)
       #if pressed_keys[K_DOWN]:
            #self.rect.move_ip(0,5)
        
        if self.rect.left > 0:
              if pressed_keys[K_LEFT]:
                  self.rect.move_ip(-5, 0)
        if self.rect.right < SCREEN_WIDTH:        
              if pressed_keys[K_RIGHT]:
                  self.rect.move_ip(5, 0)
                  
class Background():
      def __init__(self):
            self.bgimage = pygame.image.load('AnimatedStreet.png')
            self.rectBGimg = self.bgimage.get_rect()

            self.bgY1 = 0
            self.bgX1 = 0

            self.bgY2 = self.rectBGimg.height
            self.bgX2 = 0

            self.movingUpSpeed = 5
        
      def update(self):
        self.bgY1 -= self.movingUpSpeed
        self.bgY2 -= self.movingUpSpeed
        if self.bgY1 <= -self.rectBGimg.height:
            self.bgY1 = self.rectBGimg.height
        if self.bgY2 <= -self.rectBGimg.height:
            self.bgY2 = self.rectBGimg.height
            
      def render(self):
         DISPLAYSURF.blit(self.bgimage, (self.bgX1, self.bgY1))
         DISPLAYSURF.blit(self.bgimage, (self.bgX2, self.bgY2))
        
#Setting up Sprites        
P1 = Player()
E1 = Enemy()

back_ground = Background()

#Creating Sprites Groups
enemies = pygame.sprite.Group()
enemies.add(E1)
all_sprites = pygame.sprite.Group()
all_sprites.add(P1)
all_sprites.add(E1)

#Adding a new User event 
INC_SPEED = pygame.USEREVENT + 1
pygame.time.set_timer(INC_SPEED, 1000)

#Game Loop
while True:
      
    #Cycles through all occurring events   
    for event in pygame.event.get():
        if event.type == INC_SPEED:
              SPEED += 0.5      
        if event.type == QUIT:
            pygame.quit()
            sys.exit()


    back_ground.update()
    back_ground.render()

    #DISPLAYSURF.blit(background, (0,0))
    scores = font_small.render(str(SCORE), True, BLACK)
    DISPLAYSURF.blit(scores, (10,10))

    #Moves and Re-draws all Sprites
    for entity in all_sprites:
        DISPLAYSURF.blit(entity.image, entity.rect)
        entity.move()

    #To be run if collision occurs between Player and Enemy
    if pygame.sprite.spritecollideany(P1, enemies):
          pygame.mixer.Sound('crash.wav').play()
          time.sleep(0.8)
                   
          DISPLAYSURF.fill(RED)
          DISPLAYSURF.blit(game_over, (30,250))
          
          pygame.display.update()
          for entity in all_sprites:
                entity.kill() 
          time.sleep(1.5)
          pygame.quit()
          sys.exit()        
        
    pygame.display.update()
    FramePerSec.tick(FPS)

Horizontal Version

Some people may require background scrolling that occurs from left to right (or right to left) rather than up and down. As such, we’ve adapted our code to work as a horizontal scroller. Just remember to be using appropriate images . Without any editing, the above street image would look very out of place if scrolled horizontally.

class Background():
      def __init__(self):
            self.bgimage = pygame.image.load('AnimatedStreet.png')
            self.rectBGimg = self.bgimage.get_rect()

            self.bgY1 = 0
            self.bgX1 = 0

            self.bgY2 = 0
            self.bgX2 = self.rectBGimg.width

            self.moving_speed = 5
        
      def update(self):
        self.bgX1 -= self.moving_speed
        self.bgX2 -= self.moving_speed
        if self.bgX1 <= -self.rectBGimg.width:
            self.bgX1 = self.rectBGimg.width
        if self.bgX2 <= -self.rectBGimg.width:
            self.bgX2 = self.rectBGimg.width
            
      def render(self):
         DISPLAYSURF.blit(self.bgimage, (self.bgX1, self.bgY1))
         DISPLAYSURF.blit(self.bgimage, (self.bgX2, self.bgY2))

To summarize, we changed all the height related aspects to the width. You can observe this change by comparing the update functions of both classes. This is where the most change occurred.

Another difference is where we spawned the second background. Unlike the first example, we created the two backgrounds side by side, rather than one up and one down.


Download Link

Here’s the download link for those of who who bothered to scroll all the way down here. And for those having trouble with the scrolling background. (The graphics have been slightly updated too, there was a slight issue).

Download “Pygame Tutorial Materials Download” PygameTutorial_3_0.zip – Downloaded 14492 times – 215.15 KB

This marks the end of the Pygame Scrolling Background article. Any suggestions or contributions for CodersLegacy are more than welcome. Questions regarding the article content can be asked in the comments section below.

10 thoughts on “Pygame – creating a scrolling background”

  1. As my background scrolls, there tends to be a gap where it skips a chunk of the background? So for example I have a road scrolling horizontally with white dashes through the middle. But every full scroll of the image it scrolls the road without the white lines, so it alternates between with white lines and without?

    Reply
    • this is because the speed is not being taken into consideration during the stitching of the backgrounds. This will fix the issue

      def update(self):
              self.bgX1 -= self.moving_speed
              self.bgX2 -= self.moving_speed
              if self.bgX1 <= -self.rectBGimg.width + self.moving_speed:
                  self.bgX1 = self.rectBGimg.width
              if self.bgX2 <= -self.rectBGimg.width + self.moving_speed:
                  self.bgX2 = self.rectBGimg.width
      
      Reply
  2. Yes I also have those gap, seems like the background isn’t totally overlapping here. Though, besides the path of the assets, I didn’t changer anything into the code… Don’t see what causes that !

    Also, the background should indeed go the other way around I think ^^ Changing its way is easy enough, but then it stops cycling, screen glitches, and that I didn’t manage to fix… =/

    Fun mini-project though, thanks !

    Reply
    • I updated the tutorial a bit. Added a download link with the code and materials. Plus fixed a slight issue with the Street graphic. If it doesn’t show up (due to caching) let me know, and i’ll have it refreshed.

      Reply
  3. Cool project. All works. A thing that road goes wrong way is quite appropriate, because its a mini test for ourselves to make it go another direction =). Thanks!
    PS Isn’t line 50 self.rect.top = 0 a redundant,
    because we define the same “y”=0 in a line 51 as the last tuple digit?

    Reply
  4. To everyone trying to make the background move down, here’s the deal:

    def update(self):
        self.bgY2 += self.moving_speed
        self.bgY1 += self.moving_speed
        if self.bgY2 >= self.rectBGimg.height:
            self.bgY2 = -self.rectBGimg.height
        if self.bgY1 >= self.rectBGimg.height:
            self.bgY1 = -self.rectBGimg.height
    
    Reply

Leave a Comment