Reputation: 528
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
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.
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.
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