Search code examples
pythontimerpygamecountdown

How to make a wave timer in pygame


So, I'm totally new to programming (been doing it for a couple of months) and decided to try coding a game. On that note, a big thanks to Chris Bradfield for his series of tutorials in pygame coding, they are absolutely great! However, now that I'm done with the tutorials and need to work on my own, I've come across a problem. I'm making a top-down shooter and making it wave-based. So, when zombies in one wave die, I want to show a timer that counts down until the next wave begins. I THINK I'm down the right path atm, let me show you what I'm working with.

def new(self)
'''
    self.timer_flag = False
    self.x = threading.Thread(target=self.countdown, args=(TIME_BETWEEN_WAVES,))
'''

def countdown(self, time_between_waves):
    self.wave_timer = time_between_waves
    for i in range(TIME_BETWEEN_WAVES):
        while self.timer_flag:
            self.wave_timer -= 
            time.sleep(1)

def update(self)
'''
    self.countdown_has_run = False
    if len(self.mobs) == 0:
        self.timer_flag = True
        if not self.countdown_has_run:
            self.countdown_has_run = True
            self.x.start()
'''

Now, I also draw my timer when the timer_flag is True, but it doesn't decrement, so I assume the problem lies somewhere in calling/starting the threaded countdown function?

Also, it's my first time posting here, so please let me know what to do to format better etc for you to be able to help


Solution

  • Don't bother with threads. No need to make your live complicated.

    Usually, you use a Clock anyway in your game (if not, you should start using it) to limit the framerate, and to ensure that your world moves at a constant rante (if not, you should start doing it).

    So if you want to trigger something in, say, 5 seconds, just create a variable that holds the value 5000, and substract the time it took to process your last frame (which is returned by Clock.tick):

    clock = pygame.time.Clock()
    dt = 0
    timer = 5000
    while True:
        ...
        timer -= dt
        if timer <= 0:
           do_something()
        dt = clock.tick(60)
    

    I hacked together a simple example below. There, I use a simple class that is also a Sprite to draw the remaining time to the screen. When the timer runs out, it calls a function that creates a new wave of zombies.

    In the main loop, I check if there's no timer running and no zombies, and if that's the case, a new timer is created.

    Here's the code:

    import pygame
    import pygame.freetype
    import random
    
    # a dict that defines the controls
    # w moves up, s moves down etc
    CONTROLS = {
        pygame.K_w: ( 0, -1),
        pygame.K_s: ( 0,  1),
        pygame.K_a: (-1,  0),
        pygame.K_d: ( 1,  0)
    }
    
    # a function that handles the behaviour a sprite that
    # should be controled with the keys defined in CONTROLS
    def keyboard_controlled_b(player, events, dt):
    
        # let's see which keys are pressed, and create a 
        # movement vector from all pressed keys.
        move = pygame.Vector2()
        pressed = pygame.key.get_pressed()
    
        for vec in (CONTROLS[k] for k in CONTROLS if pressed[k]):
            move += vec
    
        if move.length():
            move.normalize_ip()
    
        move *= (player.speed * dt/10)
    
        # apply the movement vector to the position of the player sprite
        player.pos += move
        player.rect.center = player.pos
    
    # a function that let's a sprite follow another one
    # and kill it if they touch each other
    def zombie_runs_to_target_b(target):
        def zombie_b(zombie, events, dt):
    
            if target.rect.colliderect(zombie.rect):
                zombie.kill()
                return
    
            move = target.pos - zombie.pos
    
            if move.length():
                move.normalize_ip()
    
            move *= (zombie.speed * dt/10)
            zombie.pos += move
            zombie.rect.center = zombie.pos
    
        return zombie_b
    
    # a simple generic sprite class that displays a simple, colored rect
    # and invokes the given behaviour
    class Actor(pygame.sprite.Sprite):
    
        def __init__(self, color, pos, size, behavior, speed, *grps):
            super().__init__(*grps)
            self.image = pygame.Surface(size)
            self.image.fill(color)
            self.rect = self.image.get_rect(center=pos)
            self.pos = pygame.Vector2(pos)
            self.behavior = behavior
            self.speed = speed
    
        def update(self, events, dt):
            self.behavior(self, events, dt)
    
    # a sprite class that displays a timer
    # when the timer runs out, a function is invoked
    # and this sprite is killed
    class WaveCounter(pygame.sprite.Sprite):
    
        font = None
    
        def __init__(self, time_until, action, *grps):
            super().__init__(grps)
            self.image = pygame.Surface((300, 50))
            self.image.fill((3,2,1))
            self.image.set_colorkey((3, 2, 1))
            self.rect = self.image.get_rect(topleft=(10, 10))
    
            if not WaveCounter.font:
                WaveCounter.font = pygame.freetype.SysFont(None, 32)
    
            WaveCounter.font.render_to(self.image, (0, 0), f'new wave in {time_until}', (255, 255, 255))
            self.timer = time_until * 1000
            self.action = action
    
        def update(self, events, dt):
            self.timer -= dt
    
            self.image.fill((3,2,1))
            WaveCounter.font.render_to(self.image, (0, 0), f'new wave in {int(self.timer / 1000) + 1}', (255, 255, 255))
    
            if self.timer <= 0:
                self.action()
                self.kill()
    
    def main():
        pygame.init()
        screen = pygame.display.set_mode((600, 480))
        screen_rect = screen.get_rect()
        clock = pygame.time.Clock()
        dt = 0
        sprites_grp = pygame.sprite.Group()
        zombies_grp = pygame.sprite.Group()
        wave_tm_grp = pygame.sprite.GroupSingle()
    
        # the player is controlled with the keyboard
        player = Actor(pygame.Color('dodgerblue'), 
                       screen_rect.center, 
                       (32, 32), 
                       keyboard_controlled_b, 
                       5, 
                       sprites_grp)
    
        # this function should be invoked once the timer runs out
        def create_new_wave_func():
            # let's create a bunch of zombies that follow the player
            for _ in range(15):
                x = random.randint(0, screen_rect.width)
                y = random.randint(-100, 0)
                Actor((random.randint(180, 255), 0, 0), 
                      (x, y), 
                      (26, 26), 
                      zombie_runs_to_target_b(player), 
                      random.randint(2, 4), 
                      sprites_grp, zombies_grp)
    
        while True:
            events = pygame.event.get()
            for e in events:
                if e.type == pygame.QUIT:
                    return
    
            # no timer, no zombies => create new timer
            if len(wave_tm_grp) == 0 and len(zombies_grp) == 0:
                WaveCounter(5, create_new_wave_func, sprites_grp, wave_tm_grp)
    
            sprites_grp.update(events, dt)
    
            screen.fill((80, 80, 80))
            sprites_grp.draw(screen)
            pygame.display.flip()
            dt = clock.tick(60)
    
    if __name__ == '__main__':
        main()
    

    enter image description here