Руководство по pygame на русском

Время на прочтение
6 мин

Количество просмотров 76K

Это первая часть серии руководств «Разработка игр с помощью Pygame». Она предназначена для программистов начального и среднего уровней, которые заинтересованы в создании игр и улучшении собственных навыков кодирования на Python.

Что такое Pygame?

Pygame — это «игровая библиотека», набор инструментов, помогающих программистам создавать игры. К ним относятся:

  • Графика и анимация

  • Звук (включая музыку)

  • Управление (мышь, клавиатура, геймпад и так далее)

Игровой цикл

В сердце каждой игры лежит цикл, который принято называть «игровым циклом». Он запускается снова и снова, делая все, чтобы работала игра. Каждый цикл в игре называется кадром.

В каждом кадре происходит масса вещей, но их можно разбить на три категории:

1.Обработка ввода (события)

Речь идет обо всем, что происходит вне игры — тех событиях, на которые она должна реагировать. Это могут быть нажатия клавиш на клавиатуре, клики мышью и так далее.

2.Обновление игры

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

3.Рендеринг (прорисовка)

В этом шаге все выводится на экран: фоны, персонажи, меню. Все, что игрок должен видеть, появляется на экране в нужном месте.

Время

Еще один важный аспект игрового цикла — скорость его работы. Многие наверняка знакомы с термином FPS, который расшифровывается как Frames Per Second (или кадры в секунду). Он указывает на то, сколько раз цикл должен повториться за одну секунду. Это важно, чтобы игра не была слишком медленной или быстрой. Важно и то, чтобы игра не работала с разной скоростью на разных ПК. Если персонажу необходимо 10 секунд на то, чтобы пересечь экран, эти 10 секунд должны быть неизменными для всех компьютеров.

Создание шаблона Pygame

Теперь, зная из каких элементов состоит игра, можно переходить к процессу написания кода. Начать стоит с создания простейшей программы pygame, которая всего лишь открывает окно и запускает игровой цикл. Это отправная точка для любого проекта pygame.

В начале программы нужно импортировать необходимые библиотеки и задать базовые переменные настроек игры:

# Pygame шаблон - скелет для нового проекта Pygame
import pygame
import random

WIDTH = 360  # ширина игрового окна
HEIGHT = 480 # высота игрового окна
FPS = 30 # частота кадров в секунду

Дальше необходимо открыть окно игры:

# создаем игру и окно
pygame.init()
pygame.mixer.init()  # для звука
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("My Game")
clock = pygame.time.Clock()

pygame.init() — это команда, которая запускает pygame. screen — окно программы, которое создается, когда мы задаем его размер в настройках. Дальше необходимо создать clock, чтобы убедиться, что игра работает с заданной частотой кадров.

Теперь необходимо создать игровой цикл:

# Цикл игры
running = True
while running:
    # Ввод процесса (события)
    # Обновление
    # Визуализация (сборка)

Игровой цикл — это цикл while, контролируемый переменной running. Если нужно завершить игру, необходимо всего лишь поменять значение running на False. В результате цикл завершится. Теперь можно заполнить каждый раздел базовым кодом.

Раздел рендеринга (отрисовки)

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

Экраны компьютеров сделаны из пикселей, каждый из которых содержит 3 элемента: красный, зеленый и синий. Цвет пикселя определяется тем, как горит каждый из элементов:

Каждый из трех основных цветов может иметь значение от 0 (выключен) до 255 (включен на 100%), так что для каждого элемента есть 256 вариантов.

Узнать общее количество отображаемых компьютером цветов можно, умножив:

>>> 256 * 256 * 256
16,777,216

Теперь, зная, как работают цвета, можно задать их в начале программ:

# Цвета (R, G, B)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)

А после этого — заполнить весь экран.

    # Рендеринг
    screen.fill(BLACK)

Но этого недостаточно. Дисплей компьютера работает не так. Изменить пиксель — значит передать команду видеокарте, чтобы она передала соответствующую команду экрану. По компьютерным меркам это очень медленный процесс. Если нужно нарисовать на экране много всего, это займет много времени. Исправить это можно оригинальным способом, который называется — двойная буферизация. Звучит необычно, но вот что это такое.

Представьте, что у вас есть двусторонняя доска, которую можно поворачивать, показывая то одну, то вторую сторону. Одна будет дисплеем (то, что видит игрок), а вторая — оставаться скрытой, ее сможет «видеть» только компьютер. С каждым кадром рендеринг будет происходить на задней части доски. Когда отрисовка завершается, доска поворачивается и ее содержимое демонстрируется игроку.

А это значит, что процесс отрисовки происходит один раз за кадр, а не при добавлении каждого элемента.

В pygame это происходит автоматически. Нужно всего лишь сказать доске, чтобы она перевернулась, когда отрисовка завершена. Эта команда называется flip():

    # Рендеринг
    screen.fill(BLACK)
    # после отрисовки всего, переворачиваем экран
    pygame.display.flip()

Главное — сделать так, чтобы функция flip() была в конце. Если попытаться отрисовать что-то после поворота, это содержимое не отобразится на экране.

Раздел ввода (событий)

Игры еще нет, поэтому пока сложно сказать, какие кнопки или другие элементы управления понадобятся. Но нужно настроить одно важное событие. Если попытаться запустить программу сейчас, то станет понятно, что нет возможности закрыть окно. Нажать на крестик в верхнем углу недостаточно. Это тоже событие, и необходимо сообщить программе, чтобы она считала его и, соответственно, закрыла игру.

События происходят постоянно. Что, если игрок нажимает кнопку прыжка во время отрисовки? Это нельзя игнорировать, иначе игрок будет разочарован. Для этого pygame сохраняет все события, произошедшие с момента последнего кадра. Даже если игрок будет лупить по кнопкам, вы не пропустите ни одну из них. Создается список, и с помощью цикла for можно пройтись по всем из них.

    for event in pygame.event.get():
        # проверить закрытие окна
        if event.type == pygame.QUIT:
            running = False

В pygame много событий, на которые он способен реагировать. pygame.QUIT — событие, которое стартует после нажатия крестика и передает значение False переменной running, в результате чего игровой цикл заканчивается.

Контроль FPS

Пока что нечего поместить в раздел Update (обновление), но нужно убедиться, что настройка FPS контролирует скорость игры. Это можно сделать следующим образом:

while running:
    # держим цикл на правильной скорости
    clock.tick(FPS)

Команда tick() просит pygame определить, сколько занимает цикл, а затем сделать паузу, чтобы цикл (целый кадр) длился нужно время. Если задать значение FPS 30, это значит, что длина одного кадра — 1/30, то есть 0,03 секунды. Если цикл кода (обновление, рендеринг и прочее) занимает 0,01 секунды, тогда pygame сделает паузу на 0,02 секунды.

Итог

Наконец, нужно убедиться, что когда игровой цикл завершается, окно игры закрывается. Для этого нужно поместить функцию pygame.quit() в конце кода. Финальный шаблон pygame будет выглядеть вот так:

# Pygame шаблон - скелет для нового проекта Pygame
import pygame
import random

WIDTH = 360
HEIGHT = 480
FPS = 30
# Задаем цвета
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
# Создаем игру и окно
pygame.init()
pygame.mixer.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("My Game")
clock = pygame.time.Clock()
# Цикл игры
running = True
while running:
    # Держим цикл на правильной скорости
    clock.tick(FPS)
    # Ввод процесса (события)
    for event in pygame.event.get():
        # check for closing window
        if event.type == pygame.QUIT:
            running = False
<span style="box-sizing: border-box; font-weight: inherit !important; font-size: inherit; padding-left: 0px; color: rgb(152, 143, 129);" class="token comment"># Обновление</span>

<span style="box-sizing: border-box; font-weight: inherit !important; font-size: inherit; padding-left: 0px; color: rgb(152, 143, 129);" class="token comment"># Рендеринг</span>
screen<span style="box-sizing: border-box; font-weight: inherit !important; font-size: inherit; color: rgb(168, 160, 149);" class="token punctuation">.</span>fill<span style="box-sizing: border-box; font-weight: inherit !important; font-size: inherit; color: rgb(168, 160, 149);" class="token punctuation">(</span>BLACK<span style="box-sizing: border-box; font-weight: inherit !important; font-size: inherit; color: rgb(168, 160, 149);" class="token punctuation">)</span>
<span style="box-sizing: border-box; font-weight: inherit !important; font-size: inherit; padding-left: 0px; color: rgb(152, 143, 129);" class="token comment"># После отрисовки всего, переворачиваем экран</span>
pygame<span style="box-sizing: border-box; font-weight: inherit !important; font-size: inherit; color: rgb(168, 160, 149);" class="token punctuation">.</span>display<span style="box-sizing: border-box; font-weight: inherit !important; font-size: inherit; color: rgb(168, 160, 149);" class="token punctuation">.</span>flip<span style="box-sizing: border-box; font-weight: inherit !important; font-size: inherit; color: rgb(168, 160, 149);" class="token punctuation">(</span><span style="box-sizing: border-box; font-weight: inherit !important; font-size: inherit; color: rgb(168, 160, 149);" class="token punctuation">)</span>

pygame.quit()

Ура! У вас есть рабочий шаблон Pygame. Сохраните его в файле с понятным названием, например, pygame_template.py, чтобы можно было использовать его каждый раз при создании нового проекта pygame.

В следующем руководстве этот шаблон будет использован как отправная точка для изучения процесса отрисовки объектов на экране и их движения.

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

К концу этой статьи вы сможете:

  • Нарисуйте предметы на вашем экране

  • Воспроизведение звуковых эффектов и музыки

  • Обрабатывать пользовательский ввод

  • Реализация циклов событий

  • Опишите, чем игровое программирование отличается от стандартного процедурного программирования на Python

Вы можете получить весь код в этой статье, чтобы следовать:

Фон и настройка

pygame — это оболочка Python дляSDL library, что означаетSimple DirectMedia Layer. SDL обеспечивает кроссплатформенный доступ к базовым компонентам мультимедийного оборудования вашей системы, таким как звук, видео, мышь, клавиатура и джойстик. pygame начал свою жизнь как замена остановившемусяPySDL project. Кросс-платформенный характер как SDL, так иpygame означает, что вы можете писать игры и мультимедийные программы Python для любой платформы, которая их поддерживает!

Чтобы установитьpygame на вашу платформу, используйте соответствующую командуpip:

Вы можете проверить установку, загрузив один из примеров, поставляемых с библиотекой:

$ python3 -m pygame.examples.aliens

Если появляется окно игры, значитpygame установлен правильно! Если у вас возникнут проблемы, тоGetting Started guide описывает некоторые известные проблемы и предостережения для всех платформ.

Основная программа PyGame

Прежде чем переходить к деталям, давайте взглянем на базовую программуpygame. Эта программа создает окно, заполняет фон белым и рисует синий круг в середине:

 1 # Simple pygame program
 2
 3 # Import and initialize the pygame library
 4 import pygame
 5 pygame.init()
 6
 7 # Set up the drawing window
 8 screen = pygame.display.set_mode([500, 500])
 9
10 # Run until the user asks to quit
11 running = True
12 while running:
13
14     # Did the user click the window close button?
15     for event in pygame.event.get():
16         if event.type == pygame.QUIT:
17             running = False
18
19     # Fill the background with white
20     screen.fill((255, 255, 255))
21
22     # Draw a solid blue circle in the center
23     pygame.draw.circle(screen, (0, 0, 255), (250, 250), 75)
24
25     # Flip the display
26     pygame.display.flip()
27
28 # Done! Time to quit.
29 pygame.quit()

Когда вы запустите эту программу, вы увидите окно, которое выглядит так:

A simple pygame program

Давайте разберем этот код, раздел за разделом:

  • Lines 4 and 5 импортирует и инициализирует библиотекуpygame. Без этих строк нетpygame.

  • Line 8 устанавливает окно отображения вашей программы. Вы предоставляете либо список, либо кортеж, который определяет ширину и высоту создаваемого окна. Эта программа использует список для создания квадратного окна с 500 пикселями на каждой стороне.

  • Lines 11 and 12 настраиваетgame loop для контроля завершения программы. Об этом вы узнаете позже в этом уроке.

  • Lines 15 to 17 просматривает и обрабатываетevents в игровом цикле. Вы придете к событиям чуть позже. В этом случае обрабатывается только событиеpygame.QUIT, которое происходит, когда пользователь нажимает кнопку закрытия окна.

  • Line 20 заполняет окно сплошным цветом. screen.fill() принимает либо список, либо кортеж, определяющий значения RGB для цвета. Поскольку(255, 255, 255) предоставлен, окно заполняется белым цветом.

  • Line 23 рисует круг в окне, используя следующие параметры:

    • screen: окно, в котором нужно рисовать

    • (0, 0, 255): кортеж, содержащий значения цвета RGB

    • (250, 250): кортеж, определяющий координаты центра круга

    • 75: радиус нарисованного круга в пикселях

  • Line 26 обновляет содержимое дисплея на экране. Без этого звонка в окне ничего не появляется!

  • Line 29 выходит изpygame. Это происходит только после завершения цикла.

Этоpygame версия «Hello, World». Теперь давайте углубимся в концепции, лежащие в основе этого кода.

PyGame Concepts

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

Инициализация и Модули

Библиотекаpygame — этоcomposed of a number of Python constructs, которая включает несколько разныхmodules. Эти модули обеспечивают абстрактный доступ к конкретному оборудованию в вашей системе, а также унифицированные методы для работы с этим оборудованием. Например,display обеспечивает единообразный доступ к вашему видео дисплею, аjoystick позволяет абстрактно управлять вашим джойстиком.

После импорта библиотекиpygame в приведенном выше примере первое, что вы сделали, — этоinitialize PyGame с использованиемpygame.init(). Эта функцияcalls the separate init() functions всех включенных модулейpygame. Поскольку эти модули являются абстракциями для конкретного оборудования, этот шаг инициализации необходим для того, чтобы вы могли работать с одним и тем же кодом в Linux, Windows и Mac.

Дисплеи и поверхности

В дополнение к модулямpygame также включает несколько Pythonclasses, которые инкапсулируют концепции, не зависящие от оборудования. Один из них —Surface, который, по сути, определяет прямоугольную область, на которой вы можете рисовать. ОбъектыSurface используются во многих контекстах вpygame. Позже вы увидите, как загрузить изображение вSurface и отобразить его на экране.

Вpygame все просматривается на одном созданном пользователемdisplay, который может быть окном или полноэкранным. Отображение создается с использованием.set_mode(), который возвращаетSurface, представляющий видимую часть окна. Именно этотSurface вы передаете в функции рисования, такие какpygame.draw.circle(), и содержимое этогоSurface выводится на дисплей, когда вы вызываетеpygame.display.flip().

Изображения и Rects

Ваша базовая программаpygame нарисовала фигуру прямо на экранеSurface, но вы также можете работать с изображениями на диске. image module позволяет создавать изображенияload иsave в различных популярных форматах. Изображения загружаются в объектыSurface, которыми затем можно управлять и отображать различными способами.

Как упоминалось выше, объектыSurface представлены прямоугольниками, как и многие другие объекты вpygame, такие как изображения и окна. Прямоугольники настолько широко используются, что существуетspecial Rect class только для их обработки. Вы будете использовать объекты и изображенияRect в своей игре, чтобы рисовать игроков и врагов и управлять столкновениями между ними.

Ладно, достаточно теории. Давайте разработаем и напишем игру!

Базовый дизайн игры

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

  • Цель игры — избежать препятствий:

    • Плеер запускается с левой стороны экрана.

    • Препятствия входят случайным образом справа и движутся влево по прямой линии.

  • Игрок может двигаться влево, вправо, вверх или вниз, чтобы избежать препятствий.

  • Плеер не может отойти от экрана.

  • Игра заканчивается либо тогда, когда игрок сталкивается с препятствием, либо когда пользователь закрывает окно.

Когда он описывал программные проекты,former colleague of mine говорил: «Вы не знаете, что делаете, пока не узнаете, чего не делаете». Имея это в виду, вот некоторые вещи, которые не будут рассмотрены в этом руководстве:

  • Нет нескольких жизней

  • Нет счета

  • Нет возможности атаки игрока

  • Нет продвигающихся уровней

  • Нет боссов

Вы можете сами попробовать добавить эти и другие функции в свою программу.

Давайте начнем!

Импорт и инициализация PyGame

После импортаpygame вам также потребуется его инициализировать. Это позволяетpygame подключать свои абстракции к вашему конкретному оборудованию:

 1 # Import the pygame module
 2 import pygame
 3
 4 # Import pygame.locals for easier access to key coordinates
 5 # Updated to conform to flake8 and black standards
 6 from pygame.locals import (
 7     K_UP,
 8     K_DOWN,
 9     K_LEFT,
10     K_RIGHT,
11     K_ESCAPE,
12     KEYDOWN,
13     QUIT,
14 )
15
16 # Initialize pygame
17 pygame.init()

Библиотекаpygame определяет многие вещи помимо модулей и классов. Он также определяет некоторыеlocal constants для таких вещей, как нажатия клавиш, движения мыши и атрибуты отображения. Вы ссылаетесь на эти константы, используя синтаксисpygame.<CONSTANT>. Импортируя определенные константы изpygame.locals, вы можете вместо этого использовать синтаксис<CONSTANT>. Это сэкономит вам несколько нажатий клавиш и улучшит общую читабельность.

Настройка дисплея

Теперь вам нужно что-то нарисовать! Создайтеscreen как общий холст:

 1 # Import the pygame module
 2 import pygame
 3
 4 # Import pygame.locals for easier access to key coordinates
 5 # Updated to conform to flake8 and black standards
 6 from pygame.locals import (
 7     K_UP,
 8     K_DOWN,
 9     K_LEFT,
10     K_RIGHT,
11     K_ESCAPE,
12     KEYDOWN,
13     QUIT,
14 )
15
16 # Initialize pygame
17 pygame.init()
18
19 # Define constants for the screen width and height
20 SCREEN_WIDTH = 800
21 SCREEN_HEIGHT = 600
22
23 # Create the screen object
24 # The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
25 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

Вы создаете экран для использования, вызываяpygame.display.set_mode() и передавая кортеж или список с желаемой шириной и высотой. В этом случае размер окна составляет 800×600, что определяется константамиSCREEN_WIDTH иSCREEN_HEIGHT в строках 20 и 21. Это возвращаетSurface, который представляет внутренние размеры окна. Это часть окна, которой вы можете управлять, в то время как ОС контролирует границы окна и строку заголовка.

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

Настройка игрового цикла

В каждой игре от Pong до Fortnite используетсяgame loop для управления игровым процессом. Игровой цикл выполняет четыре очень важных вещи:

  1. Обрабатывает пользовательский ввод

  2. Обновляет состояние всех игровых объектов

  3. Обновляет дисплей и аудио выход

  4. Поддерживает скорость игры

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

  1. Игрок сталкивается с препятствием. (Вы узнаете об обнаружении столкновений позже.)

  2. Игрок закрывает окно.

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

Обработка событий

Нажатие клавиш, движения мыши и даже движения джойстика являются одними из способов, которыми пользователь может обеспечить ввод. Результатом всего пользовательского ввода является созданиеevent. События могут происходить в любое время и часто (но не всегда) происходят вне программы. Все события вpygame помещаются в очередь событий, к которой затем можно получить доступ и управлять ими. Работа с событиями обозначаетсяhandling них, а код для этого называетсяevent handler.

Каждое событие вpygame имеет связанное с ним событиеtype. Для вашей игры типы событий, на которых вы сосредоточитесь, — это нажатия клавиш и закрытие окна. События нажатия клавиш имеют тип событияKEYDOWN, а событие закрытия окна имеет типQUIT. Различные типы событий могут также иметь другие данные, связанные с ними. Например, тип событияKEYDOWN также имеет переменную с именемkey, чтобы указать, какая клавиша была нажата.

Вы получаете доступ к списку всех активных событий в очереди, вызываяpygame.event.get(). Затем вы просматриваете этот список, проверяете каждый тип события и отвечаете соответственно:

27 # Variable to keep the main loop running
28 running = True
29
30 # Main loop
31 while running:
32     # Look at every event in the queue
33     for event in pygame.event.get():
34         # Did the user hit a key?
35         if event.type == KEYDOWN:
36             # Was it the Escape key? If so, stop the loop.
37             if event.key == K_ESCAPE:
38                 running = False
39
40         # Did the user click the window close button? If so, stop the loop.
41         elif event.type == QUIT:
42             running = False

Давайте внимательнее посмотрим на этот игровой цикл:

  • Line 28 устанавливает управляющую переменную для игрового цикла. Чтобы выйти из цикла и игры, вы устанавливаетеrunning = False. Игровой цикл начинается в строке 29.

  • Line 31 запускает обработчик событий, просматривая все события в очереди событий. Если событий нет, список пуст, и обработчик ничего не сделает.

  • Lines 35 to 38 проверяет, является ли текущийevent.type событиемKEYDOWN. Если это так, то программа проверяет, какая клавиша была нажата, глядя на атрибутevent.key. Если ключ является ключом[.kbd .key-escape]#Esc #, обозначеннымK_ESCAPE, то он выходит из игрового цикла, устанавливаяrunning = False.

  • Lines 41 and 42 выполняет аналогичную проверку для типа события с именемQUIT. Это событие происходит только тогда, когда пользователь нажимает кнопку закрытия окна. Пользователь также может использовать любое другое действие операционной системы, чтобы закрыть окно.

Когда вы добавите эти строки в предыдущий код и запустите его, вы увидите окно с пустым или черным экраном:

An empty

Окно не исчезнет, ​​пока вы не нажмете клавишу[.kbd .key-escape]#Esc # или иным образом не вызовете событиеQUIT, закрыв окно.

Рисование на экране

В примере программы вы рисовали на экране с помощью двух команд:

  1. screen.fill() для заливки фона

  2. pygame.draw.circle(), чтобы нарисовать круг

Теперь вы узнаете о третьем способе рисования на экране: с помощьюSurface.

Напомним, чтоSurface — это прямоугольный объект, на котором вы можете рисовать, как чистый лист бумаги. Объектscreen — этоSurface, и вы можете создавать свои собственные объектыSurface отдельно от экрана дисплея. Давайте посмотрим, как это работает:

44 # Fill the screen with white
45 screen.fill((255, 255, 255))
46
47 # Create a surface and pass in a tuple containing its length and width
48 surf = pygame.Surface((50, 50))
49
50 # Give the surface a color to separate it from the background
51 surf.fill((0, 0, 0))
52 rect = surf.get_rect()

После заполнения экрана белым цветом в строке 45 создается новыйSurface в строке 48. ЭтотSurface имеет ширину 50 пикселей, высоту 50 пикселей и назначенsurf. На этом этапе вы относитесь к нему так же, как кscreen. Итак, на линии 51 вы заполняете его черным. Вы также можете получить доступ к его базовомуRect, используя.get_rect(). Он сохраняется какrect для дальнейшего использования.

Используя.blit() и.flip()

Просто создать новыйSurface недостаточно, чтобы увидеть его на экране. Для этого вам нужноblitSurface на другойSurface. Терминblit означаетBlock Transfer, а.blit() — это то, как вы копируете содержимое одногоSurface в другой. Вы можете только.blit() от одногоSurface к другому, но поскольку экран — это просто еще одинSurface, это не проблема. Вот как вы рисуетеsurf на экране:

54 # This line says "Draw surf onto the screen at the center"
55 screen.blit(surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
56 pygame.display.flip()

Вызов.blit() в строке 55 принимает два аргумента:

  1. Surface для рисования

  2. Место для его рисования на источникеSurface

Координаты(SCREEN_WIDTH/2, SCREEN_HEIGHT/2) говорят вашей программе разместитьsurf точно в центре экрана, но это не совсем так:

Blitting a surface onto the screen

Причина, по которой изображение выглядит не по центру, заключается в том, что.blit() помещаетtop-left corner изsurf в указанное место. Если вы хотите, чтобыsurf был центрирован, вам нужно будет выполнить некоторые вычисления, чтобы сместить его вверх и влево. Вы можете сделать это, вычтя ширину и высотуsurf из ширины и высоты экрана, разделив каждую на 2, чтобы определить местонахождение центра, а затем передав эти числа в качестве аргументов вscreen.blit():

54 # Put the center of surf at the center of the display
55 surf_center = (
56     (SCREEN_WIDTH-surf.get_width())/2,
57     (SCREEN_HEIGHT-surf.get_height())/2
58 )
59
60 # Draw surf at the new coordinates
61 screen.blit(surf, surf_center)
62 pygame.display.flip()

Обратите внимание на вызовpygame.display.flip() после вызоваblit(). Это обновляет весь экран со всем, что было нарисовано с момента последнего переворота. Без вызова.flip() ничего не отображается.

Спрайты

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

С точки зрения программированияsprite — это 2D-представление чего-либо на экране. По сути, это картина. pygame предоставляетSprite class, который предназначен для хранения одного или нескольких графических представлений любого игрового объекта, который вы хотите отобразить на экране. Чтобы использовать его, вы создаете новый класс, расширяющийSprite. Это позволяет использовать его встроенные методы.

игроки

Вот как вы используете объектыSprite в текущей игре для определения игрока. Вставьте этот код после строки 18:

20 # Define a Player object by extending pygame.sprite.Sprite
21 # The surface drawn on the screen is now an attribute of 'player'
22 class Player(pygame.sprite.Sprite):
23     def __init__(self):
24         super(Player, self).__init__()
25         self.surf = pygame.Surface((75, 25))
26         self.surf.fill((255, 255, 255))
27         self.rect = self.surf.get_rect()

Сначала вы определяетеPlayer, расширяяpygame.sprite.Sprite в строке 22. Затем.__init__() использует.super() для вызова метода.__init__() дляSprite. Для получения дополнительной информации о том, почему это необходимо, вы можете прочитатьSupercharge Your Classes With Python super().

Затем вы определяете и инициализируете.surf, чтобы удерживать изображение для отображения, которое в настоящее время представляет собой белое поле. Вы также определяете и инициализируете.rect, который вы будете использовать для рисования игрока позже. Чтобы использовать этот новый класс, вам нужно создать новый объект и изменить код для рисования. Разверните блок кода ниже, чтобы увидеть все это вместе:

Запустите этот код. Вы увидите белый прямоугольник примерно в середине экрана:

Basic player sprite being drawn

Как вы думаете, что произойдет, если вы измените строку 59 наscreen.blit(player.surf, player.rect)? Попробуйте и посмотрите:

55 # Fill the screen with black
56 screen.fill((0, 0, 0))
57
58 # Draw the player on the screen
59 screen.blit(player.surf, player.rect)
60
61 # Update the display
62 pygame.display.flip()

Когда вы передаетеRect в.blit(), он использует координаты верхнего левого угла для рисования поверхности. Вы будете использовать это позже, чтобы заставить вашего игрока двигаться!

Пользовательский ввод

До сих пор вы научились настраиватьpygame и рисовать объекты на экране. Теперь начинается самое интересное! Вы сделаете плеер управляемым с помощью клавиатуры.

Ранее вы видели, чтоpygame.event.get() возвращает список событий в очереди событий, которую вы просматриваете на предмет типов событийKEYDOWN. Ну, это не единственный способ читать нажатия клавиш. pygame также предоставляетpygame.event.get_pressed(), который возвращаетdictionary, содержащий все текущие событияKEYDOWN в очереди.

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

54 # Get the set of keys pressed and check for user input
55 pressed_keys = pygame.key.get_pressed()

Затем вы пишете метод вPlayer для приема этого словаря. Это будет определять поведение спрайта на основе нажатых клавиш. Вот как это может выглядеть:

29 # Move the sprite based on user keypresses
30 def update(self, pressed_keys):
31     if pressed_keys[K_UP]:
32         self.rect.move_ip(0, -5)
33     if pressed_keys[K_DOWN]:
34         self.rect.move_ip(0, 5)
35     if pressed_keys[K_LEFT]:
36         self.rect.move_ip(-5, 0)
37     if pressed_keys[K_RIGHT]:
38         self.rect.move_ip(5, 0)

K_UP,K_DOWN,K_LEFT иK_RIGHT соответствуют клавишам со стрелками на клавиатуре. Если словарная запись для этого ключа —True, значит, этот ключ нажат, и вы перемещаете игрока.rect в правильном направлении. Здесь вы используете.move_ip(), что означаетmove in place, для перемещения текущегоRect.

Затем вы можете вызывать.update() каждый кадр, чтобы перемещать спрайт игрока в ответ на нажатия клавиш. Добавьте этот вызов сразу после вызова.get_pressed():

52 # Main loop
53 while running:
54     # for loop through the event queue
55     for event in pygame.event.get():
56         # Check for KEYDOWN event
57         if event.type == KEYDOWN:
58             # If the Esc key is pressed, then exit the main loop
59             if event.key == K_ESCAPE:
60                 running = False
61         # Check for QUIT event. If QUIT, then set running to false.
62         elif event.type == QUIT:
63             running = False
64
65     # Get all the keys currently pressed
66     pressed_keys = pygame.key.get_pressed()
67
68     # Update the player sprite based on user keypresses
69     player.update(pressed_keys)
70
71     # Fill the screen with black
72     screen.fill((0, 0, 0))

Теперь вы можете перемещать прямоугольник игрока по экрану с помощью клавиш со стрелками:

Keypresses moving a sprite in pygame

Вы можете заметить две небольшие проблемы:

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

  2. Прямоугольник игрока может сдвинуться с экрана. Давайте решим это сейчас.

Чтобы игрок оставался на экране, вам нужно добавить некоторую логику, чтобы определять, собирается лиrect уйти с экрана. Для этого вы проверяете, не переместились ли координатыrect за пределы экрана. Если это так, то вы даете команду программе переместить ее обратно к краю:

25 # Move the sprite based on user keypresses
26 def update(self, pressed_keys):
27     if pressed_keys[K_UP]:
28         self.rect.move_ip(0, -5)
29     if pressed_keys[K_DOWN]:
30         self.rect.move_ip(0, 5)
31     if pressed_keys[K_LEFT]:
32         self.rect.move_ip(-5, 0)
33     if pressed_keys[K_RIGHT]:
34         self.rect.move_ip(5, 0)
35
36     # Keep player on the screen
37     if self.rect.left < 0:
38         self.rect.left = 0
39     if self.rect.right > SCREEN_WIDTH:
40         self.rect.right = SCREEN_WIDTH
41     if self.rect.top <= 0:
42         self.rect.top = 0
43     if self.rect.bottom >= SCREEN_HEIGHT:
44         self.rect.bottom = SCREEN_HEIGHT

Здесь вместо использования.move() вы просто меняете соответствующие координаты.top,.bottom,.left или.right напрямую. Проверьте это, и вы увидите, что прямоугольник игрока больше не может сдвинуться с экрана.

Теперь давайте добавим несколько врагов!

враги

Что за игра без врагов? Вы будете использовать методы, которые вы уже изучили, чтобы создать базовый класс врагов, а затем создадите множество из них, чтобы ваш игрок избегал их. Сначала импортируйте библиотекуrandom:

 4 # Import random for random numbers
 5 import random

Затем создайте новый класс спрайтов с именемEnemy, следуя тому же шаблону, который вы использовали дляPlayer:

55 # Define the enemy object by extending pygame.sprite.Sprite
56 # The surface you draw on the screen is now an attribute of 'enemy'
57 class Enemy(pygame.sprite.Sprite):
58     def __init__(self):
59         super(Enemy, self).__init__()
60         self.surf = pygame.Surface((20, 10))
61         self.surf.fill((255, 255, 255))
62         self.rect = self.surf.get_rect(
63             center=(
64                 random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
65                 random.randint(0, SCREEN_HEIGHT),
66             )
67         )
68         self.speed = random.randint(5, 20)
69
70     # Move the sprite based on speed
71     # Remove the sprite when it passes the left edge of the screen
72     def update(self):
73         self.rect.move_ip(-self.speed, 0)
74         if self.rect.right < 0:
75             self.kill()

МеждуEnemy иPlayer есть четыре заметных различия:

  1. On lines 62 to 67, вы обновляетеrect, чтобы он был случайным местом вдоль правого края экрана. Центр прямоугольника находится за пределами экрана. Он расположен в некотором месте между 20 и 100 пикселями от правого края и где-то между верхним и нижним краями.

  2. On line 68, вы определяете.speed как случайное число от 5 до 20. Это указывает, как быстро этот враг движется к игроку.

  3. On lines 73 to 76, вы определяете.update(). Это не требует никаких аргументов, поскольку враги двигаются автоматически. Вместо этого.update() перемещает врага в левую часть экрана на.speed, определенное при его создании.

  4. On line 74, вы проверяете, ушел ли противник за пределы экрана. Чтобы убедиться, чтоEnemy полностью за пределами экрана и не исчезнет просто так, пока он все еще виден, вы убедитесь, что правая сторона.rect прошла за левую часть экрана. Когда противник находится за кадром, вы вызываете.kill(), чтобы предотвратить его дальнейшую обработку.

Итак, что делает.kill()? Чтобы понять это, вы должны знать оSprite Groups.

Спрайт группы

Еще один очень полезный класс, который предоставляетpygame, — этоSprite Group. Это объект, содержащий группу объектовSprite. Так зачем его использовать? Разве вы не можете вместо этого просто отслеживать свои объектыSprite в списке? Что ж, вы можете, но преимущество использованияGroup заключается в методах, которые он предоставляет. Эти методы помогают определить, столкнулся ли какой-либоEnemy сPlayer, что значительно упрощает обновление.

Давайте посмотрим, как создавать группы спрайтов. Вы создадите два разных объектаGroup:

  1. ПервыеGroup будут удерживать каждыеSprite в игре.

  2. ВторойGroup будет содержать только объектыEnemy.

Вот как это выглядит в коде:

82 # Create the 'player'
83 player = Player()
84
85 # Create groups to hold enemy sprites and all sprites
86 # - enemies is used for collision detection and position updates
87 # - all_sprites is used for rendering
88 enemies = pygame.sprite.Group()
89 all_sprites = pygame.sprite.Group()
90 all_sprites.add(player)
91
92 # Variable to keep the main loop running
93 running = True

Когда вы вызываете.kill(),Sprite удаляется из каждогоGroup, которому он принадлежит. Это также удаляет ссылки наSprite, что позволяет сборщику мусора Python при необходимости освобождать память.

Теперь, когда у вас есть группаall_sprites, вы можете изменить способ рисования объектов. Вместо вызова.blit() только дляPlayer, вы можете перебирать все вall_sprites:

117 # Fill the screen with black
118 screen.fill((0, 0, 0))
119
120 # Draw all sprites
121 for entity in all_sprites:
122     screen.blit(entity.surf, entity.rect)
123
124 # Flip everything to the display
125 pygame.display.flip()

Теперь все, что помещено вall_sprites, будет отрисовываться с каждым кадром, будь то враг или игрок.

Есть только одна проблема … У тебя нет врагов! Вы можете создать кучу врагов в начале игры, но игра быстро станет скучной, когда все они покинут экран через несколько секунд. Вместо этого давайте рассмотрим, как поддерживать постоянный приток врагов в ходе игры.

Пользовательские события

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

  1. Создайте новыйEnemy.

  2. Добавьте его вall_sprites иenemies.

У вас уже есть код, который обрабатывает случайные события. Цикл обработки событий предназначен для поиска случайных событий, происходящих в каждом кадре, и надлежащего обращения с ними. К счастью,pygame не ограничивает вас использованием только тех типов событий, которые он определил. Вы можете определить свои собственные события для обработки по своему усмотрению.

Давайте посмотрим, как создать пользовательское событие, которое генерируется каждые несколько секунд. Вы можете создать собственное событие, назвав его:

78 # Create the screen object
79 # The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
80 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
81
82 # Create a custom event for adding a new enemy
83 ADDENEMY = pygame.USEREVENT + 1
84 pygame.time.set_timer(ADDENEMY, 250)
85
86 # Instantiate player. Right now, this is just a rectangle.
87 player = Player()

pygame определяет события внутри как целые числа, поэтому вам нужно определить новое событие с уникальным целым числом. Последнее событиеpygame резервов называетсяUSEREVENT, поэтому определениеADDENEMY = pygame.USEREVENT + 1 в строке 83 гарантирует его уникальность.

Затем вам нужно регулярно вставлять это новое событие в очередь на протяжении всей игры. Вот тут и пригодится модульtime. Строка 84 запускает новое событиеADDENEMY каждые 250 миллисекунд или четыре раза в секунду. Вы вызываете.set_timer() вне игрового цикла, поскольку вам нужен только один таймер, но он будет срабатывать на протяжении всей игры.

Добавьте код для обработки вашего нового события:

100 # Main loop
101 while running:
102     # Look at every event in the queue
103     for event in pygame.event.get():
104         # Did the user hit a key?
105         if event.type == KEYDOWN:
106             # Was it the Escape key? If so, stop the loop.
107             if event.key == K_ESCAPE:
108                 running = False
109
110         # Did the user click the window close button? If so, stop the loop.
111         elif event.type == QUIT:
112             running = False
113
114         # Add a new enemy?
115         elif event.type == ADDENEMY:
116             # Create the new enemy and add it to sprite groups
117             new_enemy = Enemy()
118             enemies.add(new_enemy)
119             all_sprites.add(new_enemy)
120
121     # Get the set of keys pressed and check for user input
122     pressed_keys = pygame.key.get_pressed()
123     player.update(pressed_keys)
124
125     # Update enemy position
126     enemies.update()

Каждый раз, когда обработчик событий видит новое событиеADDENEMY в строке 115, он создаетEnemy и добавляет его кenemies иall_sprites. ПосколькуEnemy находится вall_sprites, он будет отрисовываться каждый кадр. Вам также необходимо вызватьenemies.update() в строке 126, который обновляет все вenemies, чтобы убедиться, что они перемещаются правильно:

Enemies flying by in pygame

Однако это не единственная причина, по которой существует группа только дляenemies.

Обнаружение столкновений

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

Здесь пригодится фреймворк вродеpygame! Написание кода обнаружения столкновений утомительно, ноpygame имеет МНОГОcollision detection methods, доступное для использования.

В этом руководстве вы будете использовать метод под названием.spritecollideany(), который читается как «спрайт сталкивается с любым». Этот метод принимает в качестве параметровSprite иGroup. Он просматривает каждый объект вGroup и проверяет, пересекается ли его.rect с.rectSprite. Если да, то возвращаетсяTrue. В противном случае возвращаетсяFalse. Это идеально подходит для этой игры, поскольку вам нужно проверить, не сталкивается ли отдельныйplayer с одним изGroup изenemies.

Вот как это выглядит в коде:

130 # Draw all sprites
131 for entity in all_sprites:
132     screen.blit(entity.surf, entity.rect)
133
134 # Check if any enemies have collided with the player
135 if pygame.sprite.spritecollideany(player, enemies):
136     # If so, then remove the player and stop the loop
137     player.kill()
138     running = False

Строка 135 проверяет, столкнулся лиplayer с каким-либо из объектов вenemies. Если это так, то вызываетсяplayer.kill(), чтобы удалить его из каждой группы, к которой он принадлежит. Поскольку визуализируются только объекты вall_sprites,player больше не будет отображаться. После того, как игрок был убит, вам также необходимо выйти из игры, поэтому вы устанавливаетеrunning = False для выхода из игрового цикла в строке 138.

Теперь у вас есть основные элементы игры:

Pygame window

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

Спрайт Изображения

Хорошо, у вас есть игра, но давайте будем честными … Это некрасиво. Игрок и враги — это просто белые блоки на черном фоне. КогдаPong был новым, это было по последнему слову техники, но он больше не работает. Давайте заменим все эти скучные белые прямоугольники более прохладными изображениями, которые сделают игру настоящей игрой.

Ранее вы узнали, что изображения на диске могут быть загружены вSurface с некоторой помощью модуляimage. Для этого урока мы сделали небольшую струю для игрока и несколько ракет для врагов. Вы можете использовать этот рисунок, нарисовать свой собственный или загрузить несколькоfree game art assets для использования. Вы можете нажать на ссылку ниже, чтобы загрузить искусство, используемое в этом руководстве:

Изменение конструкторов объектов

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

 7 # Import pygame.locals for easier access to key coordinates
 8 # Updated to conform to flake8 and black standards
 9 # from pygame.locals import *
10 from pygame.locals import (
11     RLEACCEL,
12     K_UP,
13     K_DOWN,
14     K_LEFT,
15     K_RIGHT,
16     K_ESCAPE,
17     KEYDOWN,
18     QUIT,
19 )
20
21 # Define constants for the screen width and height
22 SCREEN_WIDTH = 800
23 SCREEN_HEIGHT = 600
24
25
26 # Define the Player object by extending pygame.sprite.Sprite
27 # Instead of a surface, use an image for a better-looking sprite
28 class Player(pygame.sprite.Sprite):
29     def __init__(self):
30         super(Player, self).__init__()
31         self.image = pygame.image.load("jet.png").convert()
32         self.image.set_colorkey((255, 255, 255), RLEACCEL)
33         self.rect = self.image.get_rect()

Давайте немного распакуем строку 31. pygame.image.load() загружает образ с диска. Вы передаете ему путь к файлу. Он возвращаетSurface, а вызов.convert() оптимизируетSurface, ускоряя будущие вызовы.blit().

В строке 32 используется.set_colorkey(), чтобы указать, что цветpygame будет отображаться как прозрачный. В этом случае вы выбираете белый, потому что это цвет фона изображения струи. КонстантаRLEACCEL — это необязательный параметр, который помогает быстрее отображатьpygame на дисплеях без ускорения. Это добавляется к оператору импортаpygame.locals в строке 11.

Больше ничего не нужно менять. Изображение по-прежнемуSurface, только теперь на нем нарисовано изображение. Вы все еще используете его таким же образом.

Вот как выглядят похожие изменения вEnemy:

59 # Define the enemy object by extending pygame.sprite.Sprite
60 # Instead of a surface, use an image for a better-looking sprite
61 class Enemy(pygame.sprite.Sprite):
62     def __init__(self):
63         super(Enemy, self).__init__()
64         self.surf = pygame.image.load("missile.png").convert()
65         self.surf.set_colorkey((255, 255, 255), RLEACCEL)
66         # The starting position is randomly generated, as is the speed
67         self.rect = self.surf.get_rect(
68             center=(
69                 random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
70                 random.randint(0, SCREEN_HEIGHT),
71             )
72         )
73         self.speed = random.randint(5, 20)

Запуск программы сейчас должен показать, что это та же игра, что и у вас раньше, только теперь вы добавили красивую графикуskins с изображениями. Но зачем останавливаться на том, чтобы заставить игрока и вражеских спрайтов выглядеть красиво? Давайте добавим несколько облаков, проходящих мимо, чтобы создать впечатление струи, летящей по небу.

Добавление фоновых изображений

Для фоновых облаков вы используете те же принципы, что и дляPlayer иEnemy:

  1. Создайте классCloud.

  2. Добавьте к нему изображение облака.

  3. Создайте метод.update(), который перемещаетcloud в левую часть экрана.

  4. Создайте настраиваемое событие и обработчик для создания новых объектовcloud через заданный интервал времени.

  5. Добавьте вновь созданные объектыcloud в новыйGroup с именемclouds.

  6. Обновите и нарисуйтеclouds в своем игровом цикле.

Вот как выглядитCloud:

 83 # Define the cloud object by extending pygame.sprite.Sprite
 84 # Use an image for a better-looking sprite
 85 class Cloud(pygame.sprite.Sprite):
 86     def __init__(self):
 87         super(Cloud, self).__init__()
 88         self.surf = pygame.image.load("cloud.png").convert()
 89         self.surf.set_colorkey((0, 0, 0), RLEACCEL)
 90         # The starting position is randomly generated
 91         self.rect = self.surf.get_rect(
 92             center=(
 93                 random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
 94                 random.randint(0, SCREEN_HEIGHT),
 95             )
 96
 97     # Move the cloud based on a constant speed
 98     # Remove the cloud when it passes the left edge of the screen
 99     def update(self):
100         self.rect.move_ip(-5, 0)
101         if self.rect.right < 0:
102             self.kill()

Это все должно выглядеть очень знакомым. Это почти то же самое, что иEnemy.

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

116 # Create custom events for adding a new enemy and a cloud
117 ADDENEMY = pygame.USEREVENT + 1
118 pygame.time.set_timer(ADDENEMY, 250)
119 ADDCLOUD = pygame.USEREVENT + 2
120 pygame.time.set_timer(ADDCLOUD, 1000)

Это говорит о том, что нужно подождать 1000 миллисекунд или одну секунду перед созданием следующегоcloud.

Затем создайте новыйGroup для хранения каждого вновь созданногоcloud:

125 # Create groups to hold enemy sprites, cloud sprites, and all sprites
126 # - enemies is used for collision detection and position updates
127 # - clouds is used for position updates
128 # - all_sprites is used for rendering
129 enemies = pygame.sprite.Group()
130 clouds = pygame.sprite.Group()
131 all_sprites = pygame.sprite.Group()
132 all_sprites.add(player)

Затем добавьте обработчик для нового событияADDCLOUD в обработчике событий:

137 # Main loop
138 while running:
139     # Look at every event in the queue
140     for event in pygame.event.get():
141         # Did the user hit a key?
142         if event.type == KEYDOWN:
143             # Was it the Escape key? If so, then stop the loop.
144             if event.key == K_ESCAPE:
145                 running = False
146
147         # Did the user click the window close button? If so, stop the loop.
148         elif event.type == QUIT:
149             running = False
150
151         # Add a new enemy?
152         elif event.type == ADDENEMY:
153             # Create the new enemy and add it to sprite groups
154             new_enemy = Enemy()
155             enemies.add(new_enemy)
156             all_sprites.add(new_enemy)
157
158         # Add a new cloud?
159         elif event.type == ADDCLOUD:
160             # Create the new cloud and add it to sprite groups
161             new_cloud = Cloud()
162             clouds.add(new_cloud)
163             all_sprites.add(new_cloud)

Наконец, убедитесь, чтоclouds обновляются каждый кадр:

167 # Update the position of enemies and clouds
168 enemies.update()
169 clouds.update()
170
171 # Fill the screen with sky blue
172 screen.fill((135, 206, 250))

Строка 172 обновляет исходныйscreen.fill(), чтобы заполнить экран приятным небесно-голубым цветом. Вы можете изменить этот цвет на что-то другое. Может быть, вы хотите инопланетный мир с пурпурным небом, ядовитую пустошь в неоново-зеленом или поверхность Марса в красном!

Обратите внимание, что каждый новыйCloud иEnemy добавляется кall_sprites, а также кclouds иenemies. Это сделано потому, что каждая группа используется для отдельной цели:

  • Rendering выполняется с использованиемall_sprites.

  • Position updates выполняется с использованиемclouds иenemies.

  • Collision detection выполняется с использованиемenemies.

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

Скорость игры

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

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

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

ИспользованиеClock для установки воспроизводимой частоты кадров требует всего двух строк кода. Первый создает новыйClock перед началом игрового цикла:

106 # Setup the clock for a decent framerate
107 clock = pygame.time.Clock()

Второй вызывает.tick(), чтобы сообщитьpygame, что программа достигла конца кадра:

188 # Flip everything to the display
189 pygame.display.flip()
190
191 # Ensure program maintains a rate of 30 frames per second
192 clock.tick(30)

Аргумент, переданный.tick(), устанавливает желаемую частоту кадров. Для этого.tick() вычисляет количество миллисекунд, которое должен занять каждый кадр, на основе желаемой частоты кадров. Затем он сравнивает это число с количеством миллисекунд, прошедших с момента последнего вызова.tick(). Если прошло недостаточно времени,.tick() задерживает обработку, чтобы гарантировать, что она никогда не превысит указанную частоту кадров.

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

Setting the frame rate in pygame

Поиграйте с этим номером, чтобы увидеть, что вам больше нравится!

Звуковые эффекты

Пока что вы сосредоточены на игровом процессе и визуальных аспектах вашей игры. Теперь давайте рассмотрим, как придать вашей игре слуховой вкус. pygame предоставляетmixer для обработки всех действий, связанных со звуком. Вы будете использовать классы и методы этого модуля для обеспечения фоновой музыки и звуковых эффектов для различных действий.

Названиеmixer относится к тому факту, что модуль смешивает различные звуки в единое целое. Используя подмодульmusic, вы можете передавать отдельные звуковые файлы в различных форматах, таких какMP3,Ogg иMod. Вы также можете использоватьSound для удержания одного звукового эффекта для воспроизведения в форматах Ogg илиuncompressed WAV. Все воспроизведение происходит в фоновом режиме, поэтому, когда вы проигрываетеSound, метод немедленно возвращается по мере воспроизведения звука.

Note:pygame documentation указывает, что поддержка MP3 ограничена, а неподдерживаемые форматы могут вызвать сбои системы. Звуки, упомянутые в этой статье, были протестированы, и мы рекомендуем тщательно протестировать любые звуки перед выпуском вашей игры.

Как и в большинстве случаевpygame, использованиеmixer начинается с этапа инициализации. К счастью, этим уже занимаетсяpygame.init(). Вам нужно только вызватьpygame.mixer.init(), если вы хотите изменить значения по умолчанию:

106 # Setup for sounds. Defaults are good.
107 pygame.mixer.init()
108
109 # Initialize pygame
110 pygame.init()
111
112 # Set up the clock for a decent framerate
113 clock = pygame.time.Clock()

pygame.mixer.init() принимаетa number of arguments, но в большинстве случаев значения по умолчанию работают нормально. Обратите внимание: если вы хотите изменить значения по умолчанию, вам нужно вызватьpygame.mixer.init() перед вызовомpygame.init(). В противном случае значения по умолчанию будут действовать независимо от ваших изменений.

После инициализации системы вы можете настроить свои звуки и фоновую музыку:

135 # Load and play background music
136 # Sound source: http://ccmixter.org/files/Apoxode/59262
137 # License: https://creativecommons.org/licenses/by/3.0/
138 pygame.mixer.music.load("Apoxode_-_Electric_1.mp3")
139 pygame.mixer.music.play(loops=-1)
140
141 # Load all sound files
142 # Sound sources: Jon Fincher
143 move_up_sound = pygame.mixer.Sound("Rising_putter.ogg")
144 move_down_sound = pygame.mixer.Sound("Falling_putter.ogg")
145 collision_sound = pygame.mixer.Sound("Collision.ogg")

Lines 138 and 139 загружает фоновый звуковой клип и начинает его воспроизведение. Вы можете указать звуковому клипу зацикливаться и никогда не заканчиваться, задав именованный параметрloops=-1.

Lines 143 to 145 загружает три звука, которые вы будете использовать для различных звуковых эффектов. Первые два — это повышающиеся и понижающиеся звуки, которые воспроизводятся, когда игрок движется вверх или вниз. Последний звук используется всякий раз, когда происходит столкновение. Вы также можете добавить другие звуки, такие как звук, когда создаетсяEnemy, или последний звук, когда игра заканчивается.

Итак, как вы используете звуковые эффекты? Вы хотите воспроизвести каждый звук, когда происходит определенное событие. Например, когда корабль движется вверх, вы хотите сыгратьmove_up_sound. Следовательно, вы добавляете вызов.play() всякий раз, когда обрабатываете это событие. В дизайне это означает добавление следующих вызовов к.update() дляPlayer:

26 # Define the Player object by extending pygame.sprite.Sprite
27 # Instead of a surface, use an image for a better-looking sprite
28 class Player(pygame.sprite.Sprite):
29     def __init__(self):
30         super(Player, self).__init__()
31         self.surf = pygame.image.load("jet.png").convert()
32         self.surf.set_colorkey((255, 255, 255), RLEACCEL)
33         self.rect = self.surf.get_rect()
34
35     # Move the sprite based on keypresses
36     def update(self, pressed_keys):
37         if pressed_keys[K_UP]:
38             self.rect.move_ip(0, -5)
39             move_up_sound.play()
40         if pressed_keys[K_DOWN]:
41             self.rect.move_ip(0, 5)
42             move_down_sound.play()

Для столкновения между игроком и врагом вы воспроизводите звук, когда обнаруживаются столкновения:

201 # Check if any enemies have collided with the player
202 if pygame.sprite.spritecollideany(player, enemies):
203     # If so, then remove the player
204     player.kill()
205
206     # Stop any moving sounds and play the collision sound
207     move_up_sound.stop()
208     move_down_sound.stop()
209     collision_sound.play()
210
211     # Stop the loop
212     running = False

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

Наконец, когда игра окончена, все звуки должны прекратиться. Это верно независимо от того, заканчивается ли игра из-за столкновения или пользователь выходит вручную. Для этого добавьте следующие строки в конце программы после цикла:

220 # All done! Stop and quit the mixer.
221 pygame.mixer.music.stop()
222 pygame.mixer.quit()

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

Это оно! Проверьте это снова, и вы должны увидеть что-то вроде этого:

Pygame window

Примечание к источникам

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

Вот некоторые источники музыки, звука и искусства, которые вы можете найти для полезного контента:

  • OpenGameArt.org: звуки, звуковые эффекты, спрайты и другие изображения

  • Kenney.nl: звуки, звуковые эффекты, спрайты и другие изображения

  • Gamer Art 2D: спрайты и другие изображения

  • CC Mixter: звуки и звуковые эффекты

  • Freesound: звуки и звуковые эффекты

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

Заключение

Из этого руководства вы узнали, чем программирование игр с помощьюpygame отличается от стандартного процедурного программирования. Вы также узнали, как:

  • Реализация циклов событий

  • Рисовать предметы на экране

  • Воспроизведение звуковых эффектов и музыки

  • Обрабатывать пользовательский ввод

Для этого вы использовали подмножество модулейpygame, включаяdisplay,mixer иmusic,time,image,event и модулиkey. Вы также использовали несколько классовpygame, включаяRect,Surface,Sound иSprite. Но это лишь малая часть того, на что способенpygame! Посмотритеofficial pygame documentation для получения полного списка доступных модулей и классов.

Вы можете найти весь код, графику и звуковые файлы для этой статьи, нажав на ссылку ниже:

Не стесняйтесь оставлять комментарии ниже. Счастливого Pythoning!

В этом руководстве мы подробно познакомимся с библиотекой PyGame и посмотрим, как с ее помощью можно создавать простые игры.

Содержание

  • Основные аспекты перед началом
  • Установка PyGame
  • Простое PyGame-приложение
  • Интерактивность
  • Добавление функциональности
  • Добавление изображений
  • Работа со звуком
  • Геометрические рисунки
  • Шрифты и текст
  • Модели ввода
  • Логика сцены
  • Заключение

Основные аспекты перед началом

Для создания игры необходимо ответить на 3 основных вопроса:

  • Какую игру вы хотите создать?
  • На каком языке вы хотите программировать?
  • Для какой платформы вы хотите выпустить свою игру?

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

PyGame вам подойдет, если на приведенные выше вопросы вы дали следующие ответы:

  • Игра будет графической, но не 3D
  • Вы хотите программировать на языке Python, который уже немного знаете
  • Вы хотите создать клиентское приложение, которое потенциально может быть обернуто в отдельный исполняемый файл

Для других сценариев, особенно для 3D-игр, лучше подойдут другие языки и фреймворки.

Итак, давайте рассмотрим, как установить PyGame.

Хотите скачать книги по Python в 2 клика? Тогда вам в наш телеграм канал PythonBooks 

Установить PyGame совсем не сложно. Но первым необходимым условием является установка Python.

Установка Python как на Windows, так и на Linux очень проста и понятна. Скачайте и установите его из официального сайта. Также вы можете установить Python через консоль, используя brew, apt, snap или любой другой менеджер пакетов, доступный в вашей ОС.

Далее необходимо загрузить PyGame. Инструкция о том, как это сделать, есть на официальном сайте библиотеки.

Установка очень проста. Просто следуйте инструкциям, а настройки по умолчанию считаются оптимальными.

Простое PyGame-приложение

Теперь можем приступить к созданию нашей первой игры при помощи PyGame.

Ниже представлено очень простое приложение, созданное с использованием конвейера PyGame. Ознакомьтесь с ним:

import pygame
 
pygame.init()
screen = pygame.display.set_mode((400, 300))
done = False
 
while not done:
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                        done = True
        
        pygame.display.flip()

Давайте разберемся с синтаксисом.

  • import pygame. Это, конечно же, необходимо для доступа к фреймворку PyGame.
  • pygame.init(). Этот вызов инициализирует все модули, необходимые для работы с PyGame.
  • pygame.display.set_mode((width, height)). Это создает окно заданного размера. Возвращаемое значение — это объект Surface, над котором вы будете выполнять графические операции.
  • pygame.event.get(). Этот вызов очищает очередь событий. Если вы не вызовете его, сообщения от операционной системы начнут накапливаться, и ваша игра станет неотзывчивой.
  • pygame.QUIT. Это тип события, который генерируется при нажатии на кнопку закрытия в углу окна.
  • pygame.display.flip(). PyGame использует двойной буфер, а этот вызов обменивает буферы. Вам просто нужно знать, что этот вызов необходим, чтобы любые обновления, которые вы вносите в коде, стали видимыми.

Итак, что же получается на выходе, когда мы выполним приведенный выше код? Результат будет примерно таким:

Выглядит довольно просто, не так ли? Давайте начнем добавлять содержимое на наш экран. Для начала мы можем нарисовать прямоугольник. Это очень просто, и для этого мы используем pygame.draw.rect.

# Add this somewhere after the event pumping and before the display.flip() 
pygame.draw.rect(screen, (0, 128, 255), pygame.Rect(30, 30, 60, 60))

Эта функция принимает три аргумента:

  1. Экземпляр поверхности, на которой нужно нарисовать прямоугольник.
  2. Кортеж (red, green, blue), представляющий цвет для рисования.
  3. Экземпляр pygame.Rect. Аргументами этого конструктора являются координаты x и y левого верхнего угла, ширина и высота.

Что же мы можем увидеть после добавления этого маленького кусочка кода? Добавился прямоугольник:

Вроде бы пока ничего особенного. Но начинать с чего-то нужно, верно?

Далее мы рассмотрим, как сделать игру более интерактивной.

Интерактивность

Смысл игры заключается в том, чтобы быть интерактивной. Сейчас единственное, с чем вы можете взаимодействовать, — это кнопка закрытия. А это не очень интересная игра, верно?

Все события пользовательского ввода проходят через очередь событий. Просто добавьте в этот цикл for больше операторов if, чтобы добавить интерактивности.

Вставьте перед циклом следующую строку:

is_blue = True

Измените код прямоугольника так, чтобы он выбирал цвет условно:

if is_blue: 
    color = (0, 128, 255)
else: 
    color = (255, 100, 0)
pygame.draw.rect(screen, color, pygame.Rect(30, 30, 60, 60))

И наконец, самое важное. Добавьте следующий оператор if в цикл for в той же последовательности, что и остальные операторы if.

if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
    is_blue = not is_blue

Нажатие клавиши пробела изменит цвет квадрата. Вывод:

Довольно просто, не так ли? Далее в этом материале мы рассмотрим, как можно добавить в игру некоторую функциональность.

Добавление функциональности

Добавим возможность двигать наш квадрат. Теперь весь наш код выглядит примерно так:

import pygame
 
pygame.init()
screen = pygame.display.set_mode((400, 300))
done = False
is_blue = True
x = 30
y = 30
 
while not done:
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                        done = True
                if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                        is_blue = not is_blue
        
        pressed = pygame.key.get_pressed()
        if pressed[pygame.K_UP]: y -= 3
        if pressed[pygame.K_DOWN]: y += 3
        if pressed[pygame.K_LEFT]: x -= 3
        if pressed[pygame.K_RIGHT]: x += 3
        
        if is_blue: color = (0, 128, 255)
        else: color = (255, 100, 0)
        pygame.draw.rect(screen, color, pygame.Rect(x, y, 60, 60))
        
        pygame.display.flip()

Проверим вывод при попытке сдвинуть прямоугольник вправо:

Это немного не то, чего мы ожидали, верно?

Две вещи являются неправильными:

  • Каждый раз при отрисовке прямоугольника на экране остается прямоугольник из предыдущих кадров.
  • Он движется очень быстро.

В первом случае перед рисованием прямоугольника нужно просто сбросить экран на черный. Для этого в Surface существует простой метод fill. Он принимает кортеж RGB:

screen.fill((0, 0, 0))

Что касается второй проблемы, длительность каждого кадра настолько мала, насколько это может сделать ваш супермощный компьютер. Частота кадров должна быть приведена до разумного числа, например 60 кадров в секунду.

К счастью, в pygame.time есть простой класс Clock, который делает это за нас. В нем есть метод tick, принимающий желаемую частоту кадров в секунду.

clock = pygame.time.Clock()
 
...
while not done:
 
    ...
 
    # will block execution until 1/60 seconds have passed 
    # since the previous time clock.tick was called. 
    clock.tick(60)

Сложите все это вместе и получите:

import pygame
 
pygame.init()
screen = pygame.display.set_mode((400, 300))
done = False
is_blue = True
x = 30
y = 30
 
clock = pygame.time.Clock()
 
while not done:
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                        done = True
                if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                        is_blue = not is_blue
        
        pressed = pygame.key.get_pressed()
        if pressed[pygame.K_UP]: y -= 3
        if pressed[pygame.K_DOWN]: y += 3
        if pressed[pygame.K_LEFT]: x -= 3
        if pressed[pygame.K_RIGHT]: x += 3
        
        screen.fill((0, 0, 0))
        if is_blue: color = (0, 128, 255)
        else: color = (255, 100, 0)
        pygame.draw.rect(screen, color, pygame.Rect(x, y, 60, 60))
        
        pygame.display.flip()
        clock.tick(60)

Как же теперь работает наш код? Посмотрите:

Далее мы рассмотрим, как можно работать с изображениями и как их можно интегрировать в нашу игру.

Добавление изображений

Вы можете создать пустую поверхность, просто вызвав конструктор Surface с кортежем ширины и высоты.

surface = pygame.Surface((100, 100))

В результате будет создано пустое 24-битное RGB-изображение размером 100 x 100 пикселей. По умолчанию оно будет иметь черный цвет. При наложении такого изображения на белый фон получится следующее:

Однако если требуется получить 32-битное RGBA-изображение, то можно включить необязательный аргумент в конструктор Surface. Для этого достаточно добавить в код следующую строку:

surface = pygame.Surface((100, 100), pygame.SRCALPHA)

В результате будет создано изображение размером 100 x 100, инициализированное как прозрачное. При рендеринге такого изображения на белом фоне получится следующее:

Одноцветные изображения и прямоугольники не очень интересны. Давайте воспользуемся файлом изображения.

Возьмем PNG-изображение шара. Его имя — ball.png. Вот это изображение, посмотрите и скачайте:

Для использования изображения из файла существует простой вызов pygame.image.load().

Обратите внимание на следующий синтаксис:

image = pygame.image.load('ball.png')

Заменив код pygame.Surface((100, 100)) на приведенный выше код, мы получим на выходе вот такой результат:

Не используйте pygame.image.load многократно для одного и того же изображения в цикле игры. Это неэффективный способ кодирования. Лучше инициализировать его единожды и использовать в дальнейшем любое количество раз.

Лучше всего создать словарь string-to-surface в одном централизованном месте. А затем написать функцию get_image, которая принимает путь к файлу.

Если изображение уже загружено, то возвращается инициализированное изображение. Если это не происходит, то выполняется инициализация.

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

Но фрагмент кода стоит тысячи слов! Вот он:

import pygame
import os
 
_image_library = {}
def get_image(path):
        global _image_library
        image = _image_library.get(path)
        if image == None:
                canonicalized_path = path.replace('/', os.sep).replace('', os.sep)
                image = pygame.image.load(canonicalized_path)
                _image_library[path] = image
        return image
 
pygame.init()
screen = pygame.display.set_mode((400, 300))
done = False
clock = pygame.time.Clock()
 
while not done:
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                        done = True
        
        screen.fill((255, 255, 255))
        
        screen.blit(get_image('ball.png'), (20, 20))
        
        pygame.display.flip()
        clock.tick(60)

Примечание

Windows не чувствительна к регистру имен файлов. Все остальные основные операционные системы чувствительны к регистру. Если ваш файл называется ball.png и вы используете pygame.image.load('BALL.PNG'), то игра будет работать, если вы работаете под Windows. Однако, если вы дадите свою игру кому-то, работающему на mac или Linux, она не будет работать и может выдать ошибочный результат.

Далее мы рассмотрим, как можно внедрить в игру музыку и звуковые эффекты.

Работа со звуком

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

pygame.mixer.music.load('foo.mp3')
pygame.mixer.music.play(0)

Воспроизведение песни бесконечно:

pygame.mixer.music.load('foo.mp3')
pygame.mixer.music.play(-1)

Передаваемое число — это количество повторов песни. 0 соответствует однократному воспроизведению.

Вызов функции play без числа аналогичен вызову с числом 0.

pygame.mixer.music.play() # play once

Постановка песни в очередь:

pygame.mixer.music.queue('next_song.mp3')

Остановка песни:

pygame.mixer.music.stop()

Функция stop также обнуляет все записи в очереди.

Если, например, требуется случайное воспроизведение из списка 5 песен, то можно создать список песен в виде глобальной очереди:

_songs = ['song_1.mp3', 'song_2.mp3', 'song_3.mp3', 'song_4.mp3', 'song_5.mp3']

Добавить флаг, указывающий, какая песня воспроизводится в данный момент:

_currently_playing_song = None

И написать функцию, выбирающую случайным образом другую песню, которая будет вызываться каждый раз при наступлении события SONG_END:

import random
 
def play_a_different_song():
    global _currently_playing_song, _songs
    next_song = random.choice(_songs)
    while next_song == _currently_playing_song:
        next_song = random.choice(_songs)
    _currently_playing_song = next_song
    pygame.mixer.music.load(next_song)
    pygame.mixer.music.play()

А если вы хотите, чтобы они каждый раз воспроизводились в одной и той же последовательности, то код будет таким:

def play_next_song():
    global _songs
    _songs = _songs[1:] + [_songs[0]] # move current song to the back of the list 
    pygame.mixer.music.load(_songs[0])
    pygame.mixer.music.play()

Музыкальный API очень централизован. Однако звуки требуют создания звуковых объектов, которые необходимо удерживать. Подобно изображениям, звуки имеют простой метод .play(), который запускает воспроизведение звука.

effect = pygame.mixer.Sound('beep.wav')
effect.play()

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

_sound_library = {}
def play_sound(path):
  global _sound_library
  sound = _sound_library.get(path)
  if sound == None:
    canonicalized_path = path.replace('/', os.sep).replace('', os.sep)
    sound = pygame.mixer.Sound(canonicalized_path)
    _sound_library[path] = sound
  sound.play()

Существует множество других возможностей, но это все, что вам нужно для выполнения 95% того, что требуется от большинства игр.

Далее мы рассмотрим, как можно реализовать в игре геометрические фигуры.

Геометрические рисунки

Как и в модуле mixer, API рисования достаточно прост и имеет несколько параметров.

Создание прямоугольника

pygame.draw.rect(surface, color, pygame.Rect(left, top, width, height))

Создание круга

pygame.draw.circle(surface, color, (x, y), radius)

Встроенные контурные изображения очень плохи!

Это первое предостережение, о котором следует знать. Метод PyGame для создания «более толстых» контуров окружностей заключается в рисовании нескольких 1-пиксельных контуров. Теоретически это звучит неплохо, пока вы не увидите результат:

Круг имеет заметные разрывы в пикселях. Еще более неудобным является прямоугольник, в котором используется 4 вызова рисования линий нужной толщины. Это создает странные углы.

Для большинства вызовов API рисования можно использовать необязательный последний параметр — толщину.

# draw a rectangle 
pygame.draw.rect(surface, color, pygame.Rect(10, 10, 100, 100), 10)
# draw a circle 
pygame.draw.circle(surface, color, (300, 60), 50, 10)

Примечание: Когда вы рисуете многоугольник, прямоугольник, круг и т.д., рисуйте его залитым или с толщиной в 1 пиксель. Все остальное реализовано не очень хорошо.

Если вам необходимо нарисовать прямоугольник с границами толщиной 10 пикселей, то лучше всего реализовать логику самостоятельно, используя либо 10 вызовов прямоугольника толщиной 1 пиксель, либо 4 вызова прямоугольника толщиной 10 пикселей для каждой стороны.

Создание многоугольника

Этот API довольно прост. Список точек представляет собой список кортежей координат x-y для многоугольника.

pygame.draw.polygon(surface, color, point_list)

Создание линии

pygame.draw.line(surface, color, (startX, startY), (endX, endY), width)

Посмотрите на этот удивительный вращающийся проволочный каркас куба, созданный с помощью метода line и большого количества математических вычислений:

import pygame
import math
import time
 
# Ignore these 3 functions. Scroll down for the relevant code. 
 
def create_background(width, height):
        colors = [(255, 255, 255), (212, 212, 212)]
        background = pygame.Surface((width, height))
        tile_width = 20
        y = 0
        while y < height:
                x = 0
                while x < width:
                        row = y // tile_width
                        col = x // tile_width
                        pygame.draw.rect(
                                background, 
                                colors[(row + col) % 2],
                                pygame.Rect(x, y, tile_width, tile_width))
                        x += tile_width
                y += tile_width
        return background
 
def is_trying_to_quit(event):
        pressed_keys = pygame.key.get_pressed()
        alt_pressed = pressed_keys[pygame.K_LALT] or pressed_keys[pygame.K_RALT]
        x_button = event.type == pygame.QUIT
        altF4 = alt_pressed and event.type == pygame.KEYDOWN and event.key == pygame.K_F4
        escape = event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
        return x_button or altF4 or escape
 
def run_demos(width, height, fps):
        pygame.init()
        screen = pygame.display.set_mode((width, height))
        pygame.display.set_caption('press space to see next demo')
        background = create_background(width, height)
        clock = pygame.time.Clock()
        demos = [
                do_rectangle_demo,
                do_circle_demo,
                do_horrible_outlines,
                do_nice_outlines,
                do_polygon_demo,
                do_line_demo
                ]
        the_world_is_a_happy_place = 0
        while True:
                the_world_is_a_happy_place += 1
                for event in pygame.event.get():
                        if is_trying_to_quit(event):
                                return
                        if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                                demos = demos[1:]
                screen.blit(background, (0, 0))
                if len(demos) == 0:
                        return
                demos[0](screen, the_world_is_a_happy_place)
                pygame.display.flip()
                clock.tick(fps)
 
# Everything above this line is irrelevant to this tutorial. 
 
def do_rectangle_demo(surface, counter):
        left = (counter // 2) % surface.get_width()
        top = (counter // 3) % surface.get_height()
        width = 30
        height = 30
        color = (128, 0, 128) # purple 
        
        # Draw a rectangle 
        pygame.draw.rect(surface, color, pygame.Rect(left, top, width, height))
 
def do_circle_demo(surface, counter):
        x = surface.get_width() // 2
        y = surface.get_height() // 2
        max_radius = min(x, y) * 4 // 5
        radius = abs(int(math.sin(counter * 3.14159 * 2 / 200) * max_radius)) + 1
        color = (0, 140, 255) # aquamarine 
        
        # Draw a circle 
        pygame.draw.circle(surface, color, (x, y), radius)
 
def do_horrible_outlines(surface, counter):
        color = (255, 0, 0) # red 
        
        # draw a rectangle 
        pygame.draw.rect(surface, color, pygame.Rect(10, 10, 100, 100), 10)
 
        # draw a circle 
        pygame.draw.circle(surface, color, (300, 60), 50, 10)
        
def do_nice_outlines(surface, counter):
        color = (0, 128, 0) # green 
        
        # draw a rectangle 
        pygame.draw.rect(surface, color, pygame.Rect(10, 10, 100, 10))
        pygame.draw.rect(surface, color, pygame.Rect(10, 10, 10, 100))
        pygame.draw.rect(surface, color, pygame.Rect(100, 10, 10, 100))
        pygame.draw.rect(surface, color, pygame.Rect(10, 100, 100, 10))
        
        # draw a circle 
        center_x = 300
        center_y = 60
        radius = 45
        iterations = 150
        for i in range(iterations):
                ang = i * 3.14159 * 2 / iterations
                dx = int(math.cos(ang) * radius)
                dy = int(math.sin(ang) * radius)
                x = center_x + dx
                y = center_y + dy
                pygame.draw.circle(surface, color, (x, y), 5)
 
 
def do_polygon_demo(surface, counter):
        color = (255, 255, 0) # yellow 
        
        num_points = 8
        point_list = []
        center_x = surface.get_width() // 2
        center_y = surface.get_height() // 2
        for i in range(num_points * 2):
                radius = 100
                if i % 2 == 0:
                        radius = radius // 2
                ang = i * 3.14159 / num_points + counter * 3.14159 / 60
                x = center_x + int(math.cos(ang) * radius)
                y = center_y + int(math.sin(ang) * radius)
                point_list.append((x, y))
        pygame.draw.polygon(surface, color, point_list)
 
def rotate_3d_points(points, angle_x, angle_y, angle_z):
        new_points = []
        for point in points:
                x = point[0]
                y = point[1]
                z = point[2]
                new_y = y * math.cos(angle_x) - z * math.sin(angle_x)
                new_z = y * math.sin(angle_x) + z * math.cos(angle_x)
                y = new_y
                # isn't math fun, kids? 
                z = new_z
                new_x = x * math.cos(angle_y) - z * math.sin(angle_y)
                new_z = x * math.sin(angle_y) + z * math.cos(angle_y)
                x = new_x
                z = new_z
                new_x = x * math.cos(angle_z) - y * math.sin(angle_z)
                new_y = x * math.sin(angle_z) + y * math.cos(angle_z)
                x = new_x
                y = new_y
                new_points.append([x, y, z])
        return new_points
 
def do_line_demo(surface, counter):
        color = (0, 0, 0) # black 
        cube_points = [
                [-1, -1, 1],
                [-1, 1, 1],
                [1, 1, 1],
                [1, -1, 1],
                [-1, -1, -1],
                [-1, 1, -1],
                [1, 1, -1],
                [1, -1, -1]]
                
        connections = [
                (0, 1),
                (1, 2),
                (2, 3),
                (3, 0),
                (4, 5),
                (5, 6),
                (6, 7),
                (7, 4),
                (0, 4),
                (1, 5),
                (2, 6),
                (3, 7)
                ]
                
        t = counter * 2 * 3.14159 / 60 # this angle is 1 rotation per second 
        
        # rotate about x axis every 2 seconds 
        # rotate about y axis every 4 seconds 
        # rotate about z axis every 6 seconds 
        points = rotate_3d_points(cube_points, t / 2, t / 4, t / 6)
        flattened_points = []
        for point in points:
                flattened_points.append(
                        (point[0] * (1 + 1.0 / (point[2] + 3)),
                         point[1] * (1 + 1.0 / (point[2] + 3))))
        
        for con in connections:
                p1 = flattened_points[con[0]]
                p2 = flattened_points[con[1]]
                x1 = p1[0] * 60 + 200
                y1 = p1[1] * 60 + 150
                x2 = p2[0] * 60 + 200
                y2 = p2[1] * 60 + 150
                
                # This is the only line that really matters 
                pygame.draw.line(surface, color, (x1, y1), (x2, y2), 4)
                
        
run_demos(400, 300, 60)

Далее мы рассмотрим, как работать со шрифтами и текстом.

Шрифты и текст

Если вы ищете быстрый ответ на вопрос о том, как отобразить текст, то вот он:

import pygame
 
pygame.init()
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()
done = False
 
font = pygame.font.SysFont("comicsansms", 72)
 
text = font.render("Hello, World", True, (0, 128, 0))
 
while not done:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            done = True
    
    screen.fill((255, 255, 255))
    screen.blit(text,
        (320 - text.get_width() // 2, 240 - text.get_height() // 2))
    
    pygame.display.flip()
    clock.tick(60)

Но, конечно, есть несколько неидеальных моментов.

Правило 1: никогда не следует предполагать, что на компьютере пользователя установлен определенный шрифт. Даже в CSS есть возможность определить иерархию используемых шрифтов. Если лучший вариант шрифта недоступен, используется альтернативный. Вы должны следовать той же схеме.

К счастью, в PyGame есть возможность перечислить все доступные на машине шрифты:

all_fonts = pygame.font.get_fonts()

Кроме того, существует способ инстанцирования системного шрифта по умолчанию:

font = pygame.font.Font(None, size)

Также вместо None можно передать имя файла шрифта, который вы включаете в код, чтобы гарантировать существование идеального шрифта:

font = pygame.font.Font("myresources/fonts/Papyrus.ttf", 26)

Используя любую комбинацию из перечисленных выше, можно написать более совершенную функцию создания шрифта.

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

def make_font(fonts, size):
    available = pygame.font.get_fonts()
    # get_fonts() returns a list of lowercase spaceless font names 
    choices = map(lambda x:x.lower().replace(' ', ''), fonts)
    for choice in choices:
        if choice in available:
            return pygame.font.SysFont(choice, size)
    return pygame.font.Font(None, size)

Можно еще более усовершенствовать его, кэшируя экземпляр шрифта по его имени и размеру.

_cached_fonts = {}
def get_font(font_preferences, size):
    global _cached_fonts
    key = str(font_preferences) + '|' + str(size)
    font = _cached_fonts.get(key, None)
    if font == None:
        font = make_font(font_preferences, size)
        _cached_fonts[key] = font
    return font

Можно пойти еще дальше и фактически кэшировать сам отрисованный текст. Хранить изображение дешевле, чем создавать новое, особенно если вы планируете, что один и тот же текст будет отображаться более чем в одном кадре подряд. Да! Если вы хотите, чтобы текст был читаемым, то это ваш план.

_cached_text = {}
def create_text(text, fonts, size, color):
    global _cached_text
    key = '|'.join(map(str, (fonts, size, color, text)))
    image = _cached_text.get(key, None)
    if image == None:
        font = get_font(fonts, size)
        image = font.render(text, True, color)
        _cached_text[key] = image
    return image

Если собрать все это вместе, то получится "Hello, World", но с улучшенным кодом:

import pygame
 
def make_font(fonts, size):
    available = pygame.font.get_fonts()
    # get_fonts() returns a list of lowercase spaceless font names 
    choices = map(lambda x:x.lower().replace(' ', ''), fonts)
    for choice in choices:
        if choice in available:
            return pygame.font.SysFont(choice, size)
    return pygame.font.Font(None, size)
    
_cached_fonts = {}
def get_font(font_preferences, size):
    global _cached_fonts
    key = str(font_preferences) + '|' + str(size)
    font = _cached_fonts.get(key, None)
    if font == None:
        font = make_font(font_preferences, size)
        _cached_fonts[key] = font
    return font
 
_cached_text = {}
def create_text(text, fonts, size, color):
    global _cached_text
    key = '|'.join(map(str, (fonts, size, color, text)))
    image = _cached_text.get(key, None)
    if image == None:
        font = get_font(fonts, size)
        image = font.render(text, True, color)
        _cached_text[key] = image
    return image
 
pygame.init()
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()
done = False
 
font_preferences = [
        "Bizarre-Ass Font Sans Serif",
        "They definitely dont have this installed Gothic",
        "Papyrus",
        "Comic Sans MS"]
 
text = create_text("Hello, World", font_preferences, 72, (0, 128, 0))
 
while not done:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            done = True
    
    screen.fill((255, 255, 255))
    screen.blit(text,
        (320 - text.get_width() // 2, 240 - text.get_height() // 2))
    
    pygame.display.flip()
    clock.tick(60)

Далее мы рассмотрим, как можно учитывать входные данные.

Модели входов

Существует два основных способа получения информации о состоянии любого устройства ввода. Это проверка очереди событий и опрос.

Каждый раз, когда нажимается или отпускается клавиша или кнопка, или перемещается мышь, событие добавляется в очередь событий. Вы должны очищать эту очередь событий каждый кадр, вызывая pygame.event.get() или pygame.event.pump().

pygame.event.get() вернет список всех событий, произошедших с момента последнего опустошения очереди. Способ обработки этих событий зависит от их типа. Тип события можно проверить, прочитав поле event.type.

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

Другой способ проверки событий — опрос состояния клавиш или кнопок.

pygame.key.get_pressed() получает список булевых значений, описывающих состояние каждой клавиши клавиатуры.

pygame.mouse.get_pos() возвращает координаты курсора мыши. Если мышь еще не перемещалась по экрану, возвращается значение (0, 0).

pygame.mouse.get_pressed() возвращает состояние каждой кнопки мыши (как и pygame.key.get_pressed()). Возвращаемое значение представляет собой кортеж размера 3, соответствующий левой, средней и правой кнопкам.

Вот небольшая программа, в которой есть немного всего:

  • При перемещении мыши за ней рисуется след.
  • Нажатие клавиши W при удержании Ctrl приводит к закрытию окна. То же самое для Alt + F4.
  • Нажатие кнопки ESC приводит к закрытию окна
  • При нажатии клавиш r, g или b след становится красным, зеленым и синим соответственно.
  • При нажатии левой кнопки мыши след становится толще.
  • При нажатии правой кнопки мыши след становится тоньше.
import pygame
 
def main():
    pygame.init()
    screen = pygame.display.set_mode((640, 480))
    clock = pygame.time.Clock()
    
    radius = 15
    x = 0
    y = 0
    mode = 'blue'
    points = []
    
    while True:
        
        pressed = pygame.key.get_pressed()
        
        alt_held = pressed[pygame.K_LALT] or pressed[pygame.K_RALT]
        ctrl_held = pressed[pygame.K_LCTRL] or pressed[pygame.K_RCTRL]
        
        for event in pygame.event.get():
            
            # determin if X was clicked, or Ctrl+W or Alt+F4 was used
            if event.type == pygame.QUIT:
                return
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_w and ctrl_held:
                    return
                if event.key == pygame.K_F4 and alt_held:
                    return
                if event.key == pygame.K_ESCAPE:
                    return
            
                # determine if a letter key was pressed 
                if event.key == pygame.K_r:
                    mode = 'red'
                elif event.key == pygame.K_g:
                    mode = 'green'
                elif event.key == pygame.K_b:
                    mode = 'blue'
            
            if event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 1: # left click grows radius 
                    radius = min(200, radius + 1)
                elif event.button == 3: # right click shrinks radius
                    radius = max(1, radius - 1)
            
            if event.type == pygame.MOUSEMOTION:
                # if mouse moved, add point to list 
                position = event.pos
                points = points + [position]
                points = points[-256:]
                
        screen.fill((0, 0, 0))
        
        # draw all points 
        i = 0
        while i < len(points) - 1:
            drawLineBetween(screen, i, points[i], points[i + 1], radius, mode)
            i += 1
        
        pygame.display.flip()
        
        clock.tick(60)
 
def drawLineBetween(screen, index, start, end, width, color_mode):
    c1 = max(0, min(255, 2 * index - 256))
    c2 = max(0, min(255, 2 * index))
    
    if color_mode == 'blue':
        color = (c1, c1, c2)
    elif color_mode == 'red':
        color = (c2, c1, c1)
    elif color_mode == 'green':
        color = (c1, c2, c1)
    
    dx = start[0] - end[0]
    dy = start[1] - end[1]
    iterations = max(abs(dx), abs(dy))
    
    for i in range(iterations):
        progress = 1.0 * i / iterations
        aprogress = 1 - progress
        x = int(aprogress * start[0] + progress * end[0])
        y = int(aprogress * start[1] + progress * end[1])
        pygame.draw.circle(screen, color, (x, y), width)
 
main()

И, наконец, мы должны рассмотреть так называемую централизованную логику сцены.

Централизованная логика сцены

Этот раздел касается не столько PyGame, сколько применения концепций проектирования хорошего программного обеспечения. Эта модель работы хорошо зарекомендовала себя при создании многих сложных игр.

Вот определение класса SceneBase:

class SceneBase:
def __init__(self):
    self.next = self
 
    def ProcessInput(self, events):
        print("uh-oh, you didn't override this in the child class")
 
    def Update(self):
        print("uh-oh, you didn't override this in the child class")
 
    def Render(self, screen):
        print("uh-oh, you didn't override this in the child class")
 
    def SwitchToScene(self, next_scene):
        self.next = next_scene

При переопределении этого класса необходимо заполнить 3 реализации методов.

  • ProcessInput будет получать все события, произошедшие с момента последнего кадра.
  • Update. Поместите сюда свою игровую логику для сцены.
  • Render. Поместите сюда код рендеринга. В качестве входных данных он будет получать Surface главного экрана.

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

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

import pygame
 
def main():
    pygame.init()
    screen = pygame.display.set_mode((640, 480))
    clock = pygame.time.Clock()
    
    radius = 15
    x = 0
    y = 0
    mode = 'blue'
    points = []
    
    while True:
        
        pressed = pygame.key.get_pressed()
        
        alt_held = pressed[pygame.K_LALT] or pressed[pygame.K_RALT]
        ctrl_held = pressed[pygame.K_LCTRL] or pressed[pygame.K_RCTRL]
        
        for event in pygame.event.get():
            
            # determin if X was clicked, or Ctrl+W or Alt+F4 was used
            if event.type == pygame.QUIT:
                return
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_w and ctrl_held:
                    return
                if event.key == pygame.K_F4 and alt_held:
                    return
                if event.key == pygame.K_ESCAPE:
                    return
            
                # determine if a letter key was pressed 
                if event.key == pygame.K_r:
                    mode = 'red'
                elif event.key == pygame.K_g:
                    mode = 'green'
                elif event.key == pygame.K_b:
                    mode = 'blue'
            
            if event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 1: # left click grows radius 
                    radius = min(200, radius + 1)
                elif event.button == 3: # right click shrinks radius
                    radius = max(1, radius - 1)
            
            if event.type == pygame.MOUSEMOTION:
                # if mouse moved, add point to list 
                position = event.pos
                points = points + [position]
                points = points[-256:]
                
        screen.fill((0, 0, 0))
        
        # draw all points 
        i = 0
        while i < len(points) - 1:
            drawLineBetween(screen, i, points[i], points[i + 1], radius, mode)
            i += 1
        
        pygame.display.flip()
        
        clock.tick(60)
 
def drawLineBetween(screen, index, start, end, width, color_mode):
    c1 = max(0, min(255, 2 * index - 256))
    c2 = max(0, min(255, 2 * index))
    
    if color_mode == 'blue':
        color = (c1, c1, c2)
    elif color_mode == 'red':
        color = (c2, c1, c1)
    elif color_mode == 'green':
        color = (c1, c2, c1)
    
    dx = start[0] - end[0]
    dy = start[1] - end[1]
    iterations = max(abs(dx), abs(dy))
    
    for i in range(iterations):
        progress = 1.0 * i / iterations
        aprogress = 1 - progress
        x = int(aprogress * start[0] + progress * end[0])
        y = int(aprogress * start[1] + progress * end[1])
        pygame.draw.circle(screen, color, (x, y), width)
 
main()

Заключение

Я надеюсь, что этот материал по PyGame поможет вам начать использовать эту библиотеку.

Разработайте несколько игр с использованием данного руководства и поделитесь результатами в комментариях!

Перевод статьи «PyGame Tutorial».

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

К концу этой статьи вы сможете:

  • Рисовать предметы на экране
  • Воспроизводить звуковые эффекты и музыку
  • Обрабатывать пользовательский ввод
  • Реализовать циклы событий
  • Описать, чем программирование игр отличается от стандартного процедурного программирования на Python.

Предыстория и установка

pygame представляет собой оболочку Python для библиотеки SDL, что означает Simple DirectMedia Layer. SDL обеспечивает межплатформенный доступ к базовым мультимедийным аппаратным компонентам вашей системы, таким как звук, видео, мышь, клавиатура и джойстик. pygame начал жизнь как замена застопорившемуся проекту PySDL . Кроссплатформенный характер SDL pygame означает, что вы можете писать игры и многофункциональные мультимедийные программы Python для любой платформы, которая их поддерживает!

Для установки pygame на вашей платформе используйте соответствующую команду pip:

$ pip install pygame

Вы можете проверить установку, загрузив один из примеров, поставляемых с библиотекой:

$ python3 -m pygame.examples.aliens

Если появится окно с игрой, значит pygame установлен правильно! Если у вас возникнут проблемы, в руководстве по началу работы описаны некоторые известные проблемы и предостережения для всех платформ.

Базовая программа PyGame

Прежде чем перейти к специфике, давайте взглянем на основную программу pygame. Эта программа создает окно, заливает фон белым цветом и рисует синий круг посередине:

# Simple pygame program

# Import and initialize the pygame library
import pygame
pygame.init()

# Set up the drawing window
screen = pygame.display.set_mode([500, 500])

# Run until the user asks to quit
running = True
while running:

# Did the user click the window close button?
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Fill the background with white
screen.fill((255, 255, 255))

# Draw a solid blue circle in the center
pygame.draw.circle(screen, (0, 0, 255), (250, 250), 75)

# Flip the display
pygame.display.flip()

# Done! Time to quit.
pygame.quit()

Когда вы запустите эту программу, вы увидите окно, похожее на это:

Разберем этот код по частям:

  • Строки 4 и 5 импортируют и инициализируют библиотеку pygame. Без этих строк не будет pygame.
  • Строка 8 настраивает окно отображения вашей программы. Вы предоставляете либо список, либо кортеж, определяющий ширину и высоту создаваемого окна. Эта программа использует список для создания квадратного окна с 500 пикселями с каждой стороны.
  • Строки 11 и 12 настраивают игровой цикл для управления завершением программы. Позже в этом руководстве вы познакомитесь с игровыми циклами.
  • Строки с 15 по 17 сканируют и обрабатывают события внутри игрового цикла. Вы также доберетесь до событий немного позже. В этом случае обрабатывается только одно событие pygame.QUIT, которое происходит, когда пользователь нажимает кнопку закрытия окна.
  • Строка 20 заполняет окно сплошным цветом. screen.fill() принимает либо список, либо кортеж, определяющий значения RGB для цвета.
  • Строка 23 рисует круг в окне, используя следующие параметры: screen: окно, в котором будет происходить процесс(0, 0, 255): кортеж, содержащий значения цвета RGB.(250, 250): кортеж, определяющий координаты центра круга75: радиус круга для рисования в пикселях
  • Строка 26 обновляет содержимое дисплея на экране. Без этого вызова в окне ничего не появляется!
  • Строка 29 выходит из pygame. Это происходит только после завершения цикла.

Это была база pygame. Теперь давайте углубимся в концепции, лежащие в основе этого кода.

Концепции PyGame

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

Инициализация и модули

Библиотека pygame состоит из ряда конструкций Python, которые включают в себя несколько различных модулей . Эти модули обеспечивают абстрактный доступ к определенному оборудованию в вашей системе, а также унифицированные методы работы с этим оборудованием. Например, display обеспечивает единый доступ к вашему видеодисплею, а joystick позволяет абстрактно управлять вашим джойстиком.

После импорта библиотеки pygame из приведенного выше примера первое, что вы сделали, — это инициализировали PyGame с помощью pygame.init(). Эта функция вызывает отдельные init() функции всех включенных модулей pygame. Поскольку эти модули являются абстракциями для конкретного оборудования, этот шаг инициализации необходим, чтобы вы могли работать с одним и тем же кодом в Linux, Windows и Mac.

Дисплеи и поверхности

В дополнение к модулям pygame также включает несколько классов Python , которые инкапсулируют концепции, не зависящие от оборудования. Одним из них является Surface, который в своей основе определяет прямоугольную область, на которой вы можете рисовать. Surface объекты используются во многих контекстах в pygame. Позже вы увидите, как загрузить изображение в файл Surface и отобразить его на экране.

В pygame всё просматривается в одном созданном пользователем display, который может быть окном или полным экраном. Дисплей создается с помощью .set_mode(), который возвращает представление Surface видимой части окна. Surface — это то, что вы передаете в функции рисования, такие как pygame.draw.circle(), и содержимое этого Surface выталкивается на дисплей, когда вы вызываете pygame.display.flip().

Изображения и прямоугольники

Ваша базовая программа pygame рисовала фигуру прямо на экране Surface, но вы также можете работать с изображениями на диске. Модуль позволяет загружать и сохранять изображения image в различных популярных форматах. Изображения загружаются в объекты, которыми затем можно манипулировать и отображать различными способами.

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

Ладно, хватит теории. Давайте придумаем и напишем игру!

Базовый дизайн игры

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

  • Цель игры состоит в том, чтобы избегать приближающихся препятствий: Игрок начинает с левой стороны экрана.Препятствия входят случайным образом справа и движутся влево по прямой линии.
  • Игрок может двигаться влево, вправо, вверх или вниз, чтобы избежать препятствий.
  • Игрок не может уйти с экрана.
  • Игра заканчивается либо когда игрок сталкивается с препятствием, либо когда пользователь закрывает окно.

Один мой бывший коллега , описывая программные проекты, говорил: «Вы не знаете, что делаете, пока не узнаете, чего не делаете». Имея это в виду, вот некоторые вещи, которые не будут рассмотрены в этом руководстве:

  • Нет нескольких жизней
  • Нет ведения счета
  • Нет возможности атаки игрока
  • Нет продвинутых уровней
  • Нет боссов

Вы можете попробовать свои силы в добавлении этих и других функций в свою программу.

Давайте начнем!

Импорт и инициализация PyGame

После импорта pygame вам также потребуется инициализировать его. Это позволяет подключить абстракции pygame к вашему конкретному оборудованию:

# Import the pygame module
import pygame

# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
from pygame.locals import (
K_UP,
K_DOWN,
K_LEFT,
K_RIGHT,
K_ESCAPE,
KEYDOWN,
QUIT,
)

# Initialize pygame
pygame.init()

Библиотека pygame определяет множество вещей помимо модулей и классов. Она также определяет некоторые локальные константы для таких вещей, как нажатия клавиш, движения мыши и атрибуты отображения. Вы ссылаетесь на эти константы, используя синтаксис pygame.<CONSTANT>. Импортируя определенные константы из pygame.locals, вы можете вместо этого использовать синтаксис <CONSTANT>. Это сэкономит вам несколько нажатий клавиш и улучшит общую читаемость.

Настройка дисплея

Теперь вам нужно то, на чём вы будете рисовать! Создайте экран , который будет общим холстом:

# Import the pygame module
import pygame

# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
from pygame.locals import (
K_UP,
K_DOWN,
K_LEFT,
K_RIGHT,
K_ESCAPE,
KEYDOWN,
QUIT,
)

# Initialize pygame
pygame.init()

# Define constants for the screen width and height
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600

# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

Вы создаете экран для использования, вызывая pygame.display.set_mode() и передавая кортеж или список с желаемой шириной и высотой. В данном случае окно имеет размер 800×600, как определено константами SCREEN_WIDTH и SCREEN_HEIGHT в строках 20 и 21. Это часть окна, которой вы можете управлять, в то время как ОС управляет границами окна и строкой заголовка.

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

Настройка игрового цикла

Каждая игра от Pong до Fortnite использует игровой цикл для управления игровым процессом. Игровой цикл делает четыре очень важные вещи:

  • Обрабатывает пользовательский ввод
  • Обновляет состояние всех игровых объектов
  • Обновляет дисплей и аудиовыход
  • Поддерживает скорость игры

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

  • Игрок сталкивается с препятствием.
  • Игрок закрывает окно.

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

Обработка событий

Нажатия клавиш, движения мыши и даже движения джойстика — вот некоторые из способов, которыми пользователь может вводить данные. Все действия пользователя приводят к генерации события . События могут произойти в любое время и часто (но не всегда) возникают вне программы. Все события pygame помещаются в очередь событий, к которой затем можно получить доступ и которой можно манипулировать. Работа с событиями называется их обработкой, а код для этого называется обработчиком событий .

Каждое событие в pygame имеет связанный с ним тип события. В вашей игре типы событий, на которых вы сосредоточитесь, — это нажатия клавиш и закрытие окна. События нажатия клавиш имеют тип события KEYDOWN, а событие закрытия окна имеет тип QUIT. Различные типы событий также могут иметь другие связанные с ними данные. Например, у типа события KEYDOWN также есть переменная , вызываемая key для указания того, какая клавиша была нажата.

Вы получаете доступ к списку всех активных событий в очереди, вызывая pygame.event.get(). Затем вы просматриваете этот список, проверяете каждый тип события и отвечаете соответствующим образом:

# Variable to keep the main loop running
running = True

# Main loop
while running:
# Look at every event in the queue
for event in pygame.event.get():
# Did the user hit a key?
if event.type == KEYDOWN:
# Was it the Escape key? If so, stop the loop.
if event.key == K_ESCAPE:
running = False

# Did the user click the window close button? If so, stop the loop.
elif event.type == QUIT:
running = False

Давайте подробнее рассмотрим этот игровой цикл:

  • В строке 28 задается управляющая переменная для игрового цикла. Для выхода из цикла и игры вы устанавливаете running = False. Игровой цикл начинается в строке 29.
  • Строка 31 запускает обработчик событий, просматривая каждое событие, находящееся в данный момент в очереди. Если событий нет, то список пуст, и обработчик ничего не делает.
  • Строки с 35 по 38 проверяют, является ли текущее KEYDOWN событием. Если да, то программа проверяет, какая клавиша была нажата, по атрибуту event.key. Если ключом является Esc, обозначенный K_ESCAPE, то он выходит из игрового цикла, устанавливая running = False.
  • Строки 41 и 42 выполняют аналогичную проверку для типа события с именем QUIT. Это событие происходит только тогда, когда пользователь нажимает кнопку закрытия окна. Пользователь также может использовать любое другое действие операционной системы, чтобы закрыть окно.

Когда вы добавите эти строки к предыдущему коду и запустите его, вы увидите окно с пустым или черным экраном:

Окно не исчезнет, пока вы не нажмете клавишу Esc или иным образом не вызовете событие QUIT, закрыв окно.

Рисование на экране

В примере программы вы рисовали на экране с помощью двух команд:

  • screen.fill() — заполнить фон
  • pygame.draw.circle() — нарисовать круг

Теперь вы узнаете о третьем способе рисования на экране: использовании файла Surface.

Напомним, что Surface— это прямоугольный объект, на котором можно рисовать как, например, на чистом листе бумаги. Объект screen представляет собой Surface, и вы можете создавать свои собственные Surface — объекты отдельно от экрана дисплея. Давайте посмотрим, как это работает:

# Fill the screen with white
screen.fill((255, 255, 255))

# Create a surface and pass in a tuple containing its length and width
surf = pygame.Surface((50, 50))

# Give the surface a color to separate it from the background
surf.fill((0, 0, 0))
rect = surf.get_rect()

После того, как экран заполнен белым цветом в строке 45, в строке 48 создается новый Surface. Он имеет ширину 50 пикселей, высоту 50 пикселей и назначается surf. На этом этапе вы относитесь к нему так же, как к файлу screen. Итак, в строке 51 вы заполняете его черным цветом. Вы также можете получить доступ к его основе, используя .get_rect(). Это сохраняет rect для последующего использования.

Использование .blit() и .flip()

Просто создать новое Surface недостаточно, чтобы увидеть его на экране. Для этого вам нужно скопировать на Surface другой Surface. Термин blit означает Block Transfer, а .blit() означает, как вы копируете содержимое одного Surface в другое. Вот как происходит процесс рисования surf на экране:

# This line says «Draw surf onto the screen at the center»
screen.blit(surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
pygame.display.flip()

Вызов .blit() в строке 55 принимает два аргумента:

  • Рисование Surface_
  • Место, в котором нужно нарисовать его

Координаты (SCREEN_WIDTH/2, SCREEN_HEIGHT/2) говорят вашей программе разместить surf точно в центре экрана, но это не совсем так:

Причина, по которой изображение выглядит не по центру, заключается в том, что верхний левый угол не помещает .blit() в указанное место. Если вы хотите быть в центре, то вам придется сделать некоторые математические расчеты, чтобы сместить его вверх и влево. Вы можете сделать это, вычитая ширину и высоту из ширины и высоты экрана, разделив каждую на 2, чтобы найти центр, а затем передав эти числа в качестве аргументов :surfsurfsurfscreen.blit()

# Put the center of surf at the center of the display
surf_center = (
(SCREEN_WIDTH-surf.get_width())/2,
(SCREEN_HEIGHT-surf.get_height())/2
)

# Draw surf at the new coordinates
screen.blit(surf, surf_center)
pygame.display.flip()

Обратите внимание на вызов pygame.display.flip() после вызова blit(). Это обновляет весь экран с момента последнего перелистывания. Без вызова .flip() ничего не будет отображаться.

Спрайты

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

В терминах программирования спрайт — это двухмерное представление чего-либо на экране. По сути, это картинка. pygame предоставляет класс Sprite , предназначенный для хранения одного или нескольких графических представлений любого игрового объекта, который вы хотите отобразить на экране. Чтобы использовать его, вы создаёте новый класс, который расширяет Sprite. Это позволяет использовать его встроенные методы.

Игроки

Вот как вы используете объекты Sprite в текущей игре для определения игрока. Вставьте этот код после строки 18:

# Define a Player object by extending pygame.sprite.Sprite
# The surface drawn on the screen is now an attribute of ‘player’
class Player(pygame.sprite.Sprite):
def __init__(self):
super(Player, self).__init__()
self.surf = pygame.Surface((75, 25))
self.surf.fill((255, 255, 255))
self.rect = self.surf.get_rect()

Сначала вы определяете Player, расширяя строку 22 pygame.sprite.Sprite. Затем используете .__init__() .super()для вызова метода.

Далее вы определяете и инициализируете сохранение изображения .surf для отображения, которое в настоящее время является белым прямоугольником. Вы также определяете и инициализируете .rect, которые позже будете использовать для рисования игрока. Чтобы использовать этот новый класс, вам нужно создать новый объект и изменить код рисования. В блоке ниже представлено всё, что у нас есть на данном этапе:

# Import the pygame module
import pygame

# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
from pygame.locals import (
K_UP,
K_DOWN,
K_LEFT,
K_RIGHT,
K_ESCAPE,
KEYDOWN,
QUIT,
)

# Define constants for the screen width and height
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600

# Define a player object by extending pygame.sprite.Sprite
# The surface drawn on the screen is now an attribute of ‘player’
class Player(pygame.sprite.Sprite):
def __init__(self):
super(Player, self).__init__()
self.surf = pygame.Surface((75, 25))
self.surf.fill((255, 255, 255))
self.rect = self.surf.get_rect()

# Initialize pygame
pygame.init()

# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

# Instantiate player. Right now, this is just a rectangle.
player = Player()

# Variable to keep the main loop running
running = True

# Main loop
while running:
# for loop through the event queue
for event in pygame.event.get():
# Check for KEYDOWN event
if event.type == KEYDOWN:
# If the Esc key is pressed, then exit the main loop
if event.key == K_ESCAPE:
running = False
# Check for QUIT event. If QUIT, then set running to false.
elif event.type == QUIT:
running = False

# Fill the screen with black
screen.fill((0, 0, 0))

# Draw the player on the screen
screen.blit(player.surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))

# Update the display
pygame.display.flip()

Запустите этот код. Вы увидите белый прямоугольник примерно посередине экрана:

Как вы думаете, что произойдет, если вы замените строку 59 на screen.blit(player.surf, player.rect)? Попробуйте и посмотрите:

# Fill the screen with black
screen.fill((0, 0, 0))

# Draw the player on the screen
screen.blit(player.surf, player.rect)

# Update the display
pygame.display.flip()

Когда вы передаете Rectto .blit(), он использует координаты верхнего левого угла для рисования поверхности. Вы будете использовать это позже, чтобы заставить вашего игрока двигаться!

Пользовательский ввод

Итак, вы узнали, как создавать pygame и рисовать объекты на экране. Теперь начинается настоящее веселье! Вы сделаете плеер управляемым с помощью клавиатуры.

Ранее вы видели, что pygame.event.get() возвращает список событий в очереди, которую вы сканируете на наличие типов событий KEYDOWN. Ну, это не единственный способ читать нажатия клавиш. pygame также предоставляет pygame.event.get_pressed(), который возвращает словарь , содержащий все текущие события в очереди KEYDOWN.

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

# Get the set of keys pressed and check for user input
pressed_keys = pygame.key.get_pressed()

Затем вы пишете метод Player, чтобы принять этот словарь. Он будет определять поведение спрайта в зависимости от нажатых клавиш. Вот как это может выглядеть:

# Move the sprite based on user keypresses
def update(self, pressed_keys):
if pressed_keys[K_UP]:
self.rect.move_ip(0, -5)
if pressed_keys[K_DOWN]:
self.rect.move_ip(0, 5)
if pressed_keys[K_LEFT]:
self.rect.move_ip(-5, 0)
if pressed_keys[K_RIGHT]:
self.rect.move_ip(5, 0)

K_UP, K_DOWN, K_LEFT и K_RIGHT соответствуют клавишам со стрелками на клавиатуре. Если словарная статья для этой клавиши — True, то эта клавиша нажата, и вы перемещаете игрока в правильном направлении. Здесь вы используете .move_ip(), что означает перемещение на месте , чтобы переместить текущий Rect.

Затем вы можете вызвать каждый кадр .update() для перемещения спрайта игрока в ответ на нажатия клавиш. Добавьте этот вызов сразу после вызова .get_pressed():

# Main loop
while running:
# for loop through the event queue
for event in pygame.event.get():
# Check for KEYDOWN event
if event.type == KEYDOWN:
# If the Esc key is pressed, then exit the main loop
if event.key == K_ESCAPE:
running = False
# Check for QUIT event. If QUIT, then set running to false.
elif event.type == QUIT:
running = False

# Get all the keys currently pressed
pressed_keys = pygame.key.get_pressed()

# Update the player sprite based on user keypresses
player.update(pressed_keys)

# Fill the screen with black
screen.fill((0, 0, 0))

Теперь вы можете перемещать прямоугольник вашего плеера по экрану с помощью клавиш со стрелками:

Далее необходимо решить 2 проблемы:

  • Прямоугольник игрока может двигаться очень быстро, если клавиша удерживается нажатой. Вы поработаете над этим позже.
  • Прямоугольник игрока может перемещаться за пределы экрана. Давайте решим это сейчас.

Чтобы игрок оставался на экране, вам нужно добавить некоторую логику, чтобы определить, собирается ли rect уйти за пределы экрана. Для этого вы проверяете, не вышли ли координаты за пределы экрана. Если это так, то вы указываете программе переместить его обратно к краю:

# Move the sprite based on user keypresses
def update(self, pressed_keys):
if pressed_keys[K_UP]:
self.rect.move_ip(0, -5)
if pressed_keys[K_DOWN]:
self.rect.move_ip(0, 5)
if pressed_keys[K_LEFT]:
self.rect.move_ip(-5, 0)
if pressed_keys[K_RIGHT]:
self.rect.move_ip(5, 0)

# Keep player on the screen
if self.rect.left < 0:
self.rect.left = 0
if self.rect.right > SCREEN_WIDTH:
self.rect.right = SCREEN_WIDTH
if self.rect.top <= 0:
self.rect.top = 0
if self.rect.bottom >= SCREEN_HEIGHT:
self.rect.bottom = SCREEN_HEIGHT

Здесь вместо использования .move() вы просто меняете соответствующие координаты .top, .bottom, .leftили .right напрямую. Проверьте это, и вы обнаружите, что прямоугольник игрока больше не может перемещаться за пределы экрана.

Теперь давайте добавим врагов!

Враги

Какая игра без врагов? Вы будете использовать методы, которые вы уже изучили, чтобы создать базовый класс врагов, а затем создать множество из них, чтобы ваш игрок их избегал. Сначала импортируйте библиотеку random:

# Import random for random numbers
import random

Затем создайте новый класс спрайтов с именем Enemy, следуя тому же шаблону, который вы использовали для Player:

# Define the enemy object by extending pygame.sprite.Sprite
# The surface you draw on the screen is now an attribute of ‘enemy’
class Enemy(pygame.sprite.Sprite):
def __init__(self):
super(Enemy, self).__init__()
self.surf = pygame.Surface((20, 10))
self.surf.fill((255, 255, 255))
self.rect = self.surf.get_rect(
center=(
random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
random.randint(0, SCREEN_HEIGHT),
)
)
self.speed = random.randint(5, 20)

# Move the sprite based on speed
# Remove the sprite when it passes the left edge of the screen
def update(self):
self.rect.move_ip(-self.speed, 0)
if self.rect.right < 0:
self.kill()

Есть четыре заметных различия между Enemy и Player:

  • В строках с 62 по 67 вы обновляете случайное место вдоль правого края экрана. Центр прямоугольника находится за пределами экрана. Он расположен в некотором положении между 20 и 100 пикселями от правого края и где-то между верхним и нижним краями.
  • В строке 68 вы определяете .speed как случайное число от 5 до 20. Это указывает, как быстро враг движется к игроку.
  • В строках с 73 по 76 вы определяете .update(). Этот метод не требует аргументов, так как враги двигаются автоматически. Вместо этого .update() перемещает врага к левой стороне экрана.
  • В строке 74 вы проверяете, ушел ли враг за пределы экрана. Чтобы убедиться, что значок Enemy полностью исчез с экрана и не исчезнет, пока он все еще виден, убедитесь, что правая часть экрана .rect вышла за левую часть экрана. Как только враг исчезает с экрана, вы вызываете .kill(), чтобы предотвратить его дальнейшую обработку.

Итак, что делает .kill()? Чтобы понять это, вы должны знать о группах спрайтов .

Группы спрайтов

Еще один очень полезный класс, предоставляющий возможности — Sprite Group. Это объект, который содержит группу объектов Sprite. Так зачем его использовать? Разве вы не можете вместо этого просто отслеживать свои объекты в списке? Ну, можете, но преимущество использования заключается в методах Group, которые он раскрывает. Эти методы помогают определить, не столкнулся ли кто-либо с файлом Player, что значительно упрощает обновление.

Давайте посмотрим, как создавать группы спрайтов. Вы создадите два разных объекта Group:

  • Первый Group проведет каждый Sprite в игре.
  • Второй Group будет содержать только объекты Enemy.

Вот как это выглядит в коде:

# Create the ‘player’
player = Player()

# Create groups to hold enemy sprites and all sprites
# — enemies is used for collision detection and position updates
# — all_sprites is used for rendering
enemies = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
all_sprites.add(player)

# Variable to keep the main loop running
running = True

Когда вы вызываете .kill(), Sprite удаляется из всех Group, которым он принадлежит. Это также удаляет ссылки на, что позволяет сборщику мусора Python освобождать память по мере необходимости.

Теперь, когда у вас есть группа all_sprites, вы можете изменить способ рисования объектов. Вместо того, чтобы вызывать .blit(), вы можете перебрать всё в all_sprites:

# Fill the screen with black
screen.fill((0, 0, 0))

# Draw all sprites
for entity in all_sprites:
screen.blit(entity.surf, entity.rect)

# Flip everything to the display
pygame.display.flip()

Теперь all_sprites с каждым кадром будет прорисовываться что угодно, будь то враг или игрок.

Есть только одна проблема… У тебя нет врагов! Вы можете создать кучу врагов в начале игры, но игра быстро станет скучной, когда все они покинут экран через несколько секунд. Вместо этого давайте рассмотрим, как поддерживать постоянный приток врагов по ходу игры.

Пользовательские события

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

  • Создавать новый Enemy.
  • Добавить его в all_sprites и enemies.

У вас уже есть код, обрабатывающий случайные события. Цикл событий предназначен для поиска случайных событий, происходящих в каждом кадре, и соответствующей их обработки. К счастью, pygame не ограничивает вас использованием только тех типов событий, которые он определил. Вы можете определить свои собственные события для обработки по своему усмотрению.

Давайте посмотрим, как создать пользовательское событие, которое генерируется каждые несколько секунд:

# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

# Create a custom event for adding a new enemy
ADDENEMY = pygame.USEREVENT + 1
pygame.time.set_timer(ADDENEMY, 250)

# Instantiate player. Right now, this is just a rectangle.
player = Player()

pygame внутренне определяет события как целые числа, поэтому вам необходимо определить новое событие с уникальным целым числом. Последнее резервное событие pygame называется USEREVENT, поэтому определение ADDENEMY = pygame.USEREVENT + 1 в строке 83 гарантирует его уникальность.

Затем вам нужно вставить это новое событие в очередь через равные промежутки времени на протяжении всей игры. Вот тут-то и появляется модуль time. Строка 84 запускает новое событие ADDENEMY каждые 250 миллисекунд, или четыре раза в секунду. Вы вызываете .set_timer() вне игрового цикла, так как вам нужен только один таймер, но он будет срабатывать на протяжении всей игры.

Добавьте код для обработки вашего нового события:

# Main loop
while running:
# Look at every event in the queue
for event in pygame.event.get():
# Did the user hit a key?
if event.type == KEYDOWN:
# Was it the Escape key? If so, stop the loop.
if event.key == K_ESCAPE:
running = False

# Did the user click the window close button? If so, stop the loop.
elif event.type == QUIT:
running = False

# Add a new enemy?
elif event.type == ADDENEMY:
# Create the new enemy and add it to sprite groups
new_enemy = Enemy()
enemies.add(new_enemy)
all_sprites.add(new_enemy)

# Get the set of keys pressed and check for user input
pressed_keys = pygame.key.get_pressed()
player.update(pressed_keys)

# Update enemy position
enemies.update()

Всякий раз, когда обработчик событий видит новое событие ADDENEMY в строке 115, он создает Enemy и добавляет его в enemies и all_sprites. Поскольку Enemy находится в all_sprites, он будет отображаться в каждом кадре. Вам также нужно вызвать строку 126 enemies.update(), которая обновляет все в enemies, чтобы убедиться, что они перемещаются правильно:

Однако это не единственная причина, по которой существует группа только для enemies.

Обнаружение столкновений

Ваш игровой дизайн требует, чтобы игра заканчивалась всякий раз, когда враг сталкивается с игроком. Проверка на коллизии — базовый метод программирования игр, и обычно требуется нетривиальная математика, чтобы определить, будут ли два спрайта накладываться друг на друга.

В этом уроке вы будете использовать метод под названием .spritecollideany(), который читается как «столкновение спрайтов». Этот метод принимает Sprite и Group в качестве параметров. Он просматривает каждый объект в Group и проверяет, пересекается ли .rect с объектом .rect в Sprite. Если да, то возвращается True. В противном случае возвращается False. Это идеально подходит для этой игры, так как вам нужно проверить, не сталкивается ли сингл player с одним из Group enemies.

Вот как это выглядит в коде:

# Draw all sprites
for entity in all_sprites:
screen.blit(entity.surf, entity.rect)

# Check if any enemies have collided with the player
if pygame.sprite.spritecollideany(player, enemies):
# If so, then remove the player and stop the loop
player.kill()
running = False

Строка 135 проверяет, не столкнулся ли player с каким-либо из объектов в enemies. Если это так, то player.kill() вызывается удалить его из каждой группы, к которой он принадлежит. Поскольку рендерятся только объекты в all_sprites, они больше не будут этого делать. Как только игрок был убит, вам также нужно выйти из игры, поэтому вы устанавливаете running = False в строке 138.

На данный момент у вас есть основные элементы игры:

Теперь давайте немного украсим её, сделаем более играбельной и добавим некоторые дополнительные возможности, чтобы она выделялась.

Изображения спрайтов

Хорошо, у вас есть игра, но давайте будем честными… Она немного уродлива. Игрок и враги — это просто белые блоки на черном фоне. Это было ультрасовременно, когда Pong был новым, но сейчас он просто не подходит. Давайте заменим все эти скучные белые прямоугольники более крутыми изображениями, которые сделают игру похожей на настоящую.

Ранее вы узнали, что изображения на диске могут быть загружены в файл Surface с помощью модуля image. Для этого урока мы сделали небольшой реактивный самолет для игрока и несколько ракет для врагов. Вы можете использовать этот вариант, рисовать свои собственные или загружать некоторые бесплатные игровые изображения. Вы можете щелкнуть ссылку ниже, чтобы загрузить рисунок, использованный в этом уроке:

Изменение конструкторов объектов

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

# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
# from pygame.locals import *
from pygame.locals import (
RLEACCEL,
K_UP,
K_DOWN,
K_LEFT,
K_RIGHT,
K_ESCAPE,
KEYDOWN,
QUIT,
)

# Define constants for the screen width and height
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600

# Define the Player object by extending pygame.sprite.Sprite
# Instead of a surface, use an image for a better-looking sprite
class Player(pygame.sprite.Sprite):
def __init__(self):
super(Player, self).__init__()
self.surf = pygame.image.load(«jet.png»).convert()
self.surf.set_colorkey((255, 255, 255), RLEACCEL)
self.rect = self.surf.get_rect()

Давайте немного распакуем строку 31 pygame.image.load(), которая загружает образ с диска. Вы передаете её путь к файлу. Она возвращает Surface, а вызов .convert() оптимизирует Surface, ускоряя будущие вызовы .blit().

Строка 32 .set_colorkey() используется для указания того, что цвет pygame будет отображаться как прозрачный. В этом случае вы выбираете белый, потому что это цвет фона изображения. Константа RLEACCEL — это необязательный параметр, который помогает pygame ускорить отрисовку на неускоренных дисплеях. Это добавляется в pygame.locals оператор импорта в строке 11.

Больше ничего не нужно менять. Изображение по-прежнему Surface, за исключением того, что теперь на нем что-то нарисовано. Вы все еще используете его таким же образом.

Вот как выглядят похожие изменения Enemy:

# Define the enemy object by extending pygame.sprite.Sprite
# Instead of a surface, use an image for a better-looking sprite
class Enemy(pygame.sprite.Sprite):
def __init__(self):
super(Enemy, self).__init__()
self.surf = pygame.image.load(«missile.png»).convert()
self.surf.set_colorkey((255, 255, 255), RLEACCEL)
# The starting position is randomly generated, as is the speed
self.rect = self.surf.get_rect(
center=(
random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
random.randint(0, SCREEN_HEIGHT),
)
)
self.speed = random.randint(5, 20)

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

Добавление фоновых изображений

Для фоновых облаков используются те же принципы, что и для Player и Enemy:

  • Создайте класс Cloud.
  • Добавьте к нему изображение облака.
  • Создайте метод .update(), который перемещает cloud к левой стороне экрана.
  • Создайте пользовательское событие и обработчик для создания новых объектов cloud с заданным интервалом времени.
  • Добавьте вновь созданные объекты cloud в новый файл Group с именем clouds.
  • Обновите и нарисуйте clouds в своем игровом цикле.

Вот как это выглядит:

# Define the cloud object by extending pygame.sprite.Sprite
# Use an image for a better-looking sprite
class Cloud(pygame.sprite.Sprite):
def __init__(self):
super(Cloud, self).__init__()
self.surf = pygame.image.load(«cloud.png»).convert()
self.surf.set_colorkey((0, 0, 0), RLEACCEL)
# The starting position is randomly generated
self.rect = self.surf.get_rect(
center=(
random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
random.randint(0, SCREEN_HEIGHT),
)
)

# Move the cloud based on a constant speed
# Remove the cloud when it passes the left edge of the screen
def update(self):
self.rect.move_ip(-5, 0)
if self.rect.right < 0:
self.kill()

Это должно выглядеть очень знакомо. Это почти то же самое, что и Enemy.

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

# Create custom events for adding a new enemy and a cloud
ADDENEMY = pygame.USEREVENT + 1
pygame.time.set_timer(ADDENEMY, 250)
ADDCLOUD = pygame.USEREVENT + 2
pygame.time.set_timer(ADDCLOUD, 1000)

Это говорит о том, что нужно подождать 1000 миллисекунд или одну секунду, прежде чем создавать следующий файл cloud.

Затем создайте новый Group для хранения каждого вновь созданного cloud:

# Create groups to hold enemy sprites, cloud sprites, and all sprites
# — enemies is used for collision detection and position updates
# — clouds is used for position updates
# — all_sprites is used for rendering
enemies = pygame.sprite.Group()
clouds = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
all_sprites.add(player)

Затем добавьте обработчик нового события ADDCLOUD в обработчик событий:

# Main loop
while running:
# Look at every event in the queue
for event in pygame.event.get():
# Did the user hit a key?
if event.type == KEYDOWN:
# Was it the Escape key? If so, then stop the loop.
if event.key == K_ESCAPE:
running = False

# Did the user click the window close button? If so, stop the loop.
elif event.type == QUIT:
running = False

# Add a new enemy?
elif event.type == ADDENEMY:
# Create the new enemy and add it to sprite groups
new_enemy = Enemy()
enemies.add(new_enemy)
all_sprites.add(new_enemy)

# Add a new cloud?
elif event.type == ADDCLOUD:
# Create the new cloud and add it to sprite groups
new_cloud = Cloud()
clouds.add(new_cloud)
all_sprites.add(new_cloud)

Наконец, убедитесь, что clouds обновляются каждый кадр:

# Update the position of enemies and clouds
enemies.update()
clouds.update()

# Fill the screen with sky blue
screen.fill((135, 206, 250))

Строка 172 обновляет оригинал screen.fill(), чтобы заполнить экран приятным небесно-голубым цветом. Вы можете изменить этот цвет на что-то другое. Может быть, вы хотите инопланетный мир с фиолетовым небом, ядовитую пустошь в неоново-зеленом цвете или поверхность Марса в красном цвете!

Обратите внимание, что каждый новый Cloud и Enemy добавляется all_sprites так же, как clouds и enemies. Это сделано потому, что каждая группа используется для отдельной цели:

  • Рендеринг выполняется с помощью all_sprites.
  • Обновление позиции выполняется с помощью clouds и enemies.
  • Обнаружение столкновений выполняется с помощью enemies.

Вы создаете несколько групп, чтобы вы могли изменить способ движения или поведения спрайтов, не влияя на движение или поведение других спрайтов.

Скорость игры

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

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

Обычно вам нужна максимально высокая частота кадров, но в этой игре вам нужно немного замедлить ее, чтобы в нее можно было играть. К счастью, модуль time содержит файл Clock, который предназначен именно для этой цели.

Использование Clock для установки воспроизводимой частоты кадров требует всего две строки кода. Первый создает новый Clock перед началом игрового цикла:

# Setup the clock for a decent framerate
clock = pygame.time.Clock()

Второй вызывает .tick(), чтобы сообщить pygame, что программа достигла конца кадра:

# Flip everything to the display
pygame.display.flip()

# Ensure program maintains a rate of 30 frames per second
clock.tick(30)

Переданный аргумент .tick() устанавливает желаемую частоту кадров. Для этого .tick() вычисляется количество миллисекунд, которое должен занимать каждый кадр, исходя из желаемой частоты кадров. Затем он сравнивает это число с количеством миллисекунд, прошедших с момента последнего вызова .tick(). Если прошло недостаточно времени, .tick()задерживает обработку, чтобы гарантировать, что она никогда не превысит указанную частоту кадров.

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

Поэкспериментируйте с этим числом, чтобы понять, что вам больше подходит!

Звуковые эффекты

До сих пор вы были сосредоточены на игровом процессе и визуальных аспектах вашей игры. Теперь давайте рассмотрим, как придать вашей игре слуховой оттенок. pygame обеспечивает обработку всех действий mixer, связанных со звуком. Вы будете использовать классы и методы этого модуля для предоставления фоновой музыки и звуковых эффектов для различных действий.

Название mixer относится к тому факту, что модуль смешивает различные звуки в единое целое. С помощью подмодуля music вы можете транслировать отдельные звуковые файлы в различных форматах, таких как MP3 , Ogg и Mod . Вы также можете использовать Sound для хранения одного звукового эффекта, который будет воспроизводиться в формате Ogg или несжатом формате WAV . Все воспроизведение происходит в фоновом режиме, поэтому при воспроизведении Sound метод возвращает значение сразу после воспроизведения звука.

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

Как и в большинстве случаев pygame, использование mixer начинается с шага инициализации. К счастью, этим уже занимается pygame.init(). Вам нужно только вызвать pygame.mixer.init(), если вы хотите изменить значения по умолчанию:

# Setup for sounds. Defaults are good.
pygame.mixer.init()

# Initialize pygame
pygame.init()

# Set up the clock for a decent framerate
clock = pygame.time.Clock()

pygame.mixer.init() принимает несколько аргументов , но в большинстве случаев значения по умолчанию работают нормально. Обратите внимание, что если вы хотите изменить значения по умолчанию, вам нужно вызвать pygame.mixer.init() перед вызовом pygame.init(). В противном случае значения по умолчанию будут действовать независимо от ваших изменений.

После инициализации системы вы можете настроить звуки и фоновую музыку:

# Load and play background music
# Sound source: http://ccmixter.org/files/Apoxode/59262
# License: https://creativecommons.org/licenses/by/3.0/
pygame.mixer.music.load(«Apoxode_-_Electric_1.mp3»)
pygame.mixer.music.play(loops=-1)

# Load all sound files
# Sound sources: Jon Fincher
move_up_sound = pygame.mixer.Sound(«Rising_putter.ogg»)
move_down_sound = pygame.mixer.Sound(«Falling_putter.ogg»)
collision_sound = pygame.mixer.Sound(«Collision.ogg»)

Строки 138 и 139 загружают фоновый звуковой клип и начинают его воспроизведение. Вы можете указать звуковому клипу зацикливаться и никогда не заканчиваться, установив именованный параметр loops=-1.

Строки с 143 по 145 загружают три звука, которые вы будете использовать для различных звуковых эффектов. Первые два — это звуки роста и падения, которые воспроизводятся, когда игрок перемещается вверх или вниз. Последний звук используется всякий раз, когда происходит столкновение. Вы также можете добавить другие звуки, например звук при создании Enemy или финальный звук при завершении игры.

Итак, как вы используете звуковые эффекты? Вы хотите воспроизводить каждый звук, когда происходит определенное событие. Например, когда корабль движется вверх, вы хотите сыграть move_up_sound. Поэтому вы добавляете вызов .play() всякий раз, когда обрабатываете это событие. В дизайне это означает добавление следующих вызовов .update() для Player:

# Define the Player object by extending pygame.sprite.Sprite
# Instead of a surface, use an image for a better-looking sprite
class Player(pygame.sprite.Sprite):
def __init__(self):
super(Player, self).__init__()
self.surf = pygame.image.load(«jet.png»).convert()
self.surf.set_colorkey((255, 255, 255), RLEACCEL)
self.rect = self.surf.get_rect()

# Move the sprite based on keypresses
def update(self, pressed_keys):
if pressed_keys[K_UP]:
self.rect.move_ip(0, -5)
move_up_sound.play()
if pressed_keys[K_DOWN]:
self.rect.move_ip(0, 5)
move_down_sound.play()

При столкновении между игроком и врагом вы воспроизводите звук, вызываемый при обнаружении столкновений:

# Check if any enemies have collided with the player
if pygame.sprite.spritecollideany(player, enemies):
# If so, then remove the player
player.kill()

# Stop any moving sounds and play the collision sound
move_up_sound.stop()
move_down_sound.stop()
collision_sound.play()

# Stop the loop
running = False

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

Наконец, когда игра закончится, все звуки должны прекратиться. Это верно независимо от того, заканчивается ли игра из-за столкновения или пользователь выходит вручную. Для этого в конце программы после цикла добавьте следующие строки:

# All done! Stop and quit the mixer.
pygame.mixer.music.stop()
pygame.mixer.quit()

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

Вот и все! Проверьте это снова, и вы должны увидеть что-то вроде этого:

Примечание об источниках

Вы могли заметить комментарий к строкам 136-137 при загрузке фоновой музыки, в котором указан источник музыки и ссылка на лицензию Creative Commons. Это было сделано потому, что этого требовал создатель этого звука. В лицензионных требованиях говорилось, что для использования звука необходимо указать как правильное указание авторства, так и ссылку на лицензию.

Вот несколько источников музыки, звука и изображений, в которых вы можете найти полезный контент:

  • OpenGameArt.org: звуки, звуковые эффекты, спрайты и другие изображения.
  • Kenney.nl: звуки, звуковые эффекты, спрайты и другие иллюстрации.
  • Gamer Art 2D: спрайты и другие арты
  • CC Mixter: звуки и звуковые эффекты
  • Freesound: звуки и звуковые эффекты

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

Заключение

Из этого руководства вы узнали, чем программирование игр pygame отличается от стандартного процедурного программирования. Вы также узнали, как:

  • Реализовать циклы событий
  • Рисовать предметы на экране
  • Воспроизводить звуковые эффекты и музыку
  • Обрабатывать пользовательский ввод

Для этого вы использовали подмножество модулей pygame, включая модули display, mixer и music, time, image, event, и key. Вы также использовали несколько классов pygame, в том числе Rect, Surface, Sound и Sprite. Но это только царапает поверхность того, что в pygame можно сделать! Ознакомьтесь с официальной документацией pygame для получения полного списка доступных модулей и классов.

Вы можете найти весь код, графические и звуковые файлы для этой статьи, нажав на ссылку ниже:

Спасибо за прочтение данной статьи!

Python – интерпретируемый язык, и по этой причине он не отличается высокой производительностью, которая необходима для сложных игр с гиперреалистичной графикой. Тем не менее есть несколько способов оптимизации, которые значительно ускоряют выполнение Python-кода, что позволяет писать на Питоне вполне приличные игры. Оптимизацию необязательно делать самостоятельно – в значительной степени эту задачу берут на себя специальные фреймворки и библиотеки. Фреймворков и библиотек для разработки игр на основе Python довольно много, вот самые популярные:

  • Pygame
  • PyKyra
  • Pyglet
  • Panda3D
  • Kivy
  • PyopenGL

Мы остановимся на самой популярной библиотеке, Pygame – она отлично подходит для начинающих разработчиков, и к тому же часто используется для быстрого прототипирования игр. На официальном сайте Pygame есть каталог игр, созданных с помощью библиотеки. Еще примеры игр на 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()

    

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Цикл 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()

    

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Обработку событий (нажатий клавиш и кликов) в 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()

    

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Как очевидно из приведенного выше примера, основной цикл 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()

    

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Рисование

В Pygame есть функции для простого рисования геометрических фигур –
прямоугольников, окружностей, эллипсов, линий, многоугольников. Вот пример:

        import pygame
import math

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)
green = (0, 255, 0)
blue = (0, 0, 255)
yellow = (255, 255, 0)
pink = (255, 192, 203)

# рисуем прямоугольник
rect_x = 50
rect_y = 50
rect_width = 100
rect_height = 50
pygame.draw.rect(screen, red, (rect_x, rect_y, rect_width, rect_height))

# рисуем круг
circle_x = 200
circle_y = 75
circle_radius = 30
pygame.draw.circle(screen, green, (circle_x, circle_y), circle_radius)

# рисуем треугольник
triangle_x = 350
triangle_y = 50
triangle_width = 100
triangle_height = 100
triangle_points = [(triangle_x, triangle_y), (triangle_x + triangle_width, triangle_y),
                   (triangle_x + triangle_width / 2, triangle_y + triangle_height)]
pygame.draw.polygon(screen, blue, triangle_points)

# рисуем пятиугольник
pent_x = 500
pent_y = 100
radius = 40
sides = 5
pent_points = []
for i in range(sides):
    angle_deg = 360 * i / sides
    angle_rad = math.radians(angle_deg)
    x = pent_x + radius * math.sin(angle_rad)
    y = pent_y - radius * math.cos(angle_rad)
    pent_points.append((x, y))
pygame.draw.polygon(screen, white, pent_points)

# рисуем эллипс
ellipse_x = 100
ellipse_y = 275
ellipse_width = 150
ellipse_height = 60
pygame.draw.ellipse(screen, red, (ellipse_x, ellipse_y, ellipse_width, ellipse_height))

# горизонтальная линия
horiz_line_y = 400
pygame.draw.line(screen, blue, (50, horiz_line_y), (590, horiz_line_y), 5)

# вертикальная линия
vert_line_x = 320
pygame.draw.line(screen, green, (vert_line_x, 50), (vert_line_x, 430), 5)

# рисуем желтую звезду
yellow_star_points = [(260 - 50, 250 - 70), (310 - 50, 250 - 70), (325 - 50, 200 - 70),
                      (340 - 50, 250 - 70), (390 - 50, 250 - 70), (350 - 50, 290 - 70),
                      (365 - 50, 340 - 70), (325 - 50, 305 - 70), (285 - 50, 340 - 70),
                      (300 - 50, 290 - 70)]
pygame.draw.polygon(screen, yellow, yellow_star_points)

# рисуем окружность с квадратом внутри
circle2_x = 490
circle2_y = 350
circle2_radius = 80
pygame.draw.circle(screen, white, (circle2_x, circle2_y), circle2_radius)
square_side = 60
square_x = circle2_x - square_side / 2
square_y = circle2_y - square_side / 2
pygame.draw.rect(screen, pink, (square_x, square_y, square_side, square_side))

pygame.display.update()

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            quit()

    

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Анимация и обработка событий

Выше, в примере с падающими символами в «Матрице», уже был
показан принцип простейшей имитации движения, который заключается в
последовательном изменении координат объекта и обновлении экрана с
установленной частотой кадра 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)

    

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Покадровая анимация

Если у вас есть в запасе картинки с раскадровкой движения,
превратить их в анимацию не составит труда:

        import pygame

pygame.init()

window_size = (640, 480)
screen = pygame.display.set_mode(window_size)
color = (216, 233, 243)
screen.fill(color)
pygame.display.flip()
pygame.display.set_caption("Покадровая анимация")
clock = pygame.time.Clock()

# загружаем кадры
frame_images = []
for i in range(1, 9):
    frame_images.append(pygame.image.load(f"frame{i}.png"))

# параметры анимации
animation_length = len(frame_images)
animation_speed = 15  # кадры в секунду
current_frame_index = 0
animation_timer = 0
frame_position = [0, 0]

# вычисляем позицию для вывода кадров в зависимости от высоты окна
window_height = screen.get_height()
frame_height = frame_images[0].get_height()
frame_position[1] = int(window_height * 0.45) - int(frame_height / 2)

# запускаем основной цикл
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # обновление состояния
    time_delta = clock.tick(60) / 1000.0
    animation_timer += time_delta
    if animation_timer >= 1.0 / animation_speed:
        current_frame_index = (current_frame_index + 1) % animation_length
        animation_timer -= 1.0 / animation_speed

    frame_position[0] += 1  # сдвигаем кадр вправо

    # выводим кадры, обновляем экран
    current_frame = frame_images[current_frame_index]
    screen.blit(current_frame, frame_position)
    pygame.display.flip()

pygame.quit()

    

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Столкновение объектов

В этом примере расстояние между объектами проверяется до тех
пор, пока объекты не столкнутся. В момент столкновение движение прекращается, а
цвет объектов – изменяется:

        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)

    

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Управление движением объекта

Для управления движением (в нашем случае – с помощью клавиш ← и →) используются 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)

    

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Практика

Задание 1 – Лестница

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

Пример:

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Решение:

Код решения.

        import pygame

pygame.init()
WINDOW_WIDTH = 500
WINDOW_HEIGHT = 500
game_display = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption('Вверх по лестнице')

# параметры ступеней
STEP_WIDTH = 20
STEP_HEIGHT = STEP_WIDTH
STEP_COLOR = (255, 255, 255)

# параметры игрока, размеры и цвет шрифта
PLAYER_RADIUS = 10
PLAYER_COLOR = (255, 0, 0)
FONT_SIZE = 20
FONT_COLOR = (255, 255, 255)
clock = pygame.time.Clock()

def game_loop():
    game_exit = False
    # стартовая позиция игрока
    player_x = PLAYER_RADIUS
    player_y = WINDOW_HEIGHT - PLAYER_RADIUS * 3
    # начальные координаты ступеней
    step_x = 0
    step_y = WINDOW_HEIGHT - STEP_HEIGHT
    # создаем счетчик шагов
    step_count = 0

    while not game_exit:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game_exit = True
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RIGHT:
                    # удаляем игрока из предыдущей позиции
                    pygame.draw.circle(game_display, (0, 0, 0), (player_x, player_y), PLAYER_RADIUS)
                    # обновляем позицию и счет
                    player_x += 20
                    player_y -= 20
                    step_count += 1

        # рисуем лестницу
        while step_x < WINDOW_WIDTH and step_y >= 0:
            pygame.draw.rect(game_display, STEP_COLOR, [step_x, step_y, STEP_WIDTH, STEP_HEIGHT])
            step_x += STEP_WIDTH
            step_y -= STEP_HEIGHT

        # перемещаем игрока на ступень выше
        pygame.draw.circle(game_display, PLAYER_COLOR, (player_x, player_y), PLAYER_RADIUS)

        # подсчитываем сделанные шаги
        pygame.draw.rect(game_display, (0, 0, 0), (10, 10, 100, FONT_SIZE))
        font = pygame.font.SysFont('Arial', FONT_SIZE)
        text = font.render(f'Шаг: {str(step_count)}', True, FONT_COLOR)
        game_display.blit(text, (10, 10))
        pygame.display.update()
        clock.tick(60)
game_loop()
pygame.quit()
    

Задание 2 – Лабиринт

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

Пример:

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на 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)
    

Задание 3 – Дождь

Используя Pygame,
напишите симулятор дождя: падение каждой сотни капель приводит к подъему уровня
воды на 1 пиксель.

Пример:

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Решение:

Код решения.

        import pygame
import random

class RainSimulator:
    def __init__(self):
        pygame.init()
        self.screen_width = 500
        self.screen_height = 700
        self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))
        pygame.display.set_caption("Дождь")
        self.font = pygame.font.SysFont("Consolas", 20)
        self.background_color = (0, 0, 0)
        self.blue = (173, 216, 230)

        # параметры дождевых капель
        self.drops = []
        self.drops_landed = 0
        self.drops_per_pixel = 100
        self.level_height = 0

        self.clock = pygame.time.Clock()

    # добавляем капли дождя
    def add_drop(self):
        self.drops.append([random.randint(0, self.screen_width), 0])

    # рисуем дождь
    def draw_drops(self):
        for drop in self.drops:
            pygame.draw.line(self.screen, self.blue, (drop[0], drop[1]), (drop[0], drop[1] + 5), 2)

    # подсчитываем капли, поднимаем уровень воды
    def update_drops(self):
        for drop in self.drops:
            drop[1] += 5
            if drop[1] >= self.screen_height:
                self.drops.remove(drop)
                self.drops_landed += 1
                if self.drops_landed % self.drops_per_pixel == 0:
                    self.level_height += 1

    # выводим количество капель
    def draw_score(self):
        score_text = self.font.render(f"Капель дождя: {str(self.drops_landed)}", True, (255, 255, 255))
        self.screen.blit(score_text, (10, 10))
        pygame.draw.rect(self.screen, self.blue, (0, self.screen_height-self.level_height, self.screen_width, self.level_height))

    def run_rain(self):
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
            self.add_drop()
            self.update_drops()
            self.screen.fill(self.background_color)
            self.draw_drops()
            self.draw_score()
            pygame.display.update()
            self.clock.tick(60)

        pygame.quit()

if __name__ == "__main__":
    app = RainSimulator()
    app.run_rain()

    

Задание 4 – Мерцающие звезды

Используя Pygame,
напишите симулятор звездного неба – окружности, представляющие собой звезды,
сжимаются и расширяются, имитируя мерцание.

Пример:

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Решение:

Код решения.

        import pygame
import random

pygame.init()
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 600
screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("Мерцающие звезды")

# параметры звезд
MAX_STARS = 200
stars = []
for i in range(MAX_STARS):
    star_radius = random.randint(1, 3)
    star_color = (255, 255, 237)
    star_position = (random.randint(0, WINDOW_WIDTH), random.randint(0, WINDOW_HEIGHT))
    star_expand = True
    star_expand_speed = random.uniform(0.1, 0.5)
    stars.append((star_radius, star_color, star_position, star_expand, star_expand_speed))

clock = pygame.time.Clock()
while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            exit()

    for i in range(MAX_STARS):
        star_radius, star_color, star_position, star_expand, star_expand_speed = stars[i]

        # вычисление радиуса расширения
        if star_expand:
            star_radius += star_expand_speed
            if star_radius >= 5:
                star_expand = False
        else:
            star_radius -= star_expand_speed
            if star_radius <= 1:
                star_expand = True

        # изменяем позиции звезд для создания эффекта мерцания
        star_position = (
            star_position[0] + random.randint(-1, 1),
            star_position[1] + random.randint(-1, 1)
        )

        stars[i] = (star_radius, star_color, star_position, star_expand, star_expand_speed)

    # рисуем звезды
    screen.fill((0, 0, 0))
    for star in stars:
        star_radius, star_color, star_position, _, _ = star
        pygame.draw.circle(screen, star_color, star_position, star_radius)

    pygame.display.update()
    clock.tick(60)

    

Задание 5 – Колобок

Используя Pygame,
создайте анимацию, в которой лиса (состоящая из этих фреймов) преследует
Колобка. Колобок вращается вокруг своей оси.

Пример:

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Решение:

Код решения и необходимые изображения.

        import pygame

pygame.init()
background = (24, 113, 147)
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 300
game_display = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption('Колобок')

# загружаем изображение Колобка
kolobok = pygame.image.load('kolobok.png')
# стартовый угол вращения и скорость
kolobok_angle = 0
kolobok_rotation_speed = 2

# загружаем фреймы лисы
fox = []
for i in range(8):
    fox.append(pygame.image.load(f'fox{i+1}.png'))

# частота обновления фреймов лисы
fox_frame = 0
fox_frame_rate = 8
fox_frame_timer = 0

# стартовые позиции и скорость движения лисы и Колобка
kolobok_x = 0
kolobok_y = WINDOW_HEIGHT // 2 + kolobok.get_height() // 4 
fox_x = -fox[0].get_width()
fox_y = WINDOW_HEIGHT // 2 - fox[0].get_height() // 2
movement_speed = 3
clock = pygame.time.Clock()

# главный цикл
game_exit = False
while not game_exit:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            game_exit = True

    # вращаем изображение Колобка вокруг своей оси
    kolobok_angle += kolobok_rotation_speed
    if kolobok_angle >= 360:
        kolobok_angle = 0
    rotated_kolobok = pygame.transform.rotate(kolobok, kolobok_angle)

    # движение Колобка и лисы слева направо
    kolobok_x += movement_speed
    if kolobok_x > WINDOW_WIDTH:
        kolobok_x = 0 - fox[0].get_width()
    fox_x += movement_speed
    if fox_x > WINDOW_WIDTH:
        fox_x = 0 - fox[0].get_width() 

    # приводим скорость анимации лисы в соответствие с частотой обновления экрана
    fox_frame_timer += clock.tick(60)
    if fox_frame_timer >= 1000 / fox_frame_rate:
        fox_frame_timer -= 1000 / fox_frame_rate
        fox_frame = (fox_frame + 1) % len(fox)

    # рисуем фон, выводим фигуры Колобка и лисы
    game_display.fill(background)
    game_display.blit(rotated_kolobok, (kolobok_x, kolobok_y))
    game_display.blit(fox[fox_frame], (fox_x, fox_y))
    pygame.display.update()

pygame.quit()
    

Задание 6 – Светофор

Напишите Pygame
приложение для демонстрации работы светофора: когда горит зеленый свет (6
секунд), прямоугольники-автомобили движутся вперед. Красный и желтый свет
включаются на 2 секунды каждый, в это время трафик останавливается.

Пример:

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Решение:

Код решения.

        import pygame
import random

pygame.init()
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 600
game_display = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption('Светофор')
clock = pygame.time.Clock()

BLACK = (0, 0, 0)
DARK_GRAY = (64, 64, 64)
GRAY = (128, 128, 128)
RED = (255, 0, 0)
YELLOW = (255, 255, 0)
BLUE = (0, 0, 255)
GREEN = (0, 255, 0)


def game_loop():
    game_exit = False

    # определяем цвета для светофора
    colors = [RED, YELLOW, GREEN]
    active_index = 0
    last_switch = pygame.time.get_ticks()
    interval = 2000

    # параметры автомобилей
    car_width = 40
    car_height = 60
    car_speed = 2
    horizontal_spacing = 12
    vertical_spacing = 20
    car_rects = []
    for i in range(2):
        left_rect = pygame.Rect(100, random.randint(50, WINDOW_HEIGHT - car_height), car_width, car_height)
        right_rect = pygame.Rect(WINDOW_WIDTH - 300 - car_width, random.randint(50, WINDOW_HEIGHT - car_height), car_width, car_height)
        car_rects.append(left_rect)
        car_rects.append(right_rect)

    # вертикальная и горизонтальная дистанция между автомобилями
    for i in range(1, len(car_rects)):
        if car_rects[i].left - car_rects[i-1].right < horizontal_spacing:
            car_rects[i].left = car_rects[i-1].right + horizontal_spacing
        if car_rects[i].top - car_rects[i-1].bottom < vertical_spacing:
            car_rects[i].top = car_rects[i-1].bottom + vertical_spacing

    while not game_exit:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game_exit = True

        # определяем нужный цвет светофора
        now = pygame.time.get_ticks()
        if now - last_switch >= interval:
            active_index = (active_index + 1) % len(colors)
            last_switch = now

        # временной интервал для зеленого цвета - 6 секунд, для остальных - 2
        interval = 6000 if active_index == 2 else 2000

        # движение машин
        if active_index == 0 or active_index == 1:
            car_speed = 0
        else:
            car_speed = 2
        for car_rect in car_rects:
            car_rect.move_ip(0, -car_speed)
            if car_rect.bottom <= 0:
                car_rect.top = WINDOW_HEIGHT
                car_rect.left = 100 if car_rect.left == WINDOW_WIDTH - 100 - car_width else WINDOW_WIDTH - 100 - car_width

        # рисуем светофор
        game_display.fill(GRAY)
        light_rect = pygame.Rect((WINDOW_WIDTH - 200) // 2, (WINDOW_HEIGHT - 300) // 2, 100, 300)
        pygame.draw.rect(game_display, DARK_GRAY, light_rect, 5)
        light_width = light_rect.width
        light_height = light_rect.height // 3
        light_y = light_rect.top
        for i in range(3):
            circle_rect = pygame.Rect(light_rect.left + 10, light_y + i * light_height + 10, light_width - 20, light_height - 20)
            circle_color = colors[i] if i == active_index else BLACK
            pygame.draw.circle(game_display, circle_color, circle_rect.center, circle_rect.width // 2)

        # рисуем автомобили
        for car_rect in car_rects:
            pygame.draw.rect(game_display, BLUE, car_rect)

        pygame.display.update()
        clock.tick(60)
    pygame.quit()
game_loop()
    

Задание 7 – Визуальная память

Напишите лайт-версию игры Memory game, используя возможности Pygame. Сначала приложение выводит (в случайном порядке) цветные
окружности и дает возможность пользователю запомнить их расположение в течение
нескольких секунд. Затем приложение закрывает цветные окружности серыми:
пользователь должен по памяти сопоставить цветные пары. Каждая угаданная пара
приносит пользователю 1 балл.

Пример:

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Решение:

Код решения.

        import pygame
from random import shuffle

pygame.init()
# определяем цвета игры
black = (0, 0, 0)
white = (255, 255, 255)
red = (255, 0, 0)
blue = (0, 0, 255)
green = (0, 255, 0)
yellow = (255, 255, 0)
purple = (128, 0, 128)
grey = (192, 192, 192)

screen_width = 800
screen_height = 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Тренируем визуальную память")

# задаем параметры окружностей и перемешиваем пары
circle_radius = 50
circle_colors = [red, blue, green, yellow, purple, white]
circle_pairs = circle_colors * 2
shuffle(circle_pairs)

# формируем список окружностей
circle_positions = []
for i in range(6):
    for j in range(2):
        center_x = ((screen_width / 6) * (i + 1)) - (screen_width / 12)
        center_y = ((screen_height / 3) * (j + 1)) - (screen_height / 6)
        circle_positions.append([center_x, center_y])

# запоминаем позиции и цвета окружностей
original_circle_positions = circle_positions.copy()
original_circle_colors = circle_pairs.copy()

# рисуем цветные окружности
for i in range(len(circle_pairs)):
    position = circle_positions[i]
    color = circle_pairs[i]
    pygame.draw.circle(screen, color, position, circle_radius)

font = pygame.font.SysFont('Arial', 20)
pygame.display.update()

# ждем 5 секунд
pygame.time.wait(5000)

# закрываем цветные окружности серыми
for i in range(len(circle_pairs)):
    position = circle_positions[i]
    pygame.draw.circle(screen, grey, position, circle_radius)

pygame.display.update()
uncovered_circles = []
last_uncovered_circle = None
score = 0

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            exit()

        if event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = event.pos
            for i in range(len(circle_positions)):
                position = circle_positions[i]
                if ((position[0] - mouse_pos[0]) ** 2 + (position[1] - mouse_pos[1]) ** 2) ** 0.5 < circle_radius:
                    if i not in uncovered_circles:
                        uncovered_circles.append(i)
                        color = original_circle_colors[i]
                        pygame.draw.circle(screen, color, position, circle_radius)
                        pygame.display.update()
                        if last_uncovered_circle is not None and original_circle_colors[last_uncovered_circle] == original_circle_colors[i]:
                            score += 1
                        last_uncovered_circle = i

            if len(uncovered_circles) == len(circle_pairs):
                # вывод результата
                final_score_text = font.render(f"Уровень памяти: {str(score)} из 6", True, white)
                screen.blit(final_score_text, (screen_width // 2, screen_height // 2 + 125))
                pygame.display.update()
                pygame.time.wait(3000)
                pygame.quit()
                exit()

    

Задание 8 – Подсчет фигур

Напишите Pygame
приложение, в котором сверху окна вниз плавно спускаются случайные разноцветные
фигуры – треугольники, квадраты и окружности. Цвет для фигур выбирается
случайным образом, результаты подсчета выводятся в верхнем левом углу окна.

Пример:

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Решение:

Код решения.

        import pygame
import random

pygame.init()
width = 800
height = 600
screen = pygame.display.set_mode((width, height))
pygame.display.set_caption("Подсчет фигур")
clock = pygame.time.Clock()

white = (255, 255, 255)
black = (0, 0, 0)
colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 0, 255), (0, 255, 255)]

# параметры фигур
class Circle:
    def __init__(self, x, y, color):
        self.x = x
        self.y = y
        self.color = color
        self.radius = 30

    def draw(self):
        pygame.draw.circle(screen, self.color, (self.x, self.y), self.radius)

class Triangle:
    def __init__(self, x, y, color):
        self.x = x
        self.y = y
        self.color = color
        self.width = 60
        self.height = 60

    def draw(self):
        pygame.draw.polygon(screen, self.color, [(self.x, self.y), (self.x + self.width, self.y), (self.x + self.width/2, self.y - self.height)])

class Square:
    def __init__(self, x, y, color):
        self.x = x
        self.y = y
        self.color = color
        self.width = 60
        self.height = 60

    def draw(self):
        pygame.draw.rect(screen, self.color, (self.x, self.y, self.width, self.height))

# создаем список фигур
shapes = []
x = random.randint(0, width - 60)
y = random.randint(-500, -50)
color = random.choice(colors)
shape_type = random.choice(["circle", "triangle", "square"])
if shape_type == "circle":
    shape = Circle(x, y, color)
elif shape_type == "triangle":
    shape = Triangle(x, y, color)
else:
    shape = Square(x, y, color)
shapes.append(shape)

# счетчики фигур
circle_count = 0
triangle_count = 0
square_count = 0

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            quit()

    screen.fill(black)

    # рисуем и подсчитываем фигуры
    for shape in shapes:
        shape.draw()
        shape.y += 5
        if shape.y > height:
            shapes.remove(shape)
            if isinstance(shape, Circle):
                circle_count += 1
            elif isinstance(shape, Triangle):
                triangle_count += 1
            else:
                square_count += 1
            x = random.randint(0, width - 60)
            y = random.randint(-500, -50)
            color = random.choice(colors)
            shape_type = random.choice(["circle", "triangle", "square"])
            if shape_type == "circle":
                shape = Circle(x, y, color)
            elif shape_type == "triangle":
                shape = Triangle(x, y, color)
            else:
                shape = Square(x, y, color)
            shapes.append(shape)

    # выводим счетчики
    font = pygame.font.SysFont("Verdana", 25)
    circle_text = font.render(f"Окружности: {circle_count}", True, white)
    triangle_text = font.render(f"Треугольники: {triangle_count}", True, white)
    square_text = font.render(f"Квадраты: {square_count}", True, white)
    screen.blit(circle_text, (10, 10))
    screen.blit(triangle_text, (10, 40))
    screen.blit(square_text, (10, 70))

    pygame.display.update()
    clock.tick(60)

    

Задание 9 – Призы и бомбы

Напишите Pygame игру, в которой игрок (зеленая окружность)
должен «ловить» синие треугольники («призы»), избегая столкновения с красными
окружностями («бомбами»). Количество пойманных призов нужно подсчитывать.

Пример:

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Решение:

Код решения.

        import pygame
import random

class RewardsBombs():
    def __init__(self):
        pygame.init()
        self.screen_width = 600
        self.screen_height = 600
        self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))
        pygame.display.set_caption("Призы и бомбы")
        self.clock = pygame.time.Clock()
        self.green_pos = [self.screen_width // 2, self.screen_height - 30]
        self.red_positions = []
        self.red_speed = 2
        self.score = 0
        self.font = pygame.font.SysFont("Arial", 24)
        self.run()

    def run(self):
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    exit()

                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_LEFT:
                        if self.green_pos[0] - 20 >= 0:
                            self.green_pos[0] -= 20
                    elif event.key == pygame.K_RIGHT:
                        if self.green_pos[0] + 20 <= self.screen_width:
                            self.green_pos[0] += 20
                    elif event.key == pygame.K_UP:
                        if self.green_pos[1] - 20 >= 0:
                            self.green_pos[1] -= 20
                    elif event.key == pygame.K_DOWN:
                        if self.green_pos[1] + 20 <= self.screen_height:
                            self.green_pos[1] += 20

            # движение красных бомб
            for i in range(len(self.red_positions)):
                self.red_positions[i][1] += self.red_speed

            # создание бомб и призов
            if random.random() < 0.02:
                x = random.randint(0, self.screen_width)
                num = random.randint(1, 10)
                if num % 2 == 0:
                    self.red_positions.append([x, 0, False])
                else:
                    self.red_positions.append([x, 0, True])

            # проверка столкновений с игроком
            for pos in self.red_positions:
                if pos[2]:
                    if abs(pos[0] - self.green_pos[0]) <= 20 and abs(pos[1] - self.green_pos[1]) <= 20:
                        self.score += 1
                        self.red_positions.remove(pos)
                else:
                    if (pos[0] - self.green_pos[0]) ** 2 + (pos[1] - self.green_pos[1]) ** 2 < 400:
                        self.game_over()

            # убираем бомбы за пределами окна
            self.red_positions = [pos for pos in self.red_positions if pos[1] < self.screen_height]
            self.screen.fill((0, 0, 0))

            for pos in self.red_positions:
                if pos[2]:
                    pygame.draw.polygon(self.screen, (0, 0, 255), [[pos[0], pos[1]-10], [pos[0]+10, pos[1]+10], [pos[0]-10, pos[1]+10]])
                else:
                    pygame.draw.circle(self.screen, (255, 0, 0), pos[:2], 10)

            pygame.draw.circle(self.screen, (0, 255, 0), self.green_pos, 10)

            self.draw_score()
            pygame.display.update()
            self.clock.tick(60)

    def draw_score(self):
        score_surface = self.font.render(f"Призы: {self.score}", True, (255, 255, 255))
        self.screen.blit(score_surface, (10, 10))

    def game_over(self):
        message_surface = self.font.render(f"Игра закончена! Призы: {self.score}", True, (255, 0, 0))
        self.screen.blit(message_surface, (self.screen_width // 2 - message_surface.get_width() // 2, self.screen_height // 2 - message_surface.get_height() // 2))
        pygame.display.update()
        pygame.time.wait(3000)
        pygame.quit()
        exit()

if __name__ == "__main__":
    RewardsBombs()
    

Задание 10 – Змейка

Напишите лайт-версию игры «Змейка», используя Pygame. Змейка
ест красные яблоки, которые появляются в случайных позициях в пределах игрового
поля, и прибавляет в длине после каждого яблока. При столкновении с хвостом или
границей окна игра заканчивается.

Пример:

🐍 Самоучитель по Python для начинающих. Часть 21: Основы разработки игр на Pygame

Решение:

Код решения.

        import pygame
import random

pygame.init()
screen_width = 600
screen_height = 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Змейка")
green = (0, 255, 0)
red = (255, 0, 0)
font = pygame.font.SysFont("Arial", 20)
clock = pygame.time.Clock()

# основные параметры игры
cell_size = 20
snake_speed = 5
snake_length = 3
snake_body = []

for i in range(snake_length):
    snake_body.append(pygame.Rect((screen_width / 2) - (cell_size * i), screen_height / 2, cell_size, cell_size))
snake_direction = "right"
new_direction = "right"
apple_position = pygame.Rect(random.randint(0, screen_width - cell_size), random.randint(0, screen_height - cell_size), cell_size, cell_size)

game_over = False
while not game_over:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            game_over = True
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP and snake_direction != "down":
                new_direction = "up"
            elif event.key == pygame.K_DOWN and snake_direction != "up":
                new_direction = "down"
            elif event.key == pygame.K_LEFT and snake_direction != "right":
                new_direction = "left"
            elif event.key == pygame.K_RIGHT and snake_direction != "left":
                new_direction = "right"

    # новое направление движения
    snake_direction = new_direction
    # управление змейкой
    if snake_direction == "up":
        snake_body.insert(0, pygame.Rect(snake_body[0].left, snake_body[0].top - cell_size, cell_size, cell_size))
    elif snake_direction == "down":
        snake_body.insert(0, pygame.Rect(snake_body[0].left, snake_body[0].top + cell_size, cell_size, cell_size))
    elif snake_direction == "left":
        snake_body.insert(0, pygame.Rect(snake_body[0].left - cell_size, snake_body[0].top, cell_size, cell_size))
    elif snake_direction == "right":
        snake_body.insert(0, pygame.Rect(snake_body[0].left + cell_size, snake_body[0].top, cell_size, cell_size))

    # проверяем, съела ли змея яблоко
    if snake_body[0].colliderect(apple_position):
        apple_position = pygame.Rect(random.randint(0, screen_width - cell_size), random.randint(0, screen_height-cell_size), cell_size, cell_size)
        snake_length += 1

    if len(snake_body) > snake_length:
        snake_body.pop()

    # проверка столкновения со стенами
    if snake_body[0].left < 0 or snake_body[0].right > screen_width or snake_body[0].top < 0 or snake_body[0].bottom > screen_height:
        game_over = True

    # проверка столкновения с собственным телом
    for i in range(1, len(snake_body)):
        if snake_body[0].colliderect(snake_body[i]):
            game_over = True

    screen.fill((0, 0, 0))
    # рисуем змейку
    for i in range(len(snake_body)):
        if i == 0:
            pygame.draw.circle(screen, green, snake_body[i].center, cell_size / 2)
        else:
            pygame.draw.circle(screen, green, snake_body[i].center, cell_size / 2)
            pygame.draw.circle(screen, (0, 200, 0), snake_body[i].center, cell_size / 4)

    # рисуем яблоко
    pygame.draw.circle(screen, red, apple_position.center, cell_size / 2)

    # выводим количество яблок
    score_text = font.render(f"Съедено яблок: {snake_length - 3}", True, (255, 255, 255))
    screen.blit(score_text, (10, 10))
    pygame.display.update()

    clock.tick(snake_speed)

pygame.quit()

    

Подведем итоги

Мы рассмотрели самые простые приемы разработки игр в Pygame
– возможности этой библиотеки намного обширнее. К примеру, для быстрой
разработки в Pygame используются спрайты – объекты для определения свойств и
поведения игровых элементов. Встроенные классы Group, GroupSingle и RenderUpdates позволяют быстро,
просто и эффективно группировать, обновлять и отрисовывать игровые элементы.

В
следующей главе будем изучать работу с
SQL и базами данных.

***

Содержание самоучителя

  1. Особенности, сферы применения, установка, онлайн IDE
  2. Все, что нужно для изучения Python с нуля – книги, сайты, каналы и курсы
  3. Типы данных: преобразование и базовые операции
  4. Методы работы со строками
  5. Методы работы со списками и списковыми включениями
  6. Методы работы со словарями и генераторами словарей
  7. Методы работы с кортежами
  8. Методы работы со множествами
  9. Особенности цикла for
  10. Условный цикл while
  11. Функции с позиционными и именованными аргументами
  12. Анонимные функции
  13. Рекурсивные функции
  14. Функции высшего порядка, замыкания и декораторы
  15. Методы работы с файлами и файловой системой
  16. Регулярные выражения
  17. Основы скрапинга и парсинга
  18. Основы ООП: инкапсуляция и наследование
  19. Основы ООП: абстракция и полиморфизм
  20. Графический интерфейс на Tkinter
  21. Основы разработки игр на Pygame
  22. Основы работы с SQLite
  23. Основы веб-разработки на Flask
  24. Основы работы с NumPy
  25. Основы анализа данных с Pandas

***

Материалы по теме

  • 🐍🕹️ Как написать игру на Python: 5 игровых движков
  • 🐍 Пишем Тетрис на Python с помощью библиотеки Pygame

Понравилась статья? Поделить с друзьями:
  • Металлоискатель tianxun tx 850 инструкция на русском языке
  • Милурит 100 инструкция по применению длительность приема
  • Инструкция dji fpv на русском языке
  • Pioneer vsx c300 инструкция по применению
  • Цитруллин инструкция по применению цена отзывы аналоги таблетки