pythonpygameflappy-bird-clone

The Pipes in my Pygame Flappy Bird clone lag and jolt around instead of moving fluidly


Below is the basic code for creating and managing the pipes of the game:

import pygame as pg
import sys,os,math,time,random

# colours
white = (255,255,255)
red = (255,0,0)
green = (0,255,0)

# general stuff
WIDTH = 1024
HEIGHT = 576
FPS = 60

# other
all_events = [pg.QUIT, pg.ACTIVEEVENT, pg.KEYDOWN, pg.KEYUP, pg.MOUSEMOTION,
              pg.MOUSEBUTTONUP, pg.MOUSEBUTTONDOWN, pg.VIDEORESIZE,
              pg.VIDEOEXPOSE, pg.USEREVENT]

pg.init()
screen = pg.display.set_mode((WIDTH, HEIGHT))
clock = pg.time.Clock()

# Class to manage Pipes
class Pipe_Manager:

    def __init__(self):
        self.pipe_width = 50
        self.pipes = []
        self.pipe_speed = 5
        self.max_tick = 75
        self.spawn_tick = self.max_tick

    def manage_pipes(self):
        self.spawner()
        self.manage()
        self.display()

    def make_pipe(self):

        height = random.randint(100,326)
        gap = random.randint(100,250)
        surf1 = pg.Surface((self.pipe_width, height))
        surf1.fill(green)
        surf2 = pg.Surface((self.pipe_width, HEIGHT - (height + gap)))
        surf2.fill(green)

            # surface, (x,y) and vertical height
        pipe = [surf1, [WIDTH, 0], height]
        pipe2 = [surf2, [WIDTH, height + gap], HEIGHT - (height + gap)]
        self.pipes.append(pipe)
        self.pipes.append(pipe2)

    def spawner(self):  

        if self.spawn_tick == self.max_tick:
            self.make_pipe()
            self.spawn_tick = 0

        self.spawn_tick += 1

    def manage(self):

        for pipe in self.pipes: 

            # move the pipe
            pipe[1][0] -= self.pipe_speed 

            # check if it's off screen           
            if pipe[1][0] + self.pipe_width < 0:
                self.pipes.remove(pipe)

    def display(self):
        for pipe in self.pipes:
            screen.blit(pipe[0], (pipe[1][0], pipe[1][1]))

################################################################################

pg.event.set_blocked(all_events)
pg.event.set_allowed([pg.QUIT, pg.KEYDOWN])

pipe_manager = Pipe_Manager()
loop = True
while loop:
    screen.fill(white)
    pipe_manager.manage_pipes()
    pg.display.update()
    clock.tick(FPS)

The pipes seem to shake as they move horizontally and sometimes the top pipe becomes misaligned from the bottom one.

I hope this isn't a problem specific to my computer because I have abstracted away a significant amount of my flappy-bird-clone code and the source of this pipe lag problem must lie somewhere in here.


Solution

  • The problem is this piece of code:

        for pipe in self.pipes: 
    
            # move the pipe
            pipe[1][0] -= self.pipe_speed 
    
            # check if it's off screen           
            if pipe[1][0] + self.pipe_width < 0:
                self.pipes.remove(pipe)
    

    Here you change the list you're currently iterating over. Once a pipe gets removed from the list, the next one misses one movement step.

    Take a look at the following example in which you can spot the problem yourself (see how 5 is missing in the output because we removed 4):

    >>> l = [1,2,3,4,5,6,7,8,9,10]
    >>> for x in l:
    ...   if x == 4:
    ...     l.remove(x)
    ...   print(x)
    ...
    1
    2
    3
    4
    6
    7
    8
    9
    10
    >>>
    

    (There's a reason other languages forbid changing the sequence you're currently iterating).

    A simple fix is to make a copy of the list first:

        for pipe in self.pipes[:]: 
    

    For smoother movement, try increasing your framerate and use timestepping. Here's a possible way:

    import pygame as pg
    import sys,os,math,time,random
    
    # colours
    white = (255,255,255)
    red = (255,0,0)
    green = (0,255,0)
    
    # general stuff
    WIDTH = 1024
    HEIGHT = 576
    FPS = 120
    
    # other
    all_events = [pg.QUIT, pg.ACTIVEEVENT, pg.KEYDOWN, pg.KEYUP, pg.MOUSEMOTION,
                  pg.MOUSEBUTTONUP, pg.MOUSEBUTTONDOWN, pg.VIDEORESIZE,
                  pg.VIDEOEXPOSE, pg.USEREVENT]
    
    pg.init()
    screen = pg.display.set_mode((WIDTH, HEIGHT))
    clock = pg.time.Clock()
    
    
    class Pipe:
        def __init__(self, img, pos):
            self.img = img
            self.pos = pos
    
    # Class to manage Pipes
    class Pipe_Manager:
    
        def __init__(self):
            self.pipe_width = 50
            self.pipes = []
            self.pipe_speed = 0.3
            self.max_tick = 1500
            self.spawn_tick = self.max_tick
    
        def manage_pipes(self, dt):
            self.spawner(dt)
            self.manage(dt)
            self.display()
    
        def make_pipe(self):
    
            height = random.randint(100,326)
            gap = random.randint(100,250)
            surf1 = pg.Surface((self.pipe_width, height))
            surf1.fill(green)
            surf2 = pg.Surface((self.pipe_width, HEIGHT - (height + gap)))
            surf2.fill(green)
    
            pipe = Pipe(surf1, pg.Vector2(WIDTH, 0))
            pipe2 = Pipe(surf2, pg.Vector2(WIDTH, height + gap))
            self.pipes.append(pipe)
            self.pipes.append(pipe2)
    
        def spawner(self, dt):
    
            if self.spawn_tick >= self.max_tick:
                self.make_pipe()
                self.spawn_tick = 0
    
            self.spawn_tick += dt
    
        def manage(self, dt):
    
            for pipe in self.pipes[:]: 
    
                # move the pipe
                pipe.pos.x -= self.pipe_speed * dt
    
                # check if it's off screen           
                if pipe.pos.x + self.pipe_width < 0:
                    self.pipes.remove(pipe)
    
        def display(self):
            for pipe in self.pipes:
                screen.blit(pipe.img, pipe.pos)
    
    ################################################################################
    
    pg.event.set_blocked(all_events)
    pg.event.set_allowed([pg.QUIT, pg.KEYDOWN])
    
    pipe_manager = Pipe_Manager()
    loop = True
    dt=0
    while loop:
        for e in pg.event.get():
            if e.type == pg.QUIT:
                loop = False
        screen.fill(white)
        pipe_manager.manage_pipes(dt)
        pg.display.update()
        dt=clock.tick(FPS)