Roni
Roni

Reputation: 649

Making an Obstacle System for an Endless Runner Game in Pygame

I'm trying to make a clone of google chrome dinosaur game in Python pygame. I don't have much experience with pygame. I managed to create all of the dinosaur sprite animations (walking, jumping and bending) and now I'm trying to add the obstacles. So what I want to do is when the user's score surpasses 35, obstacles will start being randomly generated. The problem is that when the score surpasses 35, obstacles appear on top of each other, some appear and disappear constantly and it just looks bad and very laggy. How can I render the obstacles more smoothly? The relevant parts of the code:

import pygame
import random
import os
from threading import Thread
pygame.init()

w, h = pygame.display.get_surface().get_size()
imgX = w
def generate_obstacles():
    global imgX
    obstacles_tuple = (obstacle1, obstacle2, obstacle3, obstacle4, obstacle5)
    chosen_obstacle = random.choice(obstacles_tuple)
    try:
        if WINDOW.get_width() >= imgX > 0:
            WINDOW.blit(chosen_obstacle, (imgX, 323))
        else:
            imgX = w
    except pygame.error:
        quit()

    pygame.display.update()


def draw():
    WINDOW.fill("#FFFFFF")
    pygame.display.update()


r1, r2 = 5000, 7000
def timer(score):
    global r1, r2
    timer_running = True
    while timer_running:
        rand_time = random.randint(r1, r2)
        pygame.time.delay(rand_time)
        generate_obstacles()

        if r1 >= 500 and r2 >= 500:
            if score % 100 == 0:
                r1 -= 20
                r2 -= 20
        keys = pygame.key.get_pressed()
        if event.type == pygame.QUIT or keys[pygame.K_ESCAPE]:
            break



game_intro(True)
run = True
my_sprite = Dino()
speed = 5
while run:
    pygame.time.delay(100)
    bgX -= speed
    bgX2 -= speed
    imgX -= speed
    if bgX < floor.get_width() * -1:
        bgX = floor.get_width()
    if bgX2 < floor.get_width() * -1:
        bgX2 = floor.get_width()
    for event in pygame.event.get():
        keys = pygame.key.get_pressed()
        if event.type == pygame.QUIT or keys[pygame.K_ESCAPE]:
            with open("highscore.txt", "w") as f:
                f.write(str(my_sprite.high_score))
            run = False
            pygame.quit()
            quit()

        if event.type == pygame.USEREVENT+1:
            my_sprite.fall()

        if (keys[pygame.K_UP] or keys[pygame.K_SPACE]) and not my_sprite.is_jumping:
            my_sprite.jump()

        if keys[pygame.K_DOWN]:
            my_sprite.bend()

    draw()
    my_sprite.walk()

    if my_sprite.score >= 35:
        Thread(target=lambda: timer(my_sprite.score)).start()

I also made a video so you can get a better understanding of what the problem is.

Full code:

import pygame
import random
import os
from threading import Thread
pygame.init()

WINDOW = pygame.display.set_mode((500, 500))
pygame.display.set_caption("Dinosaur Game")
walk1_image = pygame.image.load("images/walk1.png")
walk1 = pygame.transform.scale(walk1_image, (64, 64))
bend1_image = pygame.image.load("images/bend1.png")
bend1 = pygame.transform.scale(bend1_image, (64, 64))
bend2_image = pygame.image.load("images/bend2.png")
bend2 = pygame.transform.scale(bend2_image, (64, 64))
walk2_image = pygame.image.load("images/walk2.png")
walk2 = pygame.transform.scale(walk2_image, (64, 64))
die_image = pygame.image.load("images/die.png")
die = pygame.transform.scale(die_image, (64, 64))
jump_image = pygame.image.load("images/jump.png")
jump = pygame.transform.scale(jump_image, (64, 64))

images_list = [walk1, walk2, die, jump, bend1]
floor_image = pygame.image.load("obstacles/floor-0.png").convert()
floor = pygame.transform.scale(floor_image, (500, 100))
bgX = 0
bgX2 = floor.get_width()
in_main = False

obstacle1_image = pygame.image.load("obstacles/obstacle1.png")
obstacle1 = pygame.transform.scale(obstacle1_image, (78, 78))
obstacle2_image = pygame.image.load("obstacles/obstacle2.png")
obstacle2 = pygame.transform.scale(obstacle2_image, (156, 78))
obstacle3_image = pygame.image.load("obstacles/obstacle3.png")
obstacle3 = pygame.transform.scale(obstacle3_image, (64, 64))
obstacle4_image = pygame.image.load("obstacles/obstacle4.png")
obstacle4 = pygame.transform.scale(obstacle4_image, (128, 64))
obstacle5_image = pygame.image.load("obstacles/obstacle5.png")
obstacle5 = pygame.transform.scale(obstacle5_image, (192, 64))

def game_intro(intro):
    while intro:
        WINDOW.fill("#FFFFFF")
        my_font = pygame.font.SysFont("comicsans", 40)
        label = my_font.render("Press space to play", True, (105, 105, 105))
        WINDOW.blit(jump, (125, 200))
        WINDOW.blit(label, (125, 300))
        my_font = pygame.font.SysFont("comicsans", 20)
        label2 = my_font.render("Made by: Roni", True, (0, 0, 0))
        WINDOW.blit(label2, (10, 480))
        pygame.display.update()
        for event2 in pygame.event.get():
            keys2 = pygame.key.get_pressed()
            if event2.type == pygame.QUIT or keys2[pygame.K_ESCAPE]:
                intro = False
                pygame.quit()
                quit()

            if keys2[pygame.K_SPACE]:
                if not os.path.isfile("highscore.txt"):
                    f = open("highscore.txt", "w")
                    f.write("0")
                    f.close()
                intro = False
                return


class Dino(pygame.sprite.Sprite):
    def __init__(self):
        super(Dino, self).__init__()
        self.images = [walk1, walk2]
        self.index = 0
        self.image = self.images[self.index]
        self.x = 200
        self.y = 323
        self.is_falling = False
        self.is_jumping = False
        self.is_bend = False
        self.is_down = False
        self.score = 0
        self.high_score = 0

    def walk(self):
        self.update_score()
        WINDOW.blit(floor, (bgX, 300))
        WINDOW.blit(floor, (bgX2, 300))
        self.score += 1

        if self.is_jumping:
            WINDOW.blit(jump, (self.x, self.y))
        elif self.is_bend:
            bend_images = [bend1, bend2]
            keys3 = pygame.key.get_pressed()
            self.index += 1
            if self.index >= len(self.images):
                self.index = 0
            bend = bend_images[self.index]
            if keys3[pygame.K_DOWN]:
                WINDOW.blit(bend, (self.x, self.y))
            else:
                self.is_bend = False
                self.walk()
        else:
            self.index += 1
            if self.index >= len(self.images):
                self.index = 0
            self.image = self.images[self.index]
            WINDOW.blit(self.image, (self.x, self.y))

        pygame.display.update()

    def jump(self):
        self.is_jumping = True
        for i in range(3):
            self.y -= 20
            self.walk()
            pygame.time.delay(10)
            WINDOW.fill("#FFFFFF")

        self.is_falling = True
        pygame.time.set_timer(pygame.USEREVENT+1, 1000)

    def fall(self):
        if self.is_falling:
            while self.y != 323:
                self.y += 20
                self.walk()
                WINDOW.fill("#FFFFFF")
                self.is_falling = False
                self.is_jumping = False
                pygame.display.update()

    def bend(self):
        self.is_bend = True
        self.walk()

    def update_score(self):
        my_font = pygame.font.SysFont("comicsans", 40)
        zeros = 5 - len(str(self.score))
        str_score = "0" * zeros + str(self.score)
        label = my_font.render(str_score, True, (0, 0, 0))
        WINDOW.blit(label, (410, 0))

        with open("highscore.txt", "r") as f:
            self.high_score = int(f.read())

        if self.high_score <= self.score:
            self.high_score = self.score

        zeros2 = 5 - len(str(self.high_score))
        str_score2 = "0" * zeros2 + str(self.high_score)
        label2 = my_font.render("HI: " + str_score2, True, (0, 0, 0))
        WINDOW.blit(label2, (250, 0))
        pygame.display.update()


w, h = pygame.display.get_surface().get_size()
imgX = w
def generate_obstacles():
    global imgX
    obstacles_tuple = (obstacle1, obstacle2, obstacle3, obstacle4, obstacle5)
    chosen_obstacle = random.choice(obstacles_tuple)
    try:
        if WINDOW.get_width() >= imgX > 0:
            WINDOW.blit(chosen_obstacle, (imgX, 323))
        else:
            imgX = w
    except pygame.error:
        quit()

    pygame.display.update()


def draw():
    WINDOW.fill("#FFFFFF")
    pygame.display.update()


r1, r2 = 5000, 7000
def timer(score):
    global r1, r2
    timer_running = True
    while timer_running:
        rand_time = random.randint(r1, r2)
        pygame.time.delay(rand_time)
        generate_obstacles()

        if r1 >= 500 and r2 >= 500:
            if score % 100 == 0:
                r1 -= 20
                r2 -= 20
        keys = pygame.key.get_pressed()
        if event.type == pygame.QUIT or keys[pygame.K_ESCAPE]:
            break



game_intro(True)
run = True
my_sprite = Dino()
speed = 5
while run:
    pygame.time.delay(100)
    bgX -= speed
    bgX2 -= speed
    imgX -= speed
    if bgX < floor.get_width() * -1:
        bgX = floor.get_width()
    if bgX2 < floor.get_width() * -1:
        bgX2 = floor.get_width()
    for event in pygame.event.get():
        keys = pygame.key.get_pressed()
        if event.type == pygame.QUIT or keys[pygame.K_ESCAPE]:
            with open("highscore.txt", "w") as f:
                f.write(str(my_sprite.high_score))
            run = False
            pygame.quit()
            quit()

        if event.type == pygame.USEREVENT+1:
            my_sprite.fall()

        if (keys[pygame.K_UP] or keys[pygame.K_SPACE]) and not my_sprite.is_jumping:
            my_sprite.jump()

        if keys[pygame.K_DOWN]:
            my_sprite.bend()

    draw()
    my_sprite.walk()

    if my_sprite.score >= 35:
        Thread(target=lambda: timer(my_sprite.score)).start()

The problem was solved thanks to @qouify. You can look at his answer to see what solved the problem. Full updated code:

import pygame
import random
import os
from threading import Thread
pygame.init()

WINDOW = pygame.display.set_mode((500, 500))
pygame.display.set_caption("Dinosaur Game")
walk1_image = pygame.image.load("Dino/walk1.png")
walk1 = pygame.transform.scale(walk1_image, (64, 64))
bend1_image = pygame.image.load("Dino/bend1.png")
bend1 = pygame.transform.scale(bend1_image, (64, 64))
bend2_image = pygame.image.load("Dino/bend2.png")
bend2 = pygame.transform.scale(bend2_image, (64, 64))
walk2_image = pygame.image.load("Dino/walk2.png")
walk2 = pygame.transform.scale(walk2_image, (64, 64))
die_image = pygame.image.load("Dino/die.png")
die = pygame.transform.scale(die_image, (64, 64))
jump_image = pygame.image.load("Dino/jump.png")
jump = pygame.transform.scale(jump_image, (64, 64))

images_list = [walk1, walk2, die, jump, bend1]
floor_image = pygame.image.load("Others/floor-0.png").convert()
floor = pygame.transform.scale(floor_image, (500, 100))
bgX = 0
bgX2 = floor.get_width()
in_main = False

obstacle1_image = pygame.image.load("Obstacles/obstacle1.png")
obstacle1 = pygame.transform.scale(obstacle1_image, (78, 78))
obstacle2_image = pygame.image.load("Obstacles/obstacle2.png")
obstacle2 = pygame.transform.scale(obstacle2_image, (156, 78))
obstacle3_image = pygame.image.load("Obstacles/obstacle3.png")
obstacle3 = pygame.transform.scale(obstacle3_image, (64, 64))
obstacle4_image = pygame.image.load("Obstacles/obstacle4.png")
obstacle4 = pygame.transform.scale(obstacle4_image, (128, 64))
obstacle5_image = pygame.image.load("Obstacles/obstacle5.png")
obstacle5 = pygame.transform.scale(obstacle5_image, (192, 64))

def game_intro(intro):
    while intro:
        WINDOW.fill("#FFFFFF")
        my_font = pygame.font.SysFont("comicsans", 40)
        label = my_font.render("Press space to play", True, (105, 105, 105))
        WINDOW.blit(jump, (125, 200))
        WINDOW.blit(label, (125, 300))
        my_font = pygame.font.SysFont("comicsans", 20)
        label2 = my_font.render("Made by: Roni Meirom", True, (0, 0, 0))
        WINDOW.blit(label2, (10, 480))
        pygame.display.update()
        for event2 in pygame.event.get():
            keys2 = pygame.key.get_pressed()
            if event2.type == pygame.QUIT or keys2[pygame.K_ESCAPE]:
                intro = False
                pygame.quit()
                quit()

            if keys2[pygame.K_SPACE]:
                if not os.path.isfile("highscore.txt"):
                    f = open("highscore.txt", "w")
                    f.write("0")
                    f.close()
                intro = False
                return


class Dino(pygame.sprite.Sprite):
    def __init__(self):
        super(Dino, self).__init__()
        self.images = [walk1, walk2]
        self.index = 0
        self.image = self.images[self.index]
        self.x = 200
        self.y = 323
        self.is_falling = False
        self.is_jumping = False
        self.is_bend = False
        self.is_down = False
        self.score = 0
        self.high_score = 0

    def walk(self):
        self.update_score()
        WINDOW.blit(floor, (bgX, 300))
        WINDOW.blit(floor, (bgX2, 300))
        self.score += 1

        if self.is_jumping:
            WINDOW.blit(jump, (self.x, self.y))
        elif self.is_bend:
            bend_images = [bend1, bend2]
            keys3 = pygame.key.get_pressed()
            self.index += 1
            if self.index >= len(self.images):
                self.index = 0
            bend = bend_images[self.index]
            if keys3[pygame.K_DOWN]:
                WINDOW.blit(bend, (self.x, self.y))
            else:
                self.is_bend = False
                self.walk()
        else:
            self.index += 1
            if self.index >= len(self.images):
                self.index = 0
            self.image = self.images[self.index]
            WINDOW.blit(self.image, (self.x, self.y))


    def jump(self):
        self.is_jumping = True
        for i in range(3):
            self.y -= 20
            self.walk()
            pygame.time.delay(10)
            WINDOW.fill("#FFFFFF")

        self.is_falling = True
        pygame.time.set_timer(pygame.USEREVENT+1, 1000)

    def fall(self):
        if self.is_falling:
            self.y += 20
            if self.y != 323:
                self.y = 323
                self.is_falling = False
                self.is_jumping = False

    def bend(self):
        self.is_bend = True
        self.walk()

    def update_score(self):
        my_font = pygame.font.SysFont("comicsans", 40)
        zeros = 5 - len(str(self.score))
        str_score = "0" * zeros + str(self.score)
        label = my_font.render(str_score, True, (0, 0, 0))
        WINDOW.blit(label, (410, 0))

        with open("highscore.txt", "r") as f:
            self.high_score = int(f.read())

        if self.high_score <= self.score:
            self.high_score = self.score

        zeros2 = 5 - len(str(self.high_score))
        str_score2 = "0" * zeros2 + str(self.high_score)
        label2 = my_font.render("HI: " + str_score2, True, (0, 0, 0))
        WINDOW.blit(label2, (250, 0))


w, h = pygame.display.get_surface().get_size()
imgX = w
def generate_obstacles():
    global imgX
    obstacles_tuple = (obstacle1, obstacle2, obstacle3, obstacle4, obstacle5)
    chosen_obstacle = random.choice(obstacles_tuple)
    try:
        if WINDOW.get_width() >= imgX > 0:
            WINDOW.blit(chosen_obstacle, (imgX, 323))
        else:
            imgX = w
    except pygame.error:
        quit()


r1, r2 = 5000, 7000
def timer(score):
    global r1, r2
    timer_running = True
    while timer_running:
        rand_time = random.randint(r1, r2)
        pygame.time.delay(rand_time)
        generate_obstacles()

        if r1 >= 500 and r2 >= 500:
            if score % 100 == 0:
                r1 -= 20
                r2 -= 20
        keys = pygame.key.get_pressed()
        if event.type == pygame.QUIT or keys[pygame.K_ESCAPE]:
            break



game_intro(True)
run = True
my_sprite = Dino()
speed = 5
while run:
    bgX -= speed
    bgX2 -= speed
    imgX -= speed
    if bgX < floor.get_width() * -1:
        bgX = floor.get_width()
    if bgX2 < floor.get_width() * -1:
        bgX2 = floor.get_width()
    for event in pygame.event.get():
        keys = pygame.key.get_pressed()
        if event.type == pygame.QUIT or keys[pygame.K_ESCAPE]:
            with open("highscore.txt", "w") as f:
                f.write(str(my_sprite.high_score))
            run = False
            pygame.quit()
            quit()

        if event.type == pygame.USEREVENT+1:
            my_sprite.fall()

        if (keys[pygame.K_UP] or keys[pygame.K_SPACE]) and not my_sprite.is_jumping:
            my_sprite.jump()

        if keys[pygame.K_DOWN]:
            my_sprite.bend()

    WINDOW.fill("#FFFFFF")
    my_sprite.walk()

    if my_sprite.score >= 35:
        Thread(target=lambda: timer(my_sprite.score)).start()

    pygame.time.delay(100)
    pygame.display.update()

Upvotes: 0

Views: 1476

Answers (1)

qouify
qouify

Reputation: 3920

It suspect that one issue with your code is that pygame.display.update() can be called concurrently by:

  • the main program via the draw function ;
  • and by the thread(s) you launch when the score is greater than 35 via the draw_obstacles function.

This may explain why some obstacles appear and disappear very quickly. Maybe, just removing the pygame.display.update() in function draw_obstacles could fix that.

Another issue that may explain the lagging is related to your condition to launch the thread. It seems to me that, as soon as the score exceeds 35, you will launch another thread at each iteration of the main loop, which means a lot of threads. So, unless there is in your code some condition that can prevent that, you should add one.

[EDIT] After watching your full code I noticed a few issues.

First, as already mentionned, you should do pygame.display.update() only once in your main loop. Same for WINDOW.fill that you have put have several places in your code. That's what causes the effect of objects appearing and disappearing very quickly.

Your program (and every pygame program) should have the following structure.

while run:
    process_events_and_change_objects_status()
    WINDOW.fill("#FFFFFF")
    redraw_everything()
    pygame.display.update()
    wait()

And there should not be any other call to WINDOW.fill or pygame.display.update() anywhere else.

Second, looking at the fall method:

def fall(self):
    if self.is_falling:
        while self.y != 323:
            self.y += 20
            self.walk()
            WINDOW.fill("#FFFFFF")
            self.is_falling = False
            self.is_jumping = False
            pygame.display.update()

Here you're trying to do two things at the same time: update the object status and redraw everything with self.walk. That's bad because, for example, if there are currently obstacles on the screen, they won't be drawned since self.walk does not care about obstacles. Moreover this means that events cannot be processed while the dinosaur is falling. So you have to change it to only update the object status with something like that.

def fall(self):
    if self.is_falling:
        self.y += 20
        if self.y != 323:
            self.is_falling = False
            self.is_jumping = False

Then redrawing will be managed by self.walk in the main loop. Same for bend and jump.

So I think that you need to modify your program structure in order to clearly separate in your code the drawing part from the processing part (processing events and updating the dinosaur status). Otherwise if you mix both you will face the kind of problems you are facing now and your code will be impossible to maintain and evolve.

Upvotes: 1

Related Questions