Перейти к содержанию

Домашнее чтение: Знакомство с 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)

Фигура