Домашнее чтение: Знакомство с PyGame#
Pygame не входит в стандартную поставку Python, для установки библиотеки выполните
pip install pygame
Все возможности библиотеки Pygame нереально рассмотреть в одной статье, поэтому здесь мы затронем только самые базовые концепции – рисование, движение объектов, покадровую анимацию, обработку событий, обновление счетчика, обнаружение столкновения.
Окно и главный цикл приложения#
Создание любого приложения на базе Pygame начинается с импорта и инициализации библиотеки. Затем нужно определить параметры окна, и по желанию – задать цвет (или изображение) фона:
import pygame
# инициализируем библиотеку Pygame
pygame.init()
# определяем размеры окна
window_size = (300, 300)
# задаем название окна
pygame.display.set_caption("Синий фон")
# создаем окно
screen = pygame.display.set_mode(window_size)
# задаем цвет фона
background_color = (0, 0, 255) # синий
# заполняем фон заданным цветом
screen.fill(background_color)
# обновляем экран для отображения изменений
pygame.display.flip()
# показываем окно, пока пользователь не нажмет кнопку "Закрыть"
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
Цикл while True
играет роль главного цикла программы – в нем происходит отслеживание событий приложения и действий пользователя. Функция pygame.quit()
завершает работу приложения, и ее можно назвать противоположностью функции pygame.init()
. Для завершения Python-процесса используется exit()
, с той же целью можно использовать sys.exit()
, но ее нужно импортировать в начале программы: import sys
.
В качестве фона можно использовать изображение:
import pygame
pygame.init()
window_size = (400, 400)
screen = pygame.display.set_mode(window_size)
pygame.display.set_caption("Peter the Piglet")
# загружаем изображение
background_image = pygame.image.load("background.png")
# подгоняем масштаб под размер окна
background_image = pygame.transform.scale(background_image, window_size)
# накладываем изображение на поверхность
screen.blit(background_image, (0, 0))
pygame.display.flip()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
Обработку событий (нажатий клавиш и кликов) в Pygame реализовать очень просто – благодаря встроенным функциям. Приведенный ниже код изменяет цвет фона после клика по кнопке. Обратите внимание, что в Pygame можно задавать цвет несколькими способами:
import pygame
pygame.init()
pygame.display.set_caption('Измени цвет фона')
window_surface = pygame.display.set_mode((300, 300))
background = pygame.Surface((300, 300))
background.fill(pygame.Color('#000000'))
color_list = [
pygame.Color('#FF0000'), # красный
pygame.Color('#00FF00'), # зеленый
pygame.Color('#0000FF'), # синий
pygame.Color('#FFFF00'), # желтый
pygame.Color('#00FFFF'), # бирюзовый
pygame.Color('#FF00FF'), # пурпурный
pygame.Color('#FFFFFF') # белый
]
current_color_index = 0
button_font = pygame.font.SysFont('Verdana', 15) # используем шрифт Verdana
button_text_color = pygame.Color("black")
button_color = pygame.Color("gray")
button_rect = pygame.Rect(100, 115, 100, 50)
button_text = button_font.render('Нажми!', True, button_text_color)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
if button_rect.collidepoint(event.pos):
current_color_index = (current_color_index + 1) % len(color_list)
background.fill(color_list[current_color_index])
window_surface.blit(background, (0, 0))
pygame.draw.rect(window_surface, button_color, button_rect)
button_rect_center = button_text.get_rect(center=button_rect.center)
window_surface.blit(button_text, button_rect_center)
pygame.display.update()

Как очевидно из приведенного выше примера, основной цикл Pygame приложения состоит из трех повторяющихся действий:
- Обработка событий (нажатий клавиш или кнопок).
- Обновление состояния.
- Отрисовка состояния на экране.
GUI для PyGame#
Pygame позволяет легко и быстро интегрировать в проект многие нужные вещи – шрифты, звук, обработку событий, – однако не имеет встроенных виджетов для создания кнопок, лейблов, индикаторов выполнения и других подобных элементов интерфейса. Эту проблему разработчик должен решать либо самостоятельно (нарисовать прямоугольник, назначить ему функцию кнопки), либо с помощью дополнительных GUI-библиотек. Таких библиотек несколько, к самым популярным относятся:
- Pygame GUI
- Thorpy
- PGU
Вот простой пример использования Pygame GUI – зеленые нули и единицы падают вниз в стиле «Матрицы»:
import pygame
import pygame_gui
import random
window_size = (800, 600)
window = pygame.display.set_mode(window_size)
pygame.display.set_caption('Матрица Lite')
pygame.init()
gui_manager = pygame_gui.UIManager(window_size)
font = pygame.font.SysFont('Consolas', 20)
text_color = pygame.Color('green')
text_symbols = ['0', '1']
text_pos = [(random.randint(0, window_size[0]), 0) for i in range(50)]
text_speed = [(0, random.randint(1, 5)) for i in range(50)]
text_surface_list = []
button_size = (100, 50)
button_pos = (350, 250)
button_text = 'Матрица!'
button = pygame_gui.elements.UIButton(
relative_rect=pygame.Rect(button_pos, button_size),
text=button_text,
manager=gui_manager
)
while True:
time_delta = pygame.time.Clock().tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
if event.type == pygame_gui.UI_BUTTON_PRESSED:
text_surface_list = []
for i in range(50):
text_symbol = random.choice(text_symbols)
text_surface = font.render(text_symbol, True, text_color)
text_surface_list.append(text_surface)
gui_manager.process_events(event)
gui_manager.update(time_delta)
window.fill(pygame.Color('black'))
for i in range(50):
text_pos[i] = (text_pos[i][0], text_pos[i][1] + text_speed[i][1])
if text_pos[i][1] > window_size[1]:
text_pos[i] = (random.randint(0, window_size[0]), -20)
if len(text_surface_list) > i:
window.blit(text_surface_list[i], text_pos[i])
gui_manager.draw_ui(window)
pygame.display.update()

Анимация и обработка событий#
Выше, в примере с падающими символами в «Матрице», уже был показан принцип простейшей имитации движения, который заключается в последовательном изменении координат объекта и обновлении экрана с установленной частотой кадра pygame.time.Clock().tick(60)
. Усложним задачу – сделаем простую анимацию с падающими розовыми «звездами». Приложение будет поддерживать обработку двух событий:
- При клике мышью по экрану анимация останавливается.
- При нажатии клавиши Enter – возобновляется.
- Кроме того, добавим счетчик упавших звезд. Готовый код выглядит так:
import pygame
import random
pygame.init()
screen_width = 640
screen_height = 480
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Звезды падают вниз")
black = (0, 0, 0)
white = (255, 255, 255)
pink = (255, 192, 203)
font = pygame.font.SysFont("Verdana", 15)
star_list = []
for i in range(50):
x = random.randrange(screen_width)
y = random.randrange(-200, -50)
speed = random.randrange(1, 5)
star_list.append([x, y, speed])
score = 0
freeze = False # флаг для определения момента остановки
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
if event.type == pygame.MOUSEBUTTONDOWN: # останавливаем падение звезд по клику
freeze = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN: # возобновляем движение вниз, если нажат Enter
freeze = False
if not freeze: # если флаг не активен,
# звезды падают вниз
for star in star_list:
star[1] += star[2]
if star[1] > screen_height:
star[0] = random.randrange(screen_width)
star[1] = random.randrange(-200, -50)
score += 1
# рисуем звезды, выводим результаты подсчета
screen.fill(black)
for star in star_list:
pygame.draw.circle(screen, pink, (star[0], star[1]), 3)
score_text = font.render("Упало звезд: " + str(score), True, white)
screen.blit(score_text, (10, 10))
pygame.display.update()
# устанавливаем частоту обновления экрана
pygame.time.Clock().tick(60)
Столкновение объектов#
В этом примере расстояние между объектами проверяется до тех пор, пока объекты не столкнутся. В момент столкновение движение прекращается, а цвет объектов – изменяется:
import pygame
import math
pygame.init()
screen_width = 400
screen_height = 480
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Драматическое столкновение")
# размеры и позиция окружности
circle_pos = [screen_width/2, 50]
circle_radius = 20
# размеры и позиция прямоугольника
rect_pos = [screen_width/2, screen_height-50]
rect_width = 100
rect_height = 50
# цвета окружности и прямоугольника
white = (255, 255, 255)
black = (0, 0, 0)
green = (0, 255, 0)
red = (255, 0, 0)
# скорость движения окружности
speed = 5
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
# окружность движется вниз
circle_pos[1] += speed
# проверяем (используя формулу расстояния),
# столкнулась ли окружность с прямоугольником
circle_x = circle_pos[0]
circle_y = circle_pos[1]
rect_x = rect_pos[0]
rect_y = rect_pos[1]
distance_x = abs(circle_x - rect_x)
distance_y = abs(circle_y - rect_y)
if distance_x <= (rect_width/2 + circle_radius) and distance_y <= (rect_height/2 + circle_radius):
circle_color = red # изменяем цвет фигур
rect_color = green # в момент столкновения
else:
circle_color = green
rect_color = black
# рисуем окружность и прямоугольник на экране
screen.fill(white)
pygame.draw.circle(screen, circle_color, circle_pos, circle_radius)
pygame.draw.rect(screen, rect_color, (rect_pos[0]-rect_width/2, rect_pos[1]-rect_height/2, rect_width, rect_height))
pygame.display.update()
# останавливаем движение окружности, если она
# столкнулась с прямоугольником
if circle_pos[1] + circle_radius >= rect_pos[1] - rect_height/2:
speed = 0
# задаем частоту обновления экрана
pygame.time.Clock().tick(60)
Управление движением объекта#
Для управления движением (в нашем случае – с помощью клавиш ← и →) используются pygame.K_RIGHT
(вправо) и pygame.K_LEFT
(влево). В приведенном ниже примере передвижение падающих окружностей возможно только до момента приземления на дно игрового поля, или на предыдущие фигуры. Для упрощения вычисления факта столкновения фигур окружности вписываются в прямоугольники:
import pygame
import random
pygame.init()
screen_width = 640
screen_height = 480
screen = pygame.display.set_mode((screen_width, screen_height))
# цвета окружностей
white = (255, 255, 255)
red = (255, 0, 0)
green = (0, 255, 0)
blue = (0, 0, 255)
black = (0, 0, 0)
yellow = (255, 255, 0)
# цвет, скорость, начальная позиция окружности
circle_radius = 30
circle_speed = 3
circle_color = random.choice([red, green, blue, yellow, white])
circle_pos = [screen_width//2, -circle_radius]
circle_landed = False
# список приземлившихся окружностей и их позиций
landed_circles = []
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
# если окружность не приземлилась
if not circle_landed:
# меняем направление по нажатию клавиши
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
circle_pos[0] -= circle_speed
if keys[pygame.K_RIGHT]:
circle_pos[0] += circle_speed
# проверяем, столкнулась ли окружность с другой приземлившейся окружностью
for landed_circle in landed_circles:
landed_rect = pygame.Rect(landed_circle[0]-circle_radius, landed_circle[1]-circle_radius, circle_radius*2, circle_radius*2)
falling_rect = pygame.Rect(circle_pos[0]-circle_radius, circle_pos[1]-circle_radius, circle_radius*2, circle_radius*2)
if landed_rect.colliderect(falling_rect):
circle_landed = True
collision_x = circle_pos[0]
collision_y = landed_circle[1] - circle_radius*2
landed_circles.append((collision_x, collision_y, circle_color))
break
# если окружность не столкнулась с другой приземлившейся окружностью
if not circle_landed:
# окружность движется вниз
circle_pos[1] += circle_speed
# проверяем, достигла ли окружность дна
if circle_pos[1] + circle_radius > screen_height:
circle_pos[1] = screen_height - circle_radius
circle_landed = True
# добавляем окружность и ее позицию в список приземлившихся окружностей
landed_circles.append((circle_pos[0], circle_pos[1], circle_color))
if circle_landed:
# если окружность приземлилась, задаем параметры новой
circle_pos = [screen_width//2, -circle_radius]
circle_color = random.choice([red, green, blue, yellow, white])
circle_landed = False
# рисуем окружности
screen.fill(black)
for landed_circle in landed_circles:
pygame.draw.circle(screen, landed_circle[2], (landed_circle[0], landed_circle[1]), circle_radius)
pygame.draw.circle(screen, circle_color, circle_pos, circle_radius)
pygame.display.update()
# частота обновления экрана
pygame.time.Clock().tick(60)
Доп пример. Лабиринт 2D наивная реализация#
Pygame игра, которая генерирует лабиринт из вертикальных стен со случайным количеством дверей. Игрок (красная окружность) должен проходить через двери.
import pygame
import random
pygame.init()
screen_width = 640
screen_height = 480
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption('Лабиринт')
black = (0,0,0)
white = (255,255,255)
red = (255,0,0)
blue = (0,0,255)
green = (0,255,0)
# параметры стен и дверей
line_width = 10
line_gap = 40
line_offset = 20
door_width = 20
door_gap = 40
max_openings_per_line = 5
# параметры и стартовая позиция игрока
player_radius = 10
player_speed = 5
player_x = screen_width - 12
player_y = screen_height - line_offset
# рисуем стены и двери
lines = []
for i in range(0, screen_width, line_gap):
rect = pygame.Rect(i, 0, line_width, screen_height)
num_openings = random.randint(1, max_openings_per_line)
if num_openings == 1:
# одна дверь посередине стены
door_pos = random.randint(line_offset + door_width, screen_height - line_offset - door_width)
lines.append(pygame.Rect(i, 0, line_width, door_pos - door_width))
lines.append(pygame.Rect(i, door_pos + door_width, line_width, screen_height - door_pos - door_width))
else:
# несколько дверей
opening_positions = [0] + sorted([random.randint(line_offset + door_width, screen_height - line_offset - door_width) for _ in range(num_openings-1)]) + [screen_height]
for j in range(num_openings):
lines.append(pygame.Rect(i, opening_positions[j], line_width, opening_positions[j+1]-opening_positions[j]-door_width))
clock = pygame.time.Clock()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
# передвижение игрока
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] and player_x > player_radius:
player_x -= player_speed
elif keys[pygame.K_RIGHT] and player_x < screen_width - player_radius:
player_x += player_speed
elif keys[pygame.K_UP] and player_y > player_radius:
player_y -= player_speed
elif keys[pygame.K_DOWN] and player_y < screen_height - player_radius:
player_y += player_speed
# проверка столкновений игрока со стенами
player_rect = pygame.Rect(player_x - player_radius, player_y - player_radius, player_radius * 2, player_radius * 2)
for line in lines:
if line.colliderect(player_rect):
# в случае столкновения возвращаем игрока назад
if player_x > line.left and player_x < line.right:
if player_y < line.top:
player_y = line.top - player_radius
else:
player_y = line.bottom + player_radius
elif player_y > line.top and player_y < line.bottom:
if player_x < line.left:
player_x = line.left - player_radius
else:
player_x = line.right + player_radius
screen.fill(black)
for line in lines:
pygame.draw.rect(screen, green, line)
pygame.draw.circle(screen, red, (player_x, player_y), player_radius)
pygame.display.update()
clock.tick(60)