[Pygame] Space Invaders clone 'alien wave' movement issues

[Pygame] Space Invaders clone 'alien wave' movement issues

Postby russb78 » Mon Mar 31, 2014 1:41 pm

I've written a basic Pygame implementation of Space Invaders (full code on Github) and one thing has been niggling me for ages. The waves of aliens that move down the screen properly.

In the original game, if you killed a full column of aliens on either side of the screen, the remaining aliens would continue to move to the edge of the screen before descending further - I can't work out how to recreate this behaviour in my game. My aliens simply move a set number of times horizontally before dropping down vertically (and repeating).

I know the issue is centred around the making of the alien wave (line 268) and the update method in the Alien class starting on line 38. Any help overcoming this issue would be greatly appreciated. I'm guessing I need to create a 2D array when making an alien wave and re-writing the update method for them, but I have no idea how to get started.

Thanks in advance if you can get me on the right track!
Last edited by stranac on Mon Mar 31, 2014 2:04 pm, edited 1 time in total.
Reason: First post lock.
russb78
 
Posts: 3
Joined: Mon Mar 31, 2014 1:29 pm

Re: [Pygame] Space Invaders clone 'alien wave' movement issu

Postby Mekire » Mon Mar 31, 2014 4:50 pm

Just hacked in for the moment but maybe it will give you some inspiration.

pivaders.py:
Code: Select all
#!/usr/bin/env python2

import pygame, random

BLACK = (0, 0, 0)
BLUE = (0, 0, 255)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
ALIEN_SIZE = (30, 40)
ALIEN_SPACER = 20
BARRIER_ROW = 10
BARRIER_COLUMN = 4
BULLET_SIZE = (5, 10)
MISSILE_SIZE = (5, 5)
BLOCK_SIZE = (10, 10)
RES = (800, 600)


class Player(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.size = (64, 61)
        self.rect = self.image.get_rect()
        self.rect.x = (RES[0] / 2) - (self.size[0] / 2)
        self.rect.y = 520
        self.travel = 7
        self.speed = 350
        self.time = pygame.time.get_ticks()

    def update(self):
        self.rect.x += GameState.vector * self.travel
        if self.rect.x < 0:
            self.rect.x = 0
        elif self.rect.x > RES[0] - self.size[0]:
            self.rect.x = RES[0] - self.size[0]


class Alien(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.size = (ALIEN_SIZE)
        self.rect = self.image.get_rect()
        self.need_switch = False
        self.travel = [(ALIEN_SIZE[0] - 7), ALIEN_SPACER]
        self.speed = 700
        self.time = pygame.time.get_ticks()

    def update(self, switch, direction):
        #Alien update significantly changed.
        if switch:
            self.need_switch = True
        if GameState.alien_time - self.time > self.speed:
            if self.need_switch:
                self.rect.y += self.travel[1]
                self.speed = max(self.speed-20, 100)
                self.need_switch = False
            else:
                self.rect.x += direction*self.travel[0]
            self.time = GameState.alien_time


class Ammo(pygame.sprite.Sprite):
    def __init__(self, color, (width, height)):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface([width, height])
        self.image.fill(color)
        self.rect = self.image.get_rect()
        self.speed = 0
        self.vector = 0

    def update(self):
        self.rect.y += self.vector * self.speed
        if self.rect.y < 0 or self.rect.y > RES[1]:
            self.kill()


class Block(pygame.sprite.Sprite):
    def __init__(self, color, (width, height)):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface([width, height])
        self.image.fill(color)
        self.rect = self.image.get_rect()


class GameState:
    pass


class Game(object):
    def __init__(self):
        pygame.init()
        pygame.font.init()
        self.clock = pygame.time.Clock()
        self.game_font = pygame.font.Font(
            'data/Orbitracer.ttf', 28)
        self.intro_font = pygame.font.Font(
            'data/Orbitracer.ttf', 72)
        self.screen = pygame.display.set_mode([RES[0], RES[1]])
        self.screen_rect = self.screen.get_rect()
        self.time = pygame.time.get_ticks()
        self.refresh_rate = 20
        self.rounds_won = 0
        self.level_up = 50
        self.score = 0
        self.lives = 2
        self.player_group = pygame.sprite.Group()
        self.aliens = pygame.sprite.Group()
        self.bullets = pygame.sprite.Group()
        self.missiles = pygame.sprite.Group()
        self.barrier_group = pygame.sprite.Group()
        self.all_sprite_list = pygame.sprite.Group()
        self.intro_screen = pygame.image.load(
            'data/graphics/start_screen.jpg').convert()
        self.background = pygame.image.load(
            'data/graphics/Space-Background.jpg').convert()
        pygame.display.set_caption('Pivaders - ESC to exit')
        pygame.mouse.set_visible(False)
        Alien.image = pygame.image.load(
            'data/graphics/Spaceship16.png').convert()
        Alien.image.set_colorkey(WHITE)
        self.ani_pos = 5  # 11 images of ship leaning from left to right. 5th image is 'central'
        self.ship_sheet = pygame.image.load(
            'data/graphics/ship_sheet_final.png').convert_alpha()
        Player.image = self.ship_sheet.subsurface(self.ani_pos * 64, 0, 64, 61)
        self.animate_right = False
        self.animate_left = False
        self.explosion_sheet = pygame.image.load(
            'data/graphics/explosion_new1.png').convert_alpha()
        self.explosion_image = self.explosion_sheet.subsurface(0, 0, 79, 96)
        self.alien_explosion_sheet = pygame.image.load(
            'data/graphics/alien_explosion.png')
        self.alien_explode_graphics = self.alien_explosion_sheet.subsurface(0, 0, 94, 96)
        self.explode = False
        self.explode_pos = 0
        self.alien_explode = False
        self.alien_explode_pos = 0
        pygame.mixer.music.load('data/sound/10_Arpanauts.ogg')
        pygame.mixer.music.play(-1)
        pygame.mixer.music.set_volume(0.7)
        self.bullet_fx = pygame.mixer.Sound(
            'data/sound/medetix__pc-bitcrushed-lazer-beam.ogg')
        self.explosion_fx = pygame.mixer.Sound(
            'data/sound/timgormly__8-bit-explosion.ogg')
        self.explosion_fx.set_volume(0.5)
        self.explodey_alien = []
        GameState.end_game = False
        GameState.start_screen = True
        GameState.vector = 0
        GameState.shoot_bullet = False
        self.direction = 1

    def control(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                GameState.start_screen = False
                GameState.end_game = True
            if event.type == pygame.KEYDOWN \
                    and event.key == pygame.K_ESCAPE:
                if GameState.start_screen:
                    GameState.start_screen = False
                    GameState.end_game = True
                    self.kill_all()
                else:
                    GameState.start_screen = True
        self.keys = pygame.key.get_pressed()
        if self.keys[pygame.K_LEFT]:
            GameState.vector = -1
            self.animate_left = True
            self.animate_right = False
        elif self.keys[pygame.K_RIGHT]:
            GameState.vector = 1
            self.animate_right = True
            self.animate_left = False

        else:
            GameState.vector = 0
            self.animate_right = False
            self.animate_left = False

        if self.keys[pygame.K_SPACE]:
            if GameState.start_screen:
                GameState.start_screen = False
                self.lives = 2
                self.score = 0
                self.make_player()
                self.make_defenses()
                self.alien_wave(0)
            else:
                GameState.shoot_bullet = True
                self.bullet_fx.play()

    def animate_player(self):
        if self.animate_right:
            if self.ani_pos < 10:
                Player.image = self.ship_sheet.subsurface(self.ani_pos * 64, 0, 64, 61)
                self.ani_pos += 1
        else:
            if self.ani_pos > 5:
                self.ani_pos -= 1
                Player.image = self.ship_sheet.subsurface(self.ani_pos * 64, 0, 64, 61)

        if self.animate_left:
            if self.ani_pos > 0:
                self.ani_pos -= 1
                Player.image = self.ship_sheet.subsurface(self.ani_pos * 64, 0, 64, 61)
        else:
            if self.ani_pos < 5:
                Player.image = self.ship_sheet.subsurface(self.ani_pos * 64, 0, 64, 61)
                self.ani_pos += 1

    def player_explosion(self):
        if self.explode:
            if self.explode_pos < 8:
                self.explosion_image = self.explosion_sheet.subsurface(0, self.explode_pos * 96, 79, 96)
                self.explode_pos += 1
                self.screen.blit(self.explosion_image, [self.player.rect.x - 10, self.player.rect.y - 30])
            else:
                self.explode = False
                self.explode_pos = 0

    def alien_explosion(self):
        if self.alien_explode:
            if self.alien_explode_pos < 9:
                self.alien_explode_graphics = self.alien_explosion_sheet.subsurface(
                    0, self.alien_explode_pos * 96, 94, 96)
                self.alien_explode_pos += 1
                self.screen.blit(self.alien_explode_graphics, [
                    int(self.explodey_alien[0]) - 50, int(self.explodey_alien[1]) - 60])
            else:
                self.alien_explode = False
                self.alien_explode_pos = 0
                self.explodey_alien = []

    def splash_screen(self):
        while GameState.start_screen:
            self.kill_all()
            self.screen.blit(self.intro_screen, [0, 0])
            self.screen.blit(self.intro_font.render(
                "PIVADERS", 1, WHITE), (265, 120))
            self.screen.blit(self.game_font.render(
                "PRESS SPACE TO PLAY", 1, WHITE), (274, 191))
            pygame.display.flip()
            self.control()
            self.clock.tick(self.refresh_rate / 2)

    def make_player(self):
        self.player = Player()
        self.player_group.add(self.player)
        self.all_sprite_list.add(self.player)

    def refresh_screen(self):
        self.all_sprite_list.draw(self.screen)
        self.animate_player()
        self.player_explosion()
        self.alien_explosion()
        self.refresh_scores()
        pygame.display.flip()
        self.screen.blit(self.background, [0, 0])
        self.clock.tick(self.refresh_rate)

    def refresh_scores(self):
        self.screen.blit(self.game_font.render(
            "SCORE " + str(self.score), 1, WHITE), (10, 8))
        self.screen.blit(self.game_font.render(
            "LIVES " + str(self.lives + 1), 1, RED), (355, 575))

    def alien_wave(self, speed):
        for column in range(BARRIER_COLUMN):
            for row in range(BARRIER_ROW):
                alien = Alien()
                alien.rect.y = 65 + (column * (
                    ALIEN_SIZE[1] + ALIEN_SPACER))
                alien.rect.x = ALIEN_SPACER + (
                    row * (ALIEN_SIZE[0] + ALIEN_SPACER))
                self.aliens.add(alien)
                self.all_sprite_list.add(alien)
                alien.speed -= speed

    def make_bullet(self):
        if GameState.game_time - self.player.time > self.player.speed:
            bullet = Ammo(BLUE, BULLET_SIZE)
            bullet.vector = -1
            bullet.speed = 26
            bullet.rect.x = self.player.rect.x + 28
            bullet.rect.y = self.player.rect.y
            self.bullets.add(bullet)
            self.all_sprite_list.add(bullet)
            self.player.time = GameState.game_time
        GameState.shoot_bullet = False

    def make_missile(self):
        if len(self.aliens):
            shoot = random.random()
            if shoot <= 0.05:
                shooter = random.choice([
                    alien for alien in self.aliens])
                missile = Ammo(RED, MISSILE_SIZE)
                missile.vector = 1
                missile.rect.x = shooter.rect.x + 15
                missile.rect.y = shooter.rect.y + 40
                missile.speed = 10
                self.missiles.add(missile)
                self.all_sprite_list.add(missile)

    def make_barrier(self, columns, rows, spacer):
        for column in range(columns):
            for row in range(rows):
                barrier = Block(WHITE, (BLOCK_SIZE))
                barrier.rect.x = 55 + (200 * spacer) + (row * 10)
                barrier.rect.y = 450 + (column * 10)
                self.barrier_group.add(barrier)
                self.all_sprite_list.add(barrier)

    def make_defenses(self):
        for spacing, spacing in enumerate(xrange(4)):
            self.make_barrier(3, 9, spacing)

    def kill_all(self):
        for items in [self.bullets, self.player_group,
                      self.aliens, self.missiles, self.barrier_group]:
            for i in items:
                i.kill()

    def is_dead(self):
        if self.lives < 0:
            self.screen.blit(self.game_font.render(
                "The war is lost! You scored: " + str(
                    self.score), 1, RED), (250, 15))
            self.rounds_won = 0
            self.refresh_screen()
            self.level_up = 50
            self.explode = False
            self.alien_explode = False
            pygame.time.delay(3000)
            return True

    def defenses_breached(self):
        for alien in self.aliens:
            if alien.rect.y > 410:
                self.screen.blit(self.game_font.render(
                    "The aliens have breached Earth defenses!",
                    1, RED), (180, 15))
                self.refresh_screen()
                self.level_up = 50
                self.explode = False
                self.alien_explode = False
                pygame.time.delay(3000)
                return True

    def win_round(self):
        if len(self.aliens) < 1:
            self.rounds_won += 1
            self.screen.blit(self.game_font.render(
                "You won round " + str(self.rounds_won) +
                "  but the battle rages on", 1, RED), (200, 15))
            self.refresh_screen()
            pygame.time.delay(3000)
            return True

    def next_round(self):
        self.explode = False
        self.alien_explode = False
        for actor in [self.missiles,
                      self.barrier_group, self.bullets]:
            for i in actor:
                i.kill()
        self.alien_wave(self.level_up)
        self.make_defenses()
        self.level_up += 50

    def calc_collisions(self):
        pygame.sprite.groupcollide(
            self.missiles, self.barrier_group, True, True)
        pygame.sprite.groupcollide(
            self.bullets, self.barrier_group, True, True)

        for z in pygame.sprite.groupcollide(
                self.bullets, self.aliens, True, True):
            self.alien_explode = True
            self.explodey_alien.append(z.rect.x)
            self.explodey_alien.append(z.rect.y)
            self.score += 10
            self.explosion_fx.play()

        if pygame.sprite.groupcollide(
                self.player_group, self.missiles, False, True):
            self.lives -= 1
            self.explode = True
            self.explosion_fx.play()

    def main_loop(self):
        while not GameState.end_game:
            while not GameState.start_screen:
                GameState.game_time = pygame.time.get_ticks()
                GameState.alien_time = pygame.time.get_ticks()
                self.control()
                self.make_missile()
                self.calc_collisions()
                self.refresh_screen()
                if self.is_dead() or self.defenses_breached():
                    GameState.start_screen = True
                #New stuff here
                for group in [self.player_group, self.bullets, self.missiles]:
                    group.update()
                if self.aliens:
                    alien_rects = [alien.rect for alien in self.aliens]
                    all_aliens_rect = alien_rects[0].unionall(alien_rects[1:])
                    all_aliens_rect.move_ip((ALIEN_SIZE[0] - 7)*self.direction, 0)
                    switch = not self.screen_rect.contains(all_aliens_rect)
                    self.aliens.update(switch, self.direction)
                    if switch:
                        self.direction *= -1
                #End new stuff
                if GameState.shoot_bullet:
                    self.make_bullet()
                if self.win_round():
                    self.next_round()
            self.splash_screen()
        pygame.quit()


if __name__ == '__main__':
    pv = Game()
    pv.main_loop()

Let me know if you need help understanding. All changes are in the Alien update method, and the main loop.

-Mek
User avatar
Mekire
 
Posts: 988
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: [Pygame] Space Invaders clone 'alien wave' movement issu

Postby russb78 » Mon Mar 31, 2014 8:14 pm

Thanks for making the changes - that's brilliant.

It would really help me out if you could spell out some of the changes - it'd really help me understand what you've done.

The bits I'm really struggling with are mainly in the following section. Any chance you could walk me through it? I've never used union or move_ip and I didn't even know the final line here was even possible :)

I've clearly got a lot to learn...

Code: Select all
if self.aliens:
    alien_rects = [alien.rect for alien in self.aliens]
    all_aliens_rect = alien_rects[0].unionall(alien_rects[1:])
    all_aliens_rect.move_ip((ALIEN_SIZE[0] - 7)*self.direction, 0)
    switch = not self.screen_rect.contains(all_aliens_rect)
russb78
 
Posts: 3
Joined: Mon Mar 31, 2014 1:29 pm

Re: [Pygame] Space Invaders clone 'alien wave' movement issu

Postby Mekire » Mon Mar 31, 2014 10:35 pm

Code: Select all
if self.aliens:
If there are any sprites in the self.aliens group

Code: Select all
    alien_rects = [alien.rect for alien in self.aliens]
Create a list of all their rects

Code: Select all
    all_aliens_rect = alien_rects[0].unionall(alien_rects[1:])
Use pygame.Rect.unionall to merge all these rects into one big rect. This gives us a rect that fits all the enemies on the screen.

Code: Select all
    all_aliens_rect.move_ip((ALIEN_SIZE[0] - 7)*self.direction, 0)
Shift the rect over one space to check if the aliens have space to move.

Code: Select all
    switch = not self.screen_rect.contains(all_aliens_rect)
This line is identically equivalent to this:
Code: Select all
if self.screen_rect.contains(all_aliens_rect):
    switch = False
else:
    switch = True
If the rect containing all the enemies is still completely on the screen after being moved one space over, then the enemies keep going. If the screen rect no longer contains this shifted rect, set the flag to true.

And finally:
Code: Select all
    self.aliens.update(switch, self.direction)
    if switch:
        self.direction *= -1
update the aliens, passing them the switch flag and current direction. if switch was true, flip direction.

-Mek
User avatar
Mekire
 
Posts: 988
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: [Pygame] Space Invaders clone 'alien wave' movement issu

Postby russb78 » Mon Mar 31, 2014 11:33 pm

Thanks Mek - really appreciate you taking the time to spell this out.

It's great to learn more Rect methods and a bit of list comprehension.

Cheers!
russb78
 
Posts: 3
Joined: Mon Mar 31, 2014 1:29 pm

Re: [Pygame] Space Invaders clone 'alien wave' movement issu

Postby Mekire » Mon Mar 31, 2014 11:43 pm

Actually there was no need for me to use Rect.move_ip here:
Code: Select all
all_aliens_rect.move_ip((ALIEN_SIZE[0] - 7)*self.direction, 0)

This would have done the exact same thing:
Code: Select all
all_aliens_rect.x += (ALIEN_SIZE[0] - 7)*self.direction

Oh well, good to familiarize yourself with all the tools you have available regardless.

-Mek
User avatar
Mekire
 
Posts: 988
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan


Return to Game Development

Who is online

Users browsing this forum: No registered users and 0 guests