🐍 Пишем Тетрис на Python с помощью библиотеки Pygame
Изучаем основные возможности Pygame в процессе создания lite-версии одной из самых популярных игр в мире.
Pygame – самое популярное решение для создания 2D игр на Python: библиотека включает в себя удобные инструменты для рисования, работы с изображениями, видео, спрайтами, шрифтами и звуком, для обработки событий клавиатуры и мыши. Главные преимущества Pygame – легкость обучения и скорость разработки. И хотя Pygame не используется для коммерческой разработки игр, это идеальный вариант для обучения начинающих. Здесь мы рассмотрим создание клона Тетриса. Полный код игры находится здесь.
Установка Pygame
Pygame не входит в стандартную поставку Python. Для установки достаточно выполнить в cmd команду py -m pip install -U pygame --user
. Полный размер пакета – чуть более 8 Мб.
Обзор проекта
Игровое поле представляет собой прямоугольный «стакан», в который сверху падают фигуры – стилизованные буквы L, S, Z, J, O, I и T.
Каждая буква состоит из 4 блоков:
Игрок управляет движением фигур вниз – двигает их вправо и влево (но не вверх), поворачивает на 90 градусов, при желании ускоряет падение нажатием/удержанием клавиши ↓
или мгновенно сбрасывает фигуры на дно нажатием Enter.
Приземлением считается момент, когда фигура падает на дно стакана или на элемент предыдущих фигур. После этого программа проверяет, вызвало ли приземление полное (без пустот) заполнение ряда элементов. Заполненные ряды (их может быть от 1 до 4 включительно) удаляются; находящиеся над ними элементы перемещаются вниз на столько рядов, сколько было заполнено и удалено; вверху стакана добавляется соответствующее количество пустых рядов. После удаления 10 заполненных рядов происходит переход на следующий уровень, и падение фигур ускоряется.
Основные параметры игры
Прежде всего импортируем нужные модули:
Затем определяем основные константы – кадровую частоту fps
, высоту и ширину окна программы, размер базового элемента фигур-букв block
(20 х 20 пикселей), параметры стакана, символ для обозначения пустых ячеек на игровом поле:
К размеру базового элемента block
привязываются остальные параметры игрового поля: ширина и высота стакана, к примеру, равны 10 и 20 блоков соответственно; каждый раз, когда игрок нажимает клавишу →
или ←
, фигура перемещается на 1 блок в нужную сторону.
Параметры side_freq
и down_freq
задают время, которое затрачивается на перемещение фигуры в сторону или вниз, если игрок удерживает клавишу нажатой:
Для размещения стакана и информационных надписей, а также для конвертации координат нам также понадобятся константы side_margin
и top_margin
– первая задает дистанцию между правой и левой сторонами окна программы и стаканом; вторая определяет расстояние между верхней границей стакана и окном:
Шаблоны и цвет фигур
Поскольку каждую фигуру-букву можно поворачивать на 90 градусов, все возможные варианты поворотов описаны в словаре figures
с помощью вложенных списков, элементы которых состоят из строк: символом x отмечены занятые ячейки, o – пустые. Количество вращений зависит от формы буквы: у O, к примеру, будет всего один вариант:
Поскольку каждая фигура состоит из 4 блоков, размер шаблона должен быть 5 х 5: fig_w, fig_h = 5, 5
.
Цвета фигур задаются двумя кортежами: colors
и lightcolors
. Последний включает чуть более светлые оттенки тех же цветов, что и colors
– для создания псевдо 2.5 D эффекта.
FPS и производительность
Pygame немилосердно нагружает процессор: можно столкнуться с ситуацией, когда небольшая игра с простейшей графикой использует CPU на 100% и нагревает достаточно мощный компьютер гораздо сильнее, чем 3D-шутер, написанный не на Python:). Проблема решается созданием объекта pygame.time.Clock()
, который вызывается в основном цикле программы с нужной fps
– кадровой частотой.
Шрифты
Модуль Pygame поставляется с одним шрифтом – freesansbold.ttf. При этом Pygame способен использовать любые другие шрифты – как установленные в системе, так и используемые только в рамках конкретного проекта. Чтобы получить список всех шрифтов, установленных в системе, достаточно выполнить pygame.font.get_fonts()
.
Подключить шрифт можно тремя способами:
Если шрифт установлен и находится в папке Windows\Fonts\, как, например, стандартный Arial – нужно воспользоваться методом pygame.font.SysFont: pygame.font.SysFont('arial', 15)
.
Если шрифт используется только в проекте – укажите к нему путь в pygame.font.Font('/User/Tetris/game.ttf', 18).
Чтобы не указывать путь, можно поместить шрифт в одну папку с проектом: pygame.font.Font('game.ttf', 18)
Пауза, экран паузы и прозрачность
Пауза в нашей игре возникает при нажатии пробела event.key == K_SPACE
. Чтобы показать «неактивность» программы во время паузы, нужно залить игровое поле цветом.
Заливку сплошным цветом реализовать очень просто, но полупрозрачную заставку сделать сложнее – как ни странно, метод draw
в Pygame до сих пор не поддерживает эту опцию. Есть несколько способов решения этой проблемы. Мы воспользуемся методом, который предусматривает создание дополнительной поверхности с попиксельным альфа-смешением, и последующую заливку экрана паузы цветом с наложением на поверхность окна игры:
Экран паузы также активируется в случае проигрыша, вместе с сообщением Игра закончена
.
Функция main()
Эта функция отвечает за создание нескольких дополнительных глобальных констант, инициализирует модуль Pygame, рисует стартовое окно игры, вызывает запуск Тетриса runTetris()
и в случае необходимости отображает сообщение о проигрыше:
Основной код Тетриса
Код игры располагается в функции runTetris()
:
При запуске вызывается функция рисования пустого стакана emptycup()
, а возможности движения влево, вправо и вниз устанавливаются на False
:
Эти значения будут изменяться на True
во время обработки событий клавиатуры, если будет установлено, что движение в нужном направлении возможно:
Главный цикл игры
Основной цикл обрабатывает все основные события, связанные с генерацией фигур, движением вниз и показом следующей фигуры:
После приземления каждой фигуры значение fallingFig
устанавливается на None
, после чего «следующая фигура» nextFig
, уже показанная в превью, становится «падающей» fallingFig
. Следующая фигура для превью генерируется функцией getNewFig()
. Каждая новая падающая фигура генерируется в позиции, которая расположена чуть выше стакана. Функция checkPos()
вернет False
, если стакан уже заполнен настолько, что движение вниз невозможно, после чего появится сообщение Игра закончена
. Эта же функция checkPos()
проверяет, находится ли фигура в границах стакана и не натыкается ли на элементы других фигур.
Управление движением
Обработка всех событий происходит в уже упомянутом цикле:
Цикл обрабатывает паузу и определяет момент, когда пользователь нажимает и отпускает клавиши со стрелками. Если клавиши →
, ←
и ↓
не нажаты, значения соответствующих переменных меняются на False
:
Управление движением фигур происходит в ветке elif event.type == KEYDOWN
: если нажата клавиша со стрелкой и функция checkPos()
возвращает True
, положение фигуры изменяется на один блок в соответствующем направлении:
Если пользователь не отпускает клавишу, следующее перемещение на один блок произойдет в соответствии со значениями side_freq
и down_freq
. Случай, когда пользователь удерживает клавишу в течение нескольких секунд, мы рассмотрим ниже.
При нажатии ↑
происходит вращение фигуры – варианты берутся из словаря figures
. Чтобы не получить ошибку IndexError: list index out of range, мы используем конструкцию, которая обнуляет индекс элемента, когда инкремент достигает максимального значения: fallingFig['rotation'] + 1) % len(figures[fallingFig['shape']]
. Если функция checkPos()
сообщает, что очередное вращение невозможно из-за того, что фигура натыкается на какой-то блок, нужно вернуться к предыдущему варианту из списка:
Для ускорения падения игрок нажимает и удерживает клавишу ↓
:
Если пользователь хочет мгновенно сбросить фигуру на дно, он может нажать Enter.
Цикл for
здесь определяет максимально низкую свободную позицию в стакане:
Удержание клавиш
Чтобы определить, удерживает ли пользователь клавишу движения, программа использует условия:
и
В этих условиях программа проверяет, нажимает ли пользователь клавишу дольше, чем 0.15 или 0.1 секунды – в этом случае условие соответствует True
, и фигура продолжит движение в заданном направлении. Эти условия избавляют игрока от необходимости многократно нажимать клавиши передвижения – для продолжения движения достаточно их удерживать.
Свободное падение
Если пользователь никак не вмешивается в управление фигурой, движение вниз происходит так:
Отрисовка, обновление окна игры и вывод надписей
Функцию runTetris()
завершает набор функций, обеспечивающих отрисовку игрового поля, вывод названия игры, падающей и следующих фигур, а также информационных надписей:
Вспомогательные функции
Функция txtObjects()
принимает текст, шрифт и цвет, и с помощью метода render()
возвращает готовые объекты Surface
(поверхность) и Rect
(прямоугольник). Эти объекты в дальнейшем обрабатываются методом blit
в функции showText()
, выводящей информационные надписи и название игры.
Выход из игры обеспечивает функция stopGame()
, в которой используется sys.exit()
из импортированного в начале кода модуля sys
.
За добавление фигур к содержимому стакана отвечает addToCup()
:
Пока фигура двигается, ее блоки не принадлежат к содержимому стакана – добавление происходит после приземления. Этот процесс мы рассмотрим чуть ниже.
Генерация и заполнение стакана
Пустой стакан создается функцией emptycup()
:
Пустой стакан представляет собой двумерный список, заполненный символами o. Занятые ячейки в дальнейшем принимают значения 0, 1, 2, 3 – в соответствии с индексами цветов фигур в кортеже colors
. Так выглядит массив cup после приземления нескольких фигур:
Функция checkPos()
следит за тем, чтобы падающая фигура оставалась в пределах игрового поля и не накладывалась на предыдущие. На примере слева фигура остается в допустимой области, на примере справа – ошибочно накладывается на предыдущую. Чтобы определить положение фигуры в стакане, нужно суммировать собственные координаты фигуры со «стаканными»:
Собственные координаты – (2, 1), (3, 1), (2, 2), (2, 3).
Стаканные координаты фигуры – (2, 3) на примере слева и (1, 11) на примере справа. Суммирование дает следующие результаты:
(2+2, 1+3), (3+2, 1+3), (2+2, 2+3), (2+2, 3+3) = (4, 4), (5, 4), (4, 5), (4, 6). Значит, фигура находится в пределах стакана и не наталкивается ни на один элемент предыдущих фигур.
На примере слева ситуация обратная:
(2+1, 2+11), (3+1, 2+11), (2+1, 3+11), (2+1, 4+11) = (3, 13), (4, 13), (3, 14), (3, 15) – две последние координаты в массиве cup уже заняты блоками предыдущих фигур. Именно такие ситуации и предотвращают checkPos()
вместе с incup()
:
Удаление заполненных рядов и сдвиг блоков вниз
За обнаружение и удаление заполненных рядов отвечает функция clearCompleted()
вместе со вспомогательной isCompleted()
. Если isCompleted()
возвращает True
, программе нужно последовательно переместить вниз все ряды, располагающиеся над удаляемым, после чего заполнить нулевой ряд empty-значениями о
:
Переменная y
после удаления одного ряда продолжает указывать на его номер – это нужно для того, чтобы перейти к удалению других заполненных рядов, если они смещаются на место только что удаленного. В случае если других заполненных рядов пока нет, происходит уменьшение у
.
Рисование блоков фигур
Каждая фигура состоит из 4 элементов – блоков. Блоки рисует функция drawBlock(),
которая получает координаты из convertCoords()
:
Для рисования блоков используются примитивы rect
(прямоугольник) и circle
(круг). При желании верхний квадрат можно конвертировать в поверхность (Surface
), после чего наложить на эту поверхность изображение или текстовый символ. Функция drawBlock()
также используется в drawnextFig()
для вывода следующей фигуры справа от игрового поля.
Заключение
Напоминаем, что полный код игры можно скачать здесь. Это полностью функциональный Тетрис с простым интерфейсом. Pygame предоставляет немало дополнительных возможностей для дополнения программы: к примеру, в игру можно добавить звуковые эффекты, диалоговое окно для закрытия, фоновое изображение, запись рекордов в файл. Если какие-то моменты остались неясными – задавайте вопросы в комментариях.
Материалы по теме
- 🕵 Пишем кейлоггер на Python для Windows за 5 минут
- 🐍 Создание интерактивных панелей с Streamlit и Python
- 🐍 Как сделать сайт на Python за 5 минут с помощью SSG-генератора Pelican