Konrados
Konrados

Reputation: 528

Decreasing distance between objects using pygame

I'm working on Space Invaders variation. Right now all Enemies are moving horizontal and when first or last Enemy in column hits window border all Enemies start moving in opposite direction.

My problem is when Enemy hits few times in window right border distance between last and before last Enemy is decreasing. Below gif is showing an issue.

https://i.sstatic.net/bytfM.jpg

I think that problem is in move method in Enemy class but I'm not sure about that.

My code:

import pygame
import os
import random

# CONST
WIDTH, HEIGHT = 800, 800
WIN = pygame.display.set_mode((WIDTH, HEIGHT))
FPS = 60

BACKGROUND = pygame.image.load(os.path.join("assets", "background.png"))

PLAYER_IMG = pygame.image.load(os.path.join("assets", "player-space-ship.png"))
PLAYER_VELOCITY = 7
PLAYER_LIVES = 3

ENEMY_IMG = pygame.image.load(os.path.join("assets", "Enemy1.png"))
ENEMY_VELOCITY = 1
ENEMY_ROWS = 3
ENEMY_COOLDOWN_MIN, ENEMY_COOLDOWN_MAX = 60, 60 * 4
ENEMIES_NUM = 10

LASER_IMG = pygame.image.load(os.path.join("assets", "laser-bullet.png"))
LASER_VELOCITY = 10

pygame.init()
pygame.display.set_caption("Galaxian")
font = pygame.font.SysFont("Comic Sans MS", 20)


class Ship:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.ship_img = None
        self.laser = None

    def draw(self, surface):
        if self.laser:
            self.laser.draw(surface)
        surface.blit(self.ship_img, (self.x, self.y))

    def get_width(self):
        return self.ship_img.get_width()

    def get_height(self):
        return self.ship_img.get_height()

    def get_rect(self):
        return self.ship_img.get_rect(topleft=(self.x, self.y))

    def move_laser(self, vel, obj):
        if self.laser:
            self.laser.move(vel)
            if self.laser.is_offscreen():
                self.laser = None
            elif self.laser.collision(obj):
                self.laser = None
                obj.kill()

    def fire(self):
        if not self.laser:
            x = int(self.get_width() / 2 + self.x - 4)
            y = self.y - 9
            self.laser = Laser(x, y)


class Player(Ship):
    def __init__(self, x, y):
        super().__init__(x, y)
        self.ship_img = PLAYER_IMG
        self.mask = pygame.mask.from_surface(self.ship_img)
        self.score = 0
        self.lives = PLAYER_LIVES

    def move_laser(self, vel, objs):
        if self.laser:
            self.laser.move(vel)
            if self.laser.is_offscreen():
                self.laser = None
            else:
                for obj in objs:
                    if self.laser and self.laser.collision(obj):
                        self.score += 1
                        objs.remove(obj)
                        self.laser = None

    def get_lives(self):
        return self.lives

    def get_score(self):
        return self.score

    def kill(self):
        self.lives -= 1

    def collision(self, obj):
        return is_collide(self, obj)


class Enemy(Ship):
    y_offset = 0
    rect = None

    def __init__(self, x, y):
        super().__init__(x, y)
        self.ship_img = ENEMY_IMG
        self.mask = pygame.mask.from_surface(self.ship_img)
        self.vel = ENEMY_VELOCITY
        self.cooldown = random.randint(ENEMY_COOLDOWN_MIN, ENEMY_COOLDOWN_MAX)

    def move(self, objs):
        if self.x + self.vel + self.get_width() > WIDTH or self.x + self.vel < 0:
            for obj in objs:
                obj.vel *= -1

        self.x += self.vel

    def can_fire(self, enemies):
        if self.cooldown > 0:
            self.cooldown -= 1
            return False
        self.cooldown = random.randint(ENEMY_COOLDOWN_MIN, ENEMY_COOLDOWN_MAX)
        self.rect = pygame.Rect((self.x, self.y), (self.get_width(), HEIGHT))
        for enemy in enemies:
            if self.rect.contains(enemy.get_rect()) and enemy != self:
                return False
        return True

    def collision(self, obj):
        return is_collide(self, obj)


class Laser:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.img = LASER_IMG
        self.mask = pygame.mask.from_surface(self.img)

    def draw(self, surface):
        surface.blit(self.img, (self.x, self.y))

    def move(self, vel):
        self.y += vel

    def is_offscreen(self):
        return self.y + self.get_height() < 0 or self.y > HEIGHT

    def get_width(self):
        return self.img.get_width()

    def get_height(self):
        return self.img.get_height()

    def collision(self, obj):
        return is_collide(self, obj)


def is_collide(obj1, obj2):
    offset_x = obj2.x - obj1.x
    offset_y = obj2.y - obj1.y
    return obj1.mask.overlap(obj2.mask, (offset_x, offset_y)) != None


def spawn_enemies(count):
    distance = int(WIDTH / (count + 1))
    enemies = []
    enemy_width = ENEMY_IMG.get_width()
    enemy_height = ENEMY_IMG.get_height()

    if distance >= enemy_width * 2:
        for i in range(count):
            x = distance * (i + 1) - enemy_width
            for row in range(ENEMY_ROWS):
                enemies.append(Enemy(x, (row + 1) * enemy_height * 2))
    return enemies


def game():
    is_running = True
    clock = pygame.time.Clock()
    player = Player(int(WIDTH / 2), int(HEIGHT - PLAYER_IMG.get_height() - 20))
    enemies = spawn_enemies(ENEMIES_NUM)
    background = pygame.transform.scale(BACKGROUND, (WIDTH, HEIGHT))
    bg_y = 0

    def redraw():
        WIN.blits(((background, (0, bg_y - HEIGHT)), (background, (0, bg_y)),))
        score = font.render(f"Punktacja: {player.get_score()}", True, (255, 255, 255))
        lives_text = font.render(f"Życia: {player.get_lives()}", True, (255, 255, 255))
        WIN.blit(score, (20, 20))
        WIN.blit(lives_text, (WIDTH - lives_text.get_width() - 20, 20))
        player.draw(WIN)
        for enemy in enemies:
            enemy.draw(WIN)

    while is_running:
        clock.tick(FPS)
        redraw()

        # Move backgrounds
        if bg_y < HEIGHT:
            bg_y += 1
        else:
            bg_y = 0

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                is_running = False
                break

        key = pygame.key.get_pressed()
        if key[pygame.K_LEFT] and (player.x - PLAYER_VELOCITY) >= 0:
            player.x -= PLAYER_VELOCITY
        if (
            key[pygame.K_RIGHT]
            and (player.x + player.get_width() + PLAYER_VELOCITY) <= WIDTH
        ):
            player.x += PLAYER_VELOCITY
        if key[pygame.K_SPACE]:
            player.fire()

        player.move_laser(-LASER_VELOCITY, enemies)
        for enemy in enemies:
            if enemy.can_fire(enemies):
                enemy.fire()
            enemy.move(enemies)
            enemy.move_laser(LASER_VELOCITY, player)

        pygame.display.update()
    pygame.quit()


game()

-- EDIT --

Below I'm posting working solution according to Glenn's advice.

class Enemy(Ship):
    # ... 
    def move(self, objs):
        last_obj = objs[-1]
        if (
            last_obj.x + last_obj.vel + last_obj.get_width() > WIDTH
            or self.x + self.vel < 0
        ):
            for obj in objs:
                obj.vel *= -1
        self.x += self.vel

Upvotes: 1

Views: 202

Answers (1)

Glenn Mackintosh
Glenn Mackintosh

Reputation: 2790

I have not run the code, so this is from an analysis of the code logic.

You spawn your enemies and add them to the list from left to right (on all three rows), which means the three on the far left are the first three to be added to the enemies list and the three on the far right are the last three to be added. Since the list order is maintained when you iterate through the enemies list you also iterate through them in that order (left to right).

In your Enemy.move() method, you check to see if that enemy would go off the screen on either side, and if it would you change the direction of all of the enemies by reversing the velocity of them all. However when the enemies on the far right hit the edge of the screen, you have already moved all the enemies that are to the left of them. That means all the enemies to the left have moved right and only the last three don't and in fact they move to the left instead. This closes the gap on the right side each time they hit the right edge.

This does not happen when moving left because you start the iteration with those enemies and so the velocity direction is changed first before any of them are moved. (If you had created the enemies from right to left, you would see the opposite behavior).

There are two easy approaches to fixing this.

  1. Before moving any enemies you can look to see if any would go off the screen and switch the direction before moving any of them. This actually is not that bad because you can use the ordering that you created them in to your advantage here. the enemies list will always be in left to right order even after some are killed and removed. The first entry in enemies should always be as far the the left as any of them , and the last entry will be as far to the right as the farthest to that side. That means that you only have to check the first (enemies[0]) against the left edge and the last (enemies[-1]) against the right edge before doing the move.

  2. The other option is to always move them all. but have the move() method return whether it went off the edge. If any did you remember that, then after moving them all you switch the direction of them all and on the next pass they all move the other way. This means they could go off the screen slightly on either side, but you could prevent that by padding the edge check by an extra velocity amount and that would keep them from actually going off the screen at all.

I would probably go with option 1) myself. They are both pretty straightforward to implement.

Upvotes: 2

Related Questions