Глава 21 · Проект: космический шутер

Движение корабля и появление врагов

Скорость в пикселях в секунду с самого начала — и враги, появляющиеся сверху с течением времени.

Перемещаем космический корабль

Управление — через get_pressed() из главы 20 (непрерывное движение, пока клавиша зажата). Скорость сразу задана в пикселях в секунду (px/s), а позиция корабля хранится отдельным дробным числом — Rect понимает только целые координаты, и обновлять его напрямую по чуть-чуть на каждом кадре значило бы терять дробную часть движения:

dvizhenie_korablya.py
KORABL_SKOROST = 260.0   # px/s

korabl_x = float(korabl.x)   # дробная копия позиции — сам Rect хранит только целые числа

def obrabotat_klavishi(korabl_x, klavishi, dt):
    napravlenie = klavishi[pygame.K_RIGHT] - klavishi[pygame.K_LEFT]
    korabl_x += napravlenie * KORABL_SKOROST * dt
    return max(0.0, min(korabl_x, SHIRINA - KORABL_SHIRINA))

# в игровом цикле:
korabl_x = obrabotat_klavishi(korabl_x, klavishi, dt)
korabl.x = round(korabl_x)
max/min — тот же приём ограничения, что и в главе 20
max(0.0, min(korabl_x, SHIRINA - KORABL_SHIRINA)) гарантирует, что korabl_x никогда не выйдет за пределы [0, SHIRINA - KORABL_SHIRINA] — тот же самый приём «зажимания» значения в диапазон, что мы использовали для прыгающего мяча.
napravlenie — число -1, 0 или 1
klavishi[pygame.K_RIGHT] - klavishi[pygame.K_LEFT] — булевы значения True/False ведут себя как 1/0 (глава 4), поэтому разность даёт ровно то, что нужно: 1, если зажата только правая стрелка, −1, если только левая, и 0, если обе или ни одна. Раздел 21.11 обобщит этот же приём на два измерения сразу через Vector2.

Создаём и перемещаем врагов

Враг тоже появится позже как отдельный спрайт-класс (раздел 21.9), а пока — словарь с Rect и дробной координатой Y, по той же причине, что и у корабля. Врагов будет много, и они появляются со временем — значит, нужен список (глава 11) и таймер в секундах до следующего появления:

vragi.py
VRAG_SHIRINA, VRAG_VYSOTA = 32, 28
VRAG_SKOROST = 150.0            # px/s
INTERVAL_POYAVLENIYA_VRAGA = 0.75   # секунд между новыми врагами

vragi = []
vremya_do_vraga = INTERVAL_POYAVLENIYA_VRAGA

def sozdat_vraga():
    x = random.randint(0, SHIRINA - VRAG_SHIRINA)
    return {{
        "rect": pygame.Rect(x, -VRAG_VYSOTA, VRAG_SHIRINA, VRAG_VYSOTA),
        "y": float(-VRAG_VYSOTA),
    }}

# в игровом цикле, каждый кадр:
vremya_do_vraga -= dt
if vremya_do_vraga <= 0.0:
    vragi.append(sozdat_vraga())
    vremya_do_vraga += INTERVAL_POYAVLENIYA_VRAGA

for vrag in vragi:
    vrag["y"] += VRAG_SKOROST * dt
    vrag["rect"].y = round(vrag["y"])
Реальное окно: маленький оранжевый треугольный враг спускается сверху к синему кораблю игрока
Реальное окно — маленький «разведчик» (scout), один из двух типов врагов финальной версии (раздел 21.17).
y = -VRAG_VYSOTA — враг появляется чуть выше экрана
Отрицательная стартовая координата Y означает, что враг рождается чуть выше видимой области и плавно «въезжает» в кадр сверху — а не появляется резко посередине экрана.
+= interval, а не = interval — вот что сохраняет остаток
После появления врага мы прибавляем интервал к текущему значению таймера, а не присваиваем интервал заново. Поэтому небольшое превышение времени не теряется: если vremya_do_vraga ушёл в −0.01, следующий отсчёт начнётся именно с этого небольшого долга, а не с чистого интервала. В разделе 21.14 этот же принцип будет обобщён с помощью while на случай, когда один обрабатываемый шаг может охватить сразу несколько интервалов.
Практика: движение корабля и врагов
Pygame открывает нативное окно Python — выполните локально в VS Code, PyCharm или Jupyter
Практика выполняется локально
Открыть практику →