Корабль уже умеет плыть по формуле «новая позиция = старая + скорость × dt», но плывёт сам по себе, как лодка без гребца, а скорость мы вписали в код числом. Пора отдать руль игроку: научим программу слышать клавиатуру, различать «клавишу нажали» и «клавишу держат» и превращать четыре кнопки в направление полёта.
Два способа слушать клавиатуру
Представьте два устройства. Первое — дверной звонок: он звонит один раз в момент нажатия, и неважно, сколько вы потом держите палец на кнопке. Второе — педаль газа с датчиком: вы можете спросить в любую секунду «нажата ли она прямо сейчас?» и получите ответ «да» или «нет». Клавиатура в Pygame умеет и то и другое.
Звонок называется событием нажатия, pygame.KEYDOWN. Оно приходит в очередь событий ровно один раз, когда клавишу нажали; какая именно клавиша, узнаём из event.key и сравниваем с константой вроде pygame.K_SPACE. Датчик называется pygame.key.get_pressed(): функция возвращает «табличку состояний», в которой по константе клавиши смотрим, зажата ли она сейчас.
import pygame
# внутри игрового цикла
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
print("пробел нажали")
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
print("стрелка влево зажата")
Разбор по строкам
for event in pygame.event.get():— уже знакомый перебор очереди событий: за кадр там может накопиться несколько событий.if event.type == pygame.KEYDOWN:— нас интересуют только события «клавишу нажали».if event.key == pygame.K_SPACE:— внутри события спрашиваем, какая это клавиша.K_SPACE— пробел.keys = pygame.key.get_pressed()— берём состояние всей клавиатуры на этот момент; делаем это каждый кадр.if keys[pygame.K_LEFT]:— в квадратных скобках — константа нужной клавиши; получаемTrue, если она зажата прямо сейчас.
Разница хорошо видна в таблице.
Событие KEYDOWN | get_pressed() | |
|---|---|---|
| Когда срабатывает | один раз, в момент нажатия | в каждом кадре, пока клавиша зажата |
| Что даёт | разовое действие | плавное непрерывное действие |
| Для чего у нас | выход по Esc, позже выстрел и пауза | полёт корабля |
Названия констант запоминайте так: стрелки — K_LEFT, K_RIGHT, K_UP, K_DOWN; пробел — K_SPACE; Esc — K_ESCAPE; буквы — строчные: K_a, K_d, K_w, K_s.
Ловушка: порядок в кадре
Состояние клавиш обновляется, когда программа разбирает очередь событий. Поэтому get_pressed() вызываем в каждом кадре, после цикла for event in pygame.event.get(). Если взять «табличку» один раз до цикла, она так и останется застывшей.
Из четырёх клавиш — в направление
Возьмём четыре вопроса «да/нет»: нажата ли клавиша влево, вправо, вверх, вниз. Чтобы получить направление по горизонтали, делаем так: вправо засчитываем как +1, влево как −1. Нажаты обе — плюс и минус гасят друг друга, получается 0, и корабль стоит. По вертикали то же самое, помним только, что ось y смотрит вниз, поэтому dy = down - up.
Здесь пригодится факт про Python: True и False на самом деле числа — True равно 1, а False равно 0 (тип bool — разновидность целого числа). С ними можно считать, как с обычными числами.
left = True
right = False
print(right - left)
print(True + True)
-1
2
False - True это 0 − 1, то есть −1: клавиша влево тянет по оси x в минус. А True + True это 1 + 1 = 2. Итого направление по горизонтали — dx = right - left, по вертикали — dy = down - up. Каждое из чисел — −1, 0 или 1.
Чтобы работали и стрелки, и WASD, соединяем две клавиши словом or («или»): результат True, если зажата хотя бы одна из них.
keys = pygame.key.get_pressed()
left = keys[pygame.K_LEFT] or keys[pygame.K_a]
right = keys[pygame.K_RIGHT] or keys[pygame.K_d]
up = keys[pygame.K_UP] or keys[pygame.K_w]
down = keys[pygame.K_DOWN] or keys[pygame.K_s]
Если у вас включена русская раскладка, переключитесь на английскую перед проверкой WASD: так клавиши буквально совпадут с константами.
Почему по диагонали быстрее
Если зажать вправо и вверх, получится dx = 1 и dy = -1. Корабль сдвинется и на один шаг вправо, и на один вверх сразу. Но по клетчатой бумаге диагональ клетки длиннее её стороны: прямая, соединяющая противоположные углы, больше 1. Значит, за секунду корабль пролетит больше, чем по прямой, и диагональ получится быстрее.
Длину диагонали даёт теорема Пифагора: длина = √(dx² + dy²). Для (1, 1) это √2, примерно 1,414.
| Клавиши | (dx, dy) | Длина |
|---|---|---|
| только вправо | (1, 0) | 1 |
| вправо и вверх | (1, −1) | 1,414 |
| вправо и вверх, после исправления | (0,707, −0,707) | 1 |
Исправление называется нормированием вектора: делим обе компоненты на длину, и длина становится ровно 1, куда бы ни смотрело направление. Длину считает функция math.hypot(dx, dy) из модуля math — та самая теорема Пифагора в одном вызове. Соберём всё в функцию для logic.py.
import math
def direction_from_keys(left, right, up, down):
"""Направление из четырёх клавиш: длина 1, а если стоим, то (0.0, 0.0)."""
dx = right - left
dy = down - up
length = math.hypot(dx, dy)
if length == 0:
return 0.0, 0.0
return dx / length, dy / length
Разбор по строкам
import math— подключаем модуль с математическими функциями;hypotлежит в нём.dx = right - left— горизонталь: вправо +1, влево −1, обе вместе — 0 (булевы значения считаются как 1 и 0).dy = down - up— вертикаль; вверх — минус, как договорились про ось y.length = math.hypot(dx, dy)— длина вектора: 0, 1 или около 1,414.if length == 0:— ни одна клавиша не нажата (или две противоположные погасили друг друга). Делить на ноль нельзя, поэтому сразу возвращаем «стоим».return dx / length, dy / length— нормируем: возвращаем пару из двух чисел. Функция ничего не знает про Pygame — ей достаточно четырёхTrue/False.
От направления к скорости
Направление говорит только «куда»: числа от −1 до 1. Скорость в пикселях в секунду получаем, умножив направление на максимальную скорость корабля. Для неё заводим константу SHIP_SPEED = 360: за секунду корабль проходит 360 пикселей, окно шириной 960 он пересекает за 2,7 секунды. Если захочется быстрее или медленнее — правим одно число.
SHIP_SPEED = 360
dx, dy = direction_from_keys(left, right, up, down)
vx = dx * SHIP_SPEED
vy = dy * SHIP_SPEED
ship_x, ship_y = move(ship_x, ship_y, vx, vy, dt)
Разбор по строкам
SHIP_SPEED = 360— скорость в пикселях в секунду; заглавные буквы — признак константы.dx, dy = direction_from_keys(...)— функция возвращает пару, распаковываем её в две переменные, как делали сmove.vx = dx * SHIP_SPEED— скорость по x: направление 1 даёт 360, −1 даёт −360, 0 даёт 0.ship_x, ship_y = move(...)— тот жеmoveиз прошлого урока; новые координаты записываем обратно в переменные.
Цепочка получилась такая: клавиши → направление → скорость → новая позиция. Каждое звено — одна простая функция или строка, и за любое из них можно проверить себя отдельно.
Esc — выход одним нажатием
Закрывать окно крестиком удобно, но в играх принято выходить и по Esc. Это разовое действие: нажали — закончили. Значит, это работа для события KEYDOWN, а не для get_pressed(). Через «табличку» можно было бы спросить «держат ли Esc», но нам нужно именно «нажали».
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
Позже, когда в игре появятся меню и пауза, эта же клавиша станет открывать паузу — события нам для этого и понадобятся.
Практика: ведём корабль
Продолжаем проект kometa/. Нужно изменить два файла.
Шаг 1. Добавляем функцию в logic.py
Откройте logic.py. В нём уже есть ship_points и move. Добавьте import math сразу под комментарием в первой строке, а в конец — direction_from_keys. Файл целиком:
# Логика игры: только чистый Python, без pygame.
import math
def ship_points(x, y, size):
"""Три вершины треугольника-корабля: нос, левый и правый углы."""
return [
(x, y - size),
(x - size * 0.7, y + size * 0.6),
(x + size * 0.7, y + size * 0.6),
]
def move(x, y, vx, vy, dt):
"""Новая позиция объекта через dt секунд при скорости (vx, vy)."""
new_x = x + vx * dt
new_y = y + vy * dt
return new_x, new_y
def direction_from_keys(left, right, up, down):
"""Направление из четырёх клавиш: длина 1, а если стоим, то (0.0, 0.0)."""
dx = right - left
dy = down - up
length = math.hypot(dx, dy)
if length == 0:
return 0.0, 0.0
return dx / length, dy / length
Шаг 2. Проверяем без окна
Так как в logic.py нет Pygame, проверить функцию можно прямо в терминале. Откройте терминал в папке kometa/ (виртуальное окружение активировано) и выполните:
python -c "from logic import direction_from_keys; print(direction_from_keys(True, False, True, False))"
(-0.7071067811865475, -0.7071067811865475)
Нажаты влево и вверх: обе компоненты по модулю 0,707, а их «длина» равна 1. Значит, нормирование работает.
Шаг 3. Подключаем клавиатуру в main.py
Теперь main.py. Что поменялось по сравнению с прошлым уроком: импортируем direction_from_keys, добавили SHIP_SPEED, убрали пробную скорость vx = 0 / vy = -60 (теперь vx и vy считаются из клавиш), в цикле событий появился выход по Esc и добавилось чтение клавиш. Остальное осталось как в прошлом уроке. Файл целиком:
import random
import pygame
from logic import direction_from_keys, move, ship_points
WIDTH = 960
HEIGHT = 640
FPS = 60
TITLE = "Комета"
BG_COLOR = (12, 14, 32)
WHITE = (255, 255, 255)
SHIP_COLOR = (90, 200, 255)
FLAME_COLOR = (255, 170, 60)
SHIP_SIZE = 24
SHIP_SPEED = 360
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption(TITLE)
clock = pygame.time.Clock()
# Звёзды создаём один раз, до цикла
stars = []
for _ in range(120):
stars.append([random.randint(0, WIDTH), random.randint(0, HEIGHT), random.randint(1, 3)])
def draw_ship(surface, x, y):
pygame.draw.circle(surface, FLAME_COLOR, (x, y + SHIP_SIZE * 0.7), SHIP_SIZE // 4)
points = ship_points(x, y, SHIP_SIZE)
pygame.draw.polygon(surface, SHIP_COLOR, points)
pygame.draw.polygon(surface, WHITE, points, 2)
ship_x = WIDTH // 2
ship_y = HEIGHT - 80
running = True
while running:
dt = clock.tick(FPS) / 1000
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
keys = pygame.key.get_pressed()
left = keys[pygame.K_LEFT] or keys[pygame.K_a]
right = keys[pygame.K_RIGHT] or keys[pygame.K_d]
up = keys[pygame.K_UP] or keys[pygame.K_w]
down = keys[pygame.K_DOWN] or keys[pygame.K_s]
dx, dy = direction_from_keys(left, right, up, down)
vx = dx * SHIP_SPEED
vy = dy * SHIP_SPEED
ship_x, ship_y = move(ship_x, ship_y, vx, vy, dt)
screen.fill(BG_COLOR)
for x, y, size in stars:
pygame.draw.circle(screen, WHITE, (x, y), size)
draw_ship(screen, ship_x, ship_y)
pygame.display.flip()
pygame.quit()
Шаг 4. Запуск и что должно получиться
Запустите python main.py. Внизу окна на фоне звёзд стоит корабль. Он больше не плывёт сам: стрелки и WASD ведут его в любую сторону, по диагонали он летит так же быстро, как по прямой, а при зажатых влево и вправо одновременно стоит. Esc закрывает окно. Если вести корабль за край, он вылетит из окна — границы поставим в следующем уроке.
Проверьте себя
- Корабль идёт вправо, пока зажата стрелка, и останавливается, когда её отпустили.
- Клавиши W, A, S, D работают так же, как стрелки.
- Диагональ не быстрее прямой: можно сравнить на глаз, пролетев окно по горизонтали и по диагонали.
- Esc закрывает игру; повторный запуск работает без ошибок.
- В терминале
direction_from_keys(True, True, False, False)выводит(0.0, 0.0).
Частые ошибки
Корабль сдвигается на шаг на каждое нажатие
Движение положили в блок KEYDOWN. Событие приходит один раз за нажатие, поэтому и сдвиг один раз. Для плавного движения читаем get_pressed() в каждом кадре, а в KEYDOWN оставляем только разовые вещи вроде выхода.
Диагональ быстрее прямой
Забыли нормировать: вместо dx / length возвращаете сырые dx, dy. Длина диагонали получается 1,41, корабль летит примерно на 40 процентов быстрее. Проверьте, что функция делит на math.hypot(dx, dy).
AttributeError: module 'pygame' has no attribute 'K_A'
Константы букв пишутся строчными: pygame.K_a, а не pygame.K_A. Заглавной буквы у клавиши в Pygame нет — она всегда одна и та же клавиша.
Корабль не реагирует, или реагирует рывками
Проверьте, что keys = pygame.key.get_pressed() стоит внутри цикла while и после цикла по событиям. Если вызвать его один раз перед циклом, значения не обновятся никогда.
Что важно запомнить
pygame.KEYDOWNприходит один раз при нажатии — для разовых действий;pygame.key.get_pressed()показывает, что зажато сейчас — для непрерывного движения.- Константы клавиш:
pygame.K_LEFT,pygame.K_a,pygame.K_ESCAPE; буквы строчные. TrueиFalseсчитаются как 1 и 0, поэтомуdx = right - left.- Диагональ без нормирования быстрее;
direction_from_keysделит вектор наmath.hypot(dx, dy), и длина всегда не больше 1. - Скорость корабля:
vx = dx * SHIP_SPEED, гдеSHIP_SPEED = 360пикселей в секунду. direction_from_keysлежит вlogic.py, где нет Pygame, поэтому её проверяют без окна.
В следующем уроке корабль перестанет вылетать за границы экрана: поставим стенки с помощью clamp и научим звёзды возвращаться наверх с помощью wrap.