Игра "Поймай шарик" (часть 1)

Пользовательский интерфейс

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

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

Событийно-ориентированное программирование прекрасно сочетается со структурным программированием (структурированием кода через декомпозицию). Код обработчиков событий удобно оформлять в отдельные функции (такие функции по-английски ещё называют event handler, callback, slot).

Выбор того как именно обрабатывать событие (какой из обработчиков вызывать) называется диспетчеризация событий. В библиотеке PyGame это делается вами вручную, например при помощи разветвлённых конструкций if elif else.

Pygame обрабатывает события с помощью очереди событий. В неё записываются все происходящие события. Вспомним код, который использовался в прошлых лабораторных для рисования картины:

import pygame
from pygame.draw import *

pygame.init()

FPS = 30
screen = pygame.display.set_mode((400, 400))

pygame.display.update()
clock = pygame.time.Clock()
finished = False

while not finished:
    clock.tick(FPS)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            finished = True

pygame.quit()

В цикле используется функция get() модуля event библиотеки pygamepygame.event.get(). Она забирает список событий из очереди. Основные события — это закрытие окна, нажатие и отпускание кнопки мыши, перемещение мыши, нажатие и отпускание клавиши. Узнать, что за событие произошло, позволяет сравнение типа события event.type с константой из pygame:

Константа Событие
QUIT закрытие окна
KEYDOWN нажатие клавиши
KEYUP поднятие клавиши
MOUSEMOTION движение мыши
MOUSEBUTTONUP отпускание кнопки мыши
MOUSEBUTTONDOWN нажатие кнопки мыши

Cобытия мыши

В pygame обрабатываются три события мыши: нажатие кнопки, отпускание кнопки, перемещение мыши. Какая именно кнопка была нажата, записывается в другое свойство события – button. Для левой кнопки это число 1, для средней – 2, для правой – 3, для прокручивания вперед – 4, для прокручивания назад – 5. У событий MOUSEMOTION (перемещение мыши) вместо button используется свойство buttons, в которое записывается состояние трех кнопок мыши (кортеж из трех элементов).

Координаты мыши записываются в атрибут pos. Таким образом, если вы нажали правую кнопку мыши точно в середине окна размером 200x200, то будет создан объект типа Event с полями event.type = pygame.MOUSEBUTTONDOWN, event.button = 3, event.pos = (100, 100).

У событий MOUSEMOTION есть еще один атрибут – rel. Он показывает относительное смещение по обоим осям. С помощью него, например, можно отслеживать скорость движения мыши.

Код ниже создаёт круги в местах клика мыши: красные при клике левой кнопкой, синие при клике правой кнопкой.

import pygame
from pygame.draw import *
pygame.init()

FPS = 30
screen = pygame.display.set_mode((400, 400))

RED = (255, 0, 0)
BLUE = (0, 0, 255)

pygame.display.update()
clock = pygame.time.Clock()
finished = False

while not finished:
    clock.tick(FPS)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            finished = True
        elif event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:
                circle(screen, RED, event.pos, 50)
                pygame.display.update()
            elif event.button == 3:
                circle(screen,  BLUE, event.pos, 50)
                pygame.display.update()

pygame.quit()

Заготовка игры "Поймай шарик"

Суть игры проста: в случайном месте появляется на короткое время шарик, и мы должны успеть щелкнуть по нему мышкой.

Вначале создадим появляющиеся шарики:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import pygame
from pygame.draw import *
from random import randint
pygame.init()

FPS = 2
screen = pygame.display.set_mode((1200, 900))

RED = (255, 0, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
GREEN = (0, 255, 0)
MAGENTA = (255, 0, 255)
CYAN = (0, 255, 255)
BLACK = (0, 0, 0)
COLORS = [RED, BLUE, YELLOW, GREEN, MAGENTA, CYAN]

def new_ball():
    '''рисует новый шарик '''
    x = randint(100, 1100)
    y = randint(100, 900)
    r = randint(10, 100)
    color = COLORS[randint(0, 5)]
    circle(screen, color, (x, y), r)

pygame.display.update()
clock = pygame.time.Clock()
finished = False

while not finished:
    clock.tick(FPS)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            finished = True

    new_ball()
    pygame.display.update()
    screen.fill(BLACK)

pygame.quit()

Теперь добавим обработку щелчка мыши. Для начала выведем что-нибудь в консоль:

import pygame
from pygame.draw import *
from random import randint
pygame.init()

FPS = 2
screen = pygame.display.set_mode((1200, 900))

RED = (255, 0, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
GREEN = (0, 255, 0)
MAGENTA = (255, 0, 255)
CYAN = (0, 255, 255)
BLACK = (0, 0, 0)
COLORS = [RED, BLUE, YELLOW, GREEN, MAGENTA, CYAN]

def new_ball():
    '''рисует новый шарик '''
    x = randint(100, 1100)
    y = randint(100, 900)
    r = randint(10, 100)
    color = COLORS[randint(0, 5)]
    circle(screen, color, (x, y), r)

pygame.display.update()
clock = pygame.time.Clock()
finished = False

while not finished:
    clock.tick(FPS)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            finished = True
        elif event.type == pygame.MOUSEBUTTONDOWN:
            print('Click!')
    new_ball()
    pygame.display.update()
    screen.fill(BLACK)

pygame.quit()

При каждом щелчке в консоли будет появляться надпись «click».

Чтобы определить, попали ли мы в круг, нужно знать его координаты, радиус круга и координаты мыши в момент щелчка. Координаты мыши легко получить через event.pos. Попробуем получить координаты круга:

def click(event):
    print(x, y, r)

Не забудьте вставить вызов этой функции click(event) в место диспетчеризации!

И всё равно такой способ не прошел. Почему? В чем суть появившегося сообщения об ошибке, что оно означает?

Исправим ситуацию:

def new_ball():
    global x, y, r
    x = randint(100,700)
    y = randint(100,500)
    r = randint(30,50)
    color = COLORS[randint(0, 5)]
    circle(screen, color, (x, y), r)

def click(event):
    print(x, y, r)

Использование global – это не самое лучшее решение. Для данной задачи больше подходит использование ООП (объектно-ориентированного подхода), но об этом позже. А пока – будем использовать global.

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

Осталось проверить, не лежит ли точка (event.x, event.y) дальше, чем r от точки (x,y). Для этого, с помощью теоремы Пифагора мы найдем расстояние между двумя точками и сравним с радиусом круга.

Задания

  1. Сделать код читабельным и документированным.
  2. Реализовать подсчёт очков.
  3. Сделать шарики двигающимися со случайным отражением от стен.
  4. Реализовать одновременное присутствие нескольких шариков на экране.
    • Добавить второй тип мишени со своей формой и своим специфическим харктером движения.
    • Выдавать за эти мишени другое количество очков.
    • Сделать таблицу лучших игроков, автоматически сохраняющуюся в файл.