Почему в моем проекте Alien Invasion все нло пришельцев резко падают, если уничтожить хотя бы одного из них? Pygame

Я читаю книгу Эрика Мэтиза "Изучаем пайтон". Дошел до того момента, где пришельцы приходят в движение и уничтожаются при попадании по ним пулями.

Проблема заключается в том, что если уничтожить хотя бы одного пришельца, все остальные резко падают вниз и пропадают из экрана. Размер экрана выставлялся следующий: 1920, 1080. Если выставить размер экрана 1200, 800, то пришельцы падают сразу при запуске программы. В чем может быть проблема?

Сама игра (Пришельцы, каждый раз отталкиваясь от стен, приближаются к нижней части экрана): введите сюда описание изображения

Выстрел: введите сюда описание изображения

Попадание по пришельцу: введите сюда описание изображения

И сразу же за этим падение всех остальных пришельцев: введите сюда описание изображения

Сам код:

alien_invasion.py

import pygame
from pygame.sprite import Group
from settings import Settings
from ship import Ship
import game_functions as gf


def run_game():
    # Инициализирует игру и создает объект экрана
    pygame.init()

    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")

    # Создание корабля, группы пуль и группы пришельцев.
    ship = Ship(ai_settings, screen)
    bullets = Group()
    aliens = Group()

    # Создание флота пришельцев
    gf.create_fleet(ai_settings, screen, ship, aliens)

    # Запуск основного цикла игры.
    while True:
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        gf.update_bullets(bullets, aliens)
        gf.update_aliens(ai_settings, aliens)
        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

run_game()

game_functions.py

import sys
import pygame
from bullet import Bullet
from alien import Alien


def check_keydown_events(event, ai_settings, screen, ship, bullets):
    """Реагирует на нажатие клавиш"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        fire_bullet(ai_settings, screen, ship, bullets)
    elif event.key == pygame.K_q:
        sys.exit()


def fire_bullet(ai_settings, screen, ship, bullets):
    """Выпускает пулю, если максимум еще не достигнут"""
    # Создание новой пули и включение ее в группу bullets
    if len(bullets) < ai_settings.bullets_allowed:
        new_bullet = Bullet(ai_settings, screen, ship)
        bullets.add(new_bullet)


def check_keyup_events(event, ship):
    """Реагирует на отпускание клавиш"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = False
    if event.key == pygame.K_LEFT:
        ship.moving_left = False


def check_events(ai_settings, screen, ship, bullets):
    """Обрабатываем нажатия клавиш и события мыши"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, ai_settings, screen, ship, bullets)

        elif event.type == pygame.KEYUP:
            check_keyup_events(event, ship)


def update_screen(ai_settings, screen, ship, aliens, bullets):
    """Обновляет изображение на экране и отображает новый экран"""
    # При каждом проходе цикла перерисовывается экран.
    screen.fill(ai_settings.bg_color)

    # Все пули выводятся позади изображений корабля и пришельцев
    for bullet in bullets.sprites():
        bullet.draw_bullet()

    ship.blitme()

    aliens.draw(screen)

    # Отображение последнего прорисованного экран.
    pygame.display.flip()


def update_bullets(bullets, aliens):
    """Обновляет позиции пуль и уничтожает старые пули"""
    # Обновление позиций пуль
    bullets.update()

    # Удаление пуль, вышедших за край экрана
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)

    # При обнаружении попадания удалить пулю и пришельца
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)


def get_number_aliens_x(ai_settings, alien_width):
    """Вычисляет количество пришельцев в ряду"""
    available_space_x = ai_settings.screen_width - 2 * alien_width
    number_aliens_x = int(available_space_x / (2 * alien_width))
    return number_aliens_x


def get_number_rows(ai_settings, ship_height, alien_height):
    """Определяет количество рядов, помещающихся на экране"""
    available_space_y = (ai_settings.screen_height -
                         (3 * alien_height) - ship_height)
    number_rows = int(available_space_y / (2 * alien_height))
    return number_rows


def create_alien(ai_settings, screen, aliens, alien_number, row_number):
    # Создает пришельца и размещает его в ряду
    alien = Alien(screen, ai_settings)
    alien_width = alien.rect.width
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
    aliens.add(alien)


def create_fleet(ai_settings, screen, ship, aliens):
    """Создает флот пришельцев"""
    # Создание пришельца и вычисление количества пришельцев в ряду
    alien = Alien(screen, ai_settings)
    number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)
    number_rows = get_number_rows(ai_settings, ship.rect.height, alien.rect.height)

    # Создание флота пришельцев
    for row_number in range(number_rows):
        for alien_number in range(number_aliens_x):
            create_alien(ai_settings, screen, aliens, alien_number, row_number)


def check_fleet_edges(ai_settings, aliens):
    """Реагирует на достижение пришельцем края экрана"""
    for alien in aliens.sprites():
        if alien.check_edges():
            change_fleet_direction(ai_settings, aliens)
            break


def change_fleet_direction(ai_settings, aliens):
    """Опускает весь флот и меняет направление флота"""
    for alien in aliens.sprites():
        alien.rect.y += ai_settings.fleet_drop_speed
        ai_settings.fleet_direction *= -1


def update_aliens(ai_settings, aliens):
    """Проверяет, достиг ли флот края экрана,
    после чего обновляет позиции всех пришельцев во флоте"""
    check_fleet_edges(ai_settings, aliens)
    aliens.update()

settings.py

class Settings():
    """Класс для хранения всех настроек игры Alien Invasion."""

    def __init__(self):
        """Инициализирует настройки игры"""

        #Параметры экрана
        self.screen_width = 1920
        self.screen_height = 1080
        self.bg_color = (230, 230, 230)

        # Настройка корабля
        self.ship_speed_factor = 1

        # Параметры пули
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60
        self.bullets_allowed = 3

        # Настройка пришельцев
        self.alien_speed_factor = 0.2
        self.fleet_drop_speed = 10
        # fleet_direction = 1 обозначает движение вправо; а -1 - влево
        self.fleet_direction = 1

alien.py

import pygame
from pygame.sprite import Sprite

class Alien(Sprite):
    """Класс, представляющий одного пришельца"""
    def __init__(self, screen, ai_settings):
        """Инициализирует пришельца и задает его начальную позицию"""
        super().__init__()
        self.screen = screen
        self.ai_settings = ai_settings

        # Загрузка изображения пришельца и назначение атрибута rect
        self.image = pygame.image.load('images/alien.bmp')
        self.rect = self.image.get_rect()

        # Каждый новый пришелец появляется в левом верхнем углу экрана
        self.rect.x = self.rect.width
        self.rect.y = self.rect.height

        # Сохранение точной позиции пришельца
        self.x = float(self.rect.x)

    def check_edges(self):
        """Возвращает True, если пришелец находится у края экрана"""
        screen_rect = self.screen.get_rect()
        if self.rect.right >= screen_rect.right:
            return True
        elif self.rect.left <= 0:
            return True

    def update(self):
        """Перемещает пришельца влево и вправо"""
        self.x += self.ai_settings.alien_speed_factor * self.ai_settings.fleet_direction
        self.rect.x = self.x

    def blitme(self):
        self.screen.blit(self.image, self.rect)

ship.py

import pygame


class Ship():
    def __init__(self, ai_settings, screen):
        """Инициализирует корабль и задает его начальную позицию"""
        self.screen = screen
        self.ai_settings = ai_settings

        # Загрузка изображения корабля и получение прямоугольника.
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()

        # Каждый новый корабль появляется у нижнего экрана.
        self.rect.centerx = self.screen_rect.centerx
        self.rect.bottom = self.screen_rect.bottom

        # Сохранение вещественной координаты центра корабля.
        self.center = float(self.rect.centerx)

        # Флаги перемещения
        self.moving_right = False
        self.moving_left = False

    def update(self):
        """Обновляет позицию корабля с учетом флага"""
        # Обновляет атрибут center, не rect.
        if self.moving_right and self.rect.right < self.screen_rect.right:
            self.center += self.ai_settings.ship_speed_factor

        if self.moving_left and self.rect.left > 0:
            self.center -= self.ai_settings.ship_speed_factor

        # Обновление атрибута rect на основании self.center.
        self.rect.centerx = self.center

    def blitme(self):
        """Рисует корабль в текущей позиции."""
        self.screen.blit(self.image, self.rect)

bullet.py

import pygame
from pygame.sprite import Sprite


class Bullet(Sprite):
    """Класс для управления пулями, выпущенными кораблем"""

    def __init__(self, ai_settings, screen, ship):
        """Создает объект пули в текущей позиции корабля"""
        super().__init__()
        self.screen = screen

        # Создание пули в позиции (0, 0) и назначение правильной позиции
        self.rect = pygame.Rect(0, 0, ai_settings.bullet_width, ai_settings.bullet_height)
        self.rect.centerx = ship.rect.centerx
        self.rect.top = ship.rect.top

        # Позиция пули хранится в вещественном формате
        self.y = float(self.rect.y)
        self.color = ai_settings.bullet_color
        self.speed_factor = ai_settings.bullet_speed_factor

    def update(self):
        """Перемещает пулю вверх по экрану"""
        # Обновление позиции пули в вещественном формате
        self.y -= self.speed_factor

        # Обновление позиции прямоугольника
        self.rect.y = self.y

    def draw_bullet(self):
        """Выводит пули на экран"""
        pygame.draw.rect(self.screen, self.color, self.rect)

Ответы (2 шт):

Автор решения: sxmrxk

Начну с того что когда вы передаете какой-либо объект в функцию или конструктор, вы передаете не копию объекта а ссылку на него.

В вашем случае вам каждый раз нужно создавать новый экземпляр класса Settings для каждого Alien, Ship и т.д.

Думаю будет лучше если покажу вам на примере

    class Settings:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
    class Alien:
        def __init__(self, settings):
            self.settings = settings
    
    
    settings = Settings(1, 2)
    
    alien1 = Alien(settings)
    alien2 = Alien(settings)
    
    alien1.settings.x = 100
    
    # Магия... (на самом деле нет)
    print(alien2.settings.x) # 100

Т.е. оба объекта alien на самом деле пользуются одним и тем же объектом settings

Чтобы такого не происходило нужно делать так:

    settings1 = Settings(1, 2)
    settings2 = Settings(1, 2)

    alien1 = Alien(settings1)
    alien2 = Alien(settings2)

    alien1.settings.x = 100
    alien1.settings.y = 456
    print(alien2.settings.x)  # 1
    print(alien2.settings.x)  # 2

Готов ответить на Ваши вопросы, если они остались

→ Ссылка
Автор решения: Noragami

Нашел проблему. В game_functions.py в функции change_fleet_direction() добавил лишнюю табуляцию в код.

Код с ошибкой:

def change_fleet_direction(ai_settings, aliens):
"""Опускает весь флот и меняет направление флота"""
for alien in aliens.sprites():
    alien.rect.y += ai_settings.fleet_drop_speed
    ai_settings.fleet_direction *= -1

В исправленном виде:

def change_fleet_direction(ai_settings, aliens):
"""Опускает весь флот и меняет направление флота"""
for alien in aliens.sprites():
    alien.rect.y += ai_settings.fleet_drop_speed
ai_settings.fleet_direction *= -1
→ Ссылка