8. События клавиатуры

Человек может управлять объектами в игре в основном с помощь клавиатуры, мыши, джойстика. Обработкой событий занимается модуль pygame.event, который включает ряд функций, наиболее важная из которых уже ранее рассмотренная event.get(), которая забирает из очереди произошедшие события.

В pygame, когда фиксируется то или иное событие, создается соответствующий ему объект от класса Event. Уже с этими объектами работает программа. Экземпляры данного класса имеют только свойства, у них нет методов. У всех экземпляров есть свойство type. Набор остальных свойств события зависит от значения type.

События клавиатуры могут быть двух типов (иметь одно из двух значений type) – клавиша была нажата, клавиша была отпущена. Если вы нажали клавишу и отпустили, то в очередь событий будут записаны оба. Какое из них обрабатывать, зависит от контекста игры. Если вы зажали клавишу и не отпускаете ее, то в очередь записывается только один вариант – клавиша нажата.

Событию типа "клавиша нажата" в поле type записывается числовое значение, совпадающее со значением константы pygame.KEYDOWN. Событию типа "клавиша отпущена" в поле type записывается значение, совпадающее со значением константы pygame.KEYUP.

У обоих типов событий клавиатуры есть атрибуты key и mod. В key записывается конкретная клавиша, которая была нажата или отжата. В mod – клавиши-модификаторы (Shift, Ctrl и др.), которые были зажаты в момент нажатия или отжатия обычной клавиши. У событий KEYDOWN также есть поле unicode, куда записывается символ нажатой клавиши (тип данных str).

Рассмотрим, как это работает. Пусть в центре окна имеется круг, который можно двигать по горизонтали клавишами стрелок клавиатуры:

События клавиш стрелок

# здесь подключаются модули
from pygame import *
import sys

# здесь определяются константы,
# классы и функции
WHITE = (255, 255, 255)
WIN_WIDTH = 700
WIN_HEIGHT = 700
ORANGE = (255, 150, 100)
FPS = 60

# радиус будущего круга
r = 50
# координаты круга
x = WIN_WIDTH //2
y = WIN_HEIGHT // 2

# здесь происходит инициация,
# создание объектов
init()
window = display.set_mode((WIN_WIDTH, WIN_HEIGHT))
display.set_caption("Моя игра")
clock = time.Clock()
window.fill(WHITE)
# если надо до цикла отобразить
# какие-то объекты, обновляем экран
display.update()

# главный цикл
while True:
    # задержка
    clock.tick(FPS)
    # цикл обработки событий
    for i in event.get():
        if i.type == QUIT:
            sys.exit()
        elif i.type == KEYDOWN:
            if i.key == K_LEFT:
                x -= 3
            elif i.key == K_RIGHT:
                x += 3
    # --------
    # изменение объектов
    # заливаем фон
    window.fill(WHITE)
    draw.circle(window, ORANGE,(x, y), r)
    display.update()
В цикле обработки событий теперь проверяется не только событие выхода, но также нажатие клавиш. Сначала необходимо проверить тип, потому что не у всех событий есть атрибут key. Если сразу начать проверять key, то сгенерируется ошибка по той причине, что могло произойти множество событий. Например, движение мыши, у которого нет поля key. Соответственно, попытка взять значение из несуществующего поля (i.key) приведет к генерации исключения.

Часто проверку и типа и клавиши записывают в одно логическое выражение (i.type == pygame.KEYDOWN and i.key == pygame.K_LEFT). В Python так можно делать потому, что если первая часть сложного выражения возвращает ложь, то вторая часть уже не проверяется.

Если какая-либо клавиша была нажата, то проверяется, какая именно. В данном случае обрабатываются только две клавиши. В зависимости от этого меняется значение координаты x.

Проблема данного кода в том, что при выполнении программы, чтобы круг двигался, надо постоянно нажимать и отжимать клавиши. Если просто зажать их на длительный период, то объект не будет постоянно двигаться. Он сместиться только одноразово на 3 пикселя.

Так происходит потому, что событие нажатия на клавишу происходит один раз, сколь долго бы ее не держали. Это событие было забрано из очереди функцией get() и обработано. Его больше нет. Поэтому приходится генерировать новое событие, еще раз нажимая на клавишу.

Как быть, если по логике вещей надо, чтобы шар двигался до тех пор, пока клавиша зажата? Когда же она отпускается, шар должен останавливаться. Первое, что надо сделать, – это перенести изменение координаты x в основную ветку главного цикла while. В таком случае на каждой его итерации координата будет меняться, а значит шар двигаться постоянно.

Во-вторых, в цикле обработки событий нам придется следить не только за нажатием клавиши, но и ее отжатием. Когда клавиша нажимается, какая-либо переменная, играющая роль флага, должна принимать одно значение, когда клавиша отпускается эта же переменная должна принимать другое значение.

В основном теле while надо проверять значение этой переменной и в зависимости от него менять или не менять значение координаты.

События клавиш стрелок

# здесь подключаются модули
from pygame import *
import sys

# здесь определяются константы,
# классы и функции
WHITE = (255, 255, 255)
WIN_WIDTH = 700
WIN_HEIGHT = 700
ORANGE = (255, 150, 100)

RIGHT = "right"
LEFT = "left"
STOP = "stop"

FPS = 60

# радиус будущего круга
r = 50
# координаты круга
x = WIN_WIDTH //2
y = WIN_HEIGHT // 2
motion = STOP
# здесь происходит инициация,
# создание объектов
init()
window = display.set_mode((WIN_WIDTH, WIN_HEIGHT))
display.set_caption("Моя игра")
clock = time.Clock()
window.fill(WHITE)
# если надо до цикла отобразить
# какие-то объекты, обновляем экран
display.update()

# главный цикл
while True:
    # задержка
    clock.tick(FPS)
    # цикл обработки событий
    for i in event.get():
        if i.type == QUIT:
            sys.exit()
        elif i.type == KEYDOWN:
            if i.key == K_LEFT:
                motion = LEFT
            elif i.key == K_RIGHT:
                motion = RIGHT
        elif i.type == KEYUP:
            if i.key in [K_LEFT, K_RIGHT]:
                motion = STOP
                
    if motion == LEFT:
        x -= 3
    elif motion == RIGHT:
        x += 3
    # --------
    # изменение объектов
    # заливаем фон
    window.fill(WHITE)
    draw.circle(window, ORANGE,(x, y), r)
    display.update()
Использовать константы не обязательно, можно сразу присваивать строки или даже числа (например, motion = 1 обозначает движение вправо, -1 – влево, 0 – остановка). Однако константы позволяют легче понимать и обслуживать в дальнейшем код, делают его более информативным. Лучше привыкнуть к такому стилю.

Должно проверяться отжатие только двух клавиш. Если проверять исключительно KEYUP без последующей конкретизации, то отжатие любой клавиши приведет к остановке, даже если в это время будет по-прежнему зажиматься клавиша влево или вправо. Выражение i.key in [K_LEFT, K_RIGHT] обозначает, что если значение i.key совпадает с одним из значений в списке, то все выражение возвращает истину.

На самом деле существует способ по-проще. В библиотеке pygame с событиями работает не только модуль event. Так модуль pygame.key включает функции, связанные исключительно с клавиатурой. Здесь есть функция key.get_pressed(), которая возвращает кортеж двоичных значений. Индекс каждого значения соответствует своей клавиатурной константе. Само значение равно 1, если клавиша нажата, и 0 – если не нажата.

Эта функция подходит не для всех случаев обработки клавиатурных событий, но в нашем подойдет. Поэтому мы можем упростить код до такого варианта:
События клавиш стрелок key.get_pressed()

...
    keys = key.get_pressed()
 
    if keys[K_LEFT]:
        x -= 3
    elif keys[K_RIGHT]:
        x += 3
... 
Весь перечень констант pygame, соответствующих клавишам клавиатуры, смотрите в документации: https://www.pygame.org/docs/ref/key.html
Задачи
Измените приведенную в уроке программу так, чтобы круг с той же скоростью, т. е. постепенно, возвращался назад в исходную точку, когда клавиша отпускается.