Изучаем основные возможности 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 блоков: Фигуры и варианты поворотов описаны в 2D-списках 5 х 5 Игрок управляет движением фигур вниз – двигает их вправо и влево (но не вверх), поворачивает на 90 градусов, при желании ускоряет падение нажатием/удержанием клавиши ↓ или мгновенно сбрасывает фигуры на дно нажатием Enter. Приземлением считается момент, когда фигура падает на дно стакана или на элемент предыдущих фигур. После этого программа проверяет, вызвало ли приземление полное (без пустот) заполнение ряда элементов. Заполненные ряды (их может быть от 1 до 4 включительно) удаляются; находящиеся над ними элементы перемещаются вниз на столько рядов, сколько было заполнено и удалено; вверху стакана добавляется соответствующее количество пустых рядов. После удаления 10 заполненных рядов происходит переход на следующий уровень, и падение фигур ускоряется. Все экраны игры Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста» Интересно, перейти к каналу Основные параметры игры Прежде всего импортируем нужные модули: import pygame as pg import random, time, sys from pygame.locals import * Затем определяем основные константы – кадровую частоту fps, высоту и ширину окна программы, размер базового элемента фигур-букв block (20 х 20 пикселей), параметры стакана, символ для обозначения пустых ячеек на игровом поле: fps = 25 window_w, window_h = 600, 500 block, cup_h, cup_w = 20, 20, 10 К размеру базового элемента block привязываются остальные параметры игрового поля: ширина и высота стакана, к примеру, равны 10 и 20 блоков соответственно; каждый раз, когда игрок нажимает клавишу → или ←, фигура перемещается на 1 блок в нужную сторону. Параметры side_freq и down_freq задают время, которое затрачивается на перемещение фигуры в сторону или вниз, если игрок удерживает клавишу нажатой: side_freq, down_freq = 0.15, 0.1 Для размещения стакана и информационных надписей, а также для конвертации координат нам также понадобятся константы side_margin и top_margin – первая задает дистанцию между правой и левой сторонами окна программы и стаканом; вторая определяет расстояние между верхней границей стакана и окном: side_margin = int((window_w - cup_w * block) / 2) top_margin = window_h - (cup_h * block) - 5 Шаблоны и цвет фигур Поскольку каждую фигуру-букву можно поворачивать на 90 градусов, все возможные варианты поворотов описаны в словаре figures с помощью вложенных списков, элементы которых состоят из строк: символом x отмечены занятые ячейки, o – пустые. Количество вращений зависит от формы буквы: у O, к примеру, будет всего один вариант: 'O': [['ooooo', 'ooooo', 'oxxoo', 'oxxoo', 'ooooo']] Поскольку каждая фигура состоит из 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(). Подключить шрифт можно тремя способами: Если шрифт установлен и находится в папке WindowsFonts, как, например, стандартный 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 до сих пор не поддерживает эту опцию. Есть несколько способов решения этой проблемы. Мы воспользуемся методом, который предусматривает создание дополнительной поверхности с попиксельным альфа-смешением, и последующую заливку экрана паузы цветом с наложением на поверхность окна игры: pause = pg.Surface((600, 500), pg.SRCALPHA) pause.fill((0, 0, 255, 127)) display_surf.blit(pause, (0, 0)) Экран паузы также активируется в случае проигрыша, вместе с сообщением Игра закончена. Функция main() Эта функция отвечает за создание нескольких дополнительных глобальных констант, инициализирует модуль Pygame, рисует стартовое окно игры, вызывает запуск Тетриса runTetris() и в случае необходимости отображает сообщение о проигрыше: def main(): global fps_clock, display_surf, basic_font, big_font pg.init() fps_clock = pg.time.Clock() display_surf = pg.display.set_mode((window_w, window_h)) basic_font = pg.font.Font('freesansbold.ttf', 18) big_font = pg.font.Font('freesansbold.ttf', 45) pg.display.set_caption('Тетрис Lite') showText('Тетрис Lite') while True: # начинаем игру runTetris() pauseScreen() showText('Игра закончена') Основной код Тетриса Код игры располагается в функции runTetris(): def runTetris(): cup = emptycup() last_move_down = time.time() last_side_move = time.time() last_fall = time.time() going_down = False going_left = False going_right = False points = 0 level, fall_speed = calcSpeed(points) fallingFig = getNewFig() nextFig = getNewFig() При запуске вызывается функция рисования пустого стакана emptycup(), а возможности движения влево, вправо и вниз устанавливаются на False: going_down = False going_left = False going_right = False Эти значения будут изменяться на True во время обработки событий клавиатуры, если будет установлено, что движение в нужном направлении возможно: for event in pg.event.get(): if event.type == KEYUP: Главный цикл игры Основной цикл обрабатывает все основные события, связанные с генерацией фигур, движением вниз и показом следующей фигуры: while True: if fallingFig == None: fallingFig = nextFig nextFig = getNewFig() last_fall = time.time() if not checkPos(cup, fallingFig): return quitGame() После приземления каждой фигуры значение fallingFig устанавливается на None, после чего «следующая фигура» nextFig, уже показанная в превью, становится «падающей» fallingFig. Следующая фигура для превью генерируется функцией getNewFig(). Каждая новая падающая фигура генерируется в позиции, которая расположена чуть выше стакана. Функция checkPos() вернет False, если стакан уже заполнен настолько, что движение вниз невозможно, после чего появится сообщение Игра закончена. Эта же функция checkPos() проверяет, находится ли фигура в границах стакана и не натыкается ли на элементы других фигур. Управление движением Обработка всех событий происходит в уже упомянутом цикле: for event in pg.event.get(): if event.type == KEYUP: Цикл обрабатывает паузу и определяет момент, когда пользователь нажимает и отпускает клавиши со стрелками. Если клавиши →, ← и ↓ не нажаты, значения соответствующих переменных меняются на False: elif event.key == K_LEFT: going_left = False elif event.key == K_RIGHT: going_right = False elif event.key == K_DOWN: going_down = False Управление движением фигур происходит в ветке elif event.type == KEYDOWN: если нажата клавиша со стрелкой и функция checkPos() возвращает True, положение фигуры изменяется на один блок в соответствующем направлении: if event.key == K_LEFT and checkPos(cup, fallingFig, adjX=-1): fallingFig['x'] -= 1 going_left = True going_right = False last_side_move = time.time() Если пользователь не отпускает клавишу, следующее перемещение на один блок произойдет в соответствии со значениями side_freq и down_freq. Случай, когда пользователь удерживает клавишу в течение нескольких секунд, мы рассмотрим ниже. При нажатии ↑ происходит вращение фигуры – варианты берутся из словаря figures. Чтобы не получить ошибку IndexError: list index out of range, мы используем конструкцию, которая обнуляет индекс элемента, когда инкремент достигает максимального значения: fallingFig['rotation'] + 1) % len(figures[fallingFig['shape']]. Если функция checkPos() сообщает, что очередное вращение невозможно из-за того, что фигура натыкается на какой-то блок, нужно вернуться к предыдущему варианту из списка: if not checkPos(cup, fallingFig): fallingFig['rotation'] = (fallingFig['rotation'] - 1) % len(figures[fallingFig['shape']]) Для ускорения падения игрок нажимает и удерживает клавишу ↓: elif event.key == K_DOWN: going_down = True if checkPos(cup, fallingFig, adjY=1): fallingFig['y'] += 1 last_move_down = time.time() Если пользователь хочет мгновенно сбросить фигуру на дно, он может нажать Enter. Цикл for здесь определяет максимально низкую свободную позицию в стакане: elif event.key == K_RETURN: going_down = False going_left = False going_right = False for i in range(1, cup_h): if not checkPos(cup, fallingFig, adjY=i): break fallingFig['y'] += i - 1 Удержание клавиш Чтобы определить, удерживает ли пользователь клавишу движения, программа использует условия: if (going_left or going_right) and time.time() - last_side_move > side_freq: и if going_down and time.time() - last_move_down > down_freq and checkPos(cup, fallingFig, adjY=1): В этих условиях программа проверяет, нажимает ли пользователь клавишу дольше, чем 0.15 или 0.1 секунды – в этом случае условие соответствует True, и фигура продолжит движение в заданном направлении. Эти условия избавляют игрока от необходимости многократно нажимать клавиши передвижения – для продолжения движения достаточно их удерживать. Свободное падение Если пользователь никак не вмешивается в управление фигурой, движение вниз происходит так: if time.time() - last_fall > fall_speed: # свободное падение фигуры if not checkPos(cup, fallingFig, adjY=1): # проверка "приземления" фигуры addToCup(cup, fallingFig) # фигура приземлилась, добавляем ее в содержимое стакана points += clearCompleted(cup) level, fall_speed = calcSpeed(points) fallingFig = None else: # фигура пока не приземлилась, продолжаем движение вниз fallingFig['y'] += 1 last_fall = time.time() Отрисовка, обновление окна игры и вывод надписей Функцию runTetris() завершает набор функций, обеспечивающих отрисовку игрового поля, вывод названия игры, падающей и следующих фигур, а также информационных надписей: display_surf.fill(bg_color) drawTitle() gamecup(cup) drawInfo(points, level) drawnextFig(nextFig) if fallingFig != None: drawFig(fallingFig) pg.display.update() fps_clock.tick(fps) Вспомогательные функции Функция txtObjects() принимает текст, шрифт и цвет, и с помощью метода render() возвращает готовые объекты Surface (поверхность) и Rect (прямоугольник). Эти объекты в дальнейшем обрабатываются методом blit в функции showText(), выводящей информационные надписи и название игры. Выход из игры обеспечивает функция stopGame(), в которой используется sys.exit() из импортированного в начале кода модуля sys. За добавление фигур к содержимому стакана отвечает addToCup(): def addToCup(cup, fig): for x in range(fig_w): for y in range(fig_h): if figures[fig['shape']][fig['rotation']][y][x] != empty: cup[x + fig['x']][y + fig['y']] = fig['color'] Пока фигура двигается, ее блоки не принадлежат к содержимому стакана – добавление происходит после приземления. Этот процесс мы рассмотрим чуть ниже. Генерация и заполнение стакана Пустой стакан создается функцией emptycup(): def emptycup(): cup = [] for i in range(cup_w): cup.append([empty] * cup_h) return cup Пустой стакан представляет собой двумерный список, заполненный символами o. Занятые ячейки в дальнейшем принимают значения 0, 1, 2, 3 – в соответствии с индексами цветов фигур в кортеже colors. Так выглядит массив cup после приземления нескольких фигур: ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 2, 2, 1, 1] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 3, 2, 2, 1, 1] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 2, 2, 'o', 'o', 'o', 1] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 3, 2, 2, 0, 2, 1] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 0, 2, 0, 0, 2, 'o'] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 0, 0, 0, 0, 0, 0, 1, 'o'] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 0, 0, 2, 1, 0, 1, 1] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 2, 2, 1, 1, 1, 'o'] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 1, 0, 0, 0, 0, 0] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 1, 1, 1, 2, 2, 2, 0, 0] Допустимое и недопустимое положение фигуры в стакане Функция 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(): if not incup(x + fig['x'] + adjX, y + fig['y'] + adjY): return False if cup[x + fig['x'] + adjX][y + fig['y'] + adjY] != empty: return False Удаление заполненных рядов и сдвиг блоков вниз За обнаружение и удаление заполненных рядов отвечает функция clearCompleted() вместе со вспомогательной isCompleted(). Если isCompleted() возвращает True, программе нужно последовательно переместить вниз все ряды, располагающиеся над удаляемым, после чего заполнить нулевой ряд empty-значениями о: def clearCompleted(cup): # Удаление заполенных рядов и сдвиг верхних рядов вниз removed_lines = 0 y = cup_h - 1 while y >= 0: if isCompleted(cup, y): for pushDownY in range(y, 0, -1): for x in range(cup_w): cup[x][pushDownY] = cup[x][pushDownY-1] for x in range(cup_w): cup[x][0] = empty removed_lines += 1 else: y -= 1 return removed_lines Переменная указывает на удаленный ряд Переменная y после удаления одного ряда продолжает указывать на его номер – это нужно для того, чтобы перейти к удалению других заполненных рядов, если они смещаются на место только что удаленного. В случае если других заполненных рядов пока нет, происходит уменьшение у. Рисование блоков фигур Каждая фигура состоит из 4 элементов – блоков. Блоки рисует функция drawBlock(), которая получает координаты из convertCoords(): def drawBlock(block_x, block_y, color, pixelx=None, pixely=None): #отрисовка квадратных блоков, из которых состоят фигуры if color == empty: return if pixelx == None and pixely == None: pixelx, pixely = convertCoords(block_x, block_y) pg.draw.rect(display_surf, colors[color], (pixelx + 1, pixely + 1, block - 1, block - 1), 0, 3) pg.draw.rect(display_surf, lightcolors[color], (pixelx + 1, pixely + 1, block - 4, block - 4), 0, 3) pg.draw.circle(display_surf, colors[color], (pixelx + block / 2, pixely + block / 2), 5) Для рисования блоков используются примитивы rect (прямоугольник) и circle (круг). При желании верхний квадрат можно конвертировать в поверхность (Surface), после чего наложить на эту поверхность изображение или текстовый символ. Функция drawBlock() также используется в drawnextFig() для вывода следующей фигуры справа от игрового поля. Заключение Напоминаем, что полный код игры можно скачать здесь. Это полностью функциональный Тетрис с простым интерфейсом. Pygame предоставляет немало дополнительных возможностей для дополнения программы: к примеру, в игру можно добавить звуковые эффекты, диалоговое окно для закрытия, фоновое изображение, запись рекордов в файл. Если какие-то моменты остались неясными – задавайте вопросы в комментариях. Материалы по теме 🕵 Пишем кейлоггер на Python для Windows за 5 минут 🐍 Создание интерактивных панелей с Streamlit и Python 🐍 Как сделать сайт на Python за 5 минут с помощью SSG-генератора Pelican
Pygame – самое популярное решение для создания 2D игр на Python: библиотека включает в себя удобные инструменты для рисования, работы с изображениями, видео, спрайтами, шрифтами и звуком, для обработки событий клавиатуры и мыши. Главные преимущества Pygame – легкость обучения и скорость разработки. И хотя Pygame не используется для коммерческой разработки игр, это идеальный вариант для обучения начинающих. Здесь мы рассмотрим создание клона Тетриса. Полный код игры находится здесь.
Pygame не входит в стандартную поставку Python. Для установки достаточно выполнить в cmd команду py -m pip install -U pygame --user. Полный размер пакета – чуть более 8 Мб.
py -m pip install -U pygame --user
Основной экран Тетриса
Игровое поле представляет собой прямоугольный «стакан», в который сверху падают фигуры – стилизованные буквы L, S, Z, J, O, I и T.
Буквы-фигуры в Тетрисе
Каждая буква состоит из 4 блоков:
Фигуры и варианты поворотов описаны в 2D-списках 5 х 5
Игрок управляет движением фигур вниз – двигает их вправо и влево (но не вверх), поворачивает на 90 градусов, при желании ускоряет падение нажатием/удержанием клавиши ↓ или мгновенно сбрасывает фигуры на дно нажатием Enter.
↓
Приземлением считается момент, когда фигура падает на дно стакана или на элемент предыдущих фигур. После этого программа проверяет, вызвало ли приземление полное (без пустот) заполнение ряда элементов. Заполненные ряды (их может быть от 1 до 4 включительно) удаляются; находящиеся над ними элементы перемещаются вниз на столько рядов, сколько было заполнено и удалено; вверху стакана добавляется соответствующее количество пустых рядов. После удаления 10 заполненных рядов происходит переход на следующий уровень, и падение фигур ускоряется.
Все экраны игры Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста» Интересно, перейти к каналу
Прежде всего импортируем нужные модули:
import pygame as pg import random, time, sys from pygame.locals import *
Затем определяем основные константы – кадровую частоту fps, высоту и ширину окна программы, размер базового элемента фигур-букв block (20 х 20 пикселей), параметры стакана, символ для обозначения пустых ячеек на игровом поле:
fps
block
fps = 25 window_w, window_h = 600, 500 block, cup_h, cup_w = 20, 20, 10
К размеру базового элемента block привязываются остальные параметры игрового поля: ширина и высота стакана, к примеру, равны 10 и 20 блоков соответственно; каждый раз, когда игрок нажимает клавишу → или ←, фигура перемещается на 1 блок в нужную сторону.
→
←
Параметры side_freq и down_freq задают время, которое затрачивается на перемещение фигуры в сторону или вниз, если игрок удерживает клавишу нажатой:
side_freq
down_freq
side_freq, down_freq = 0.15, 0.1
Для размещения стакана и информационных надписей, а также для конвертации координат нам также понадобятся константы side_margin и top_margin – первая задает дистанцию между правой и левой сторонами окна программы и стаканом; вторая определяет расстояние между верхней границей стакана и окном:
side_margin
top_margin
side_margin = int((window_w - cup_w * block) / 2) top_margin = window_h - (cup_h * block) - 5
Поскольку каждую фигуру-букву можно поворачивать на 90 градусов, все возможные варианты поворотов описаны в словаре figures с помощью вложенных списков, элементы которых состоят из строк: символом x отмечены занятые ячейки, o – пустые. Количество вращений зависит от формы буквы: у O, к примеру, будет всего один вариант:
figures
'O': [['ooooo', 'ooooo', 'oxxoo', 'oxxoo', 'ooooo']]
Поскольку каждая фигура состоит из 4 блоков, размер шаблона должен быть 5 х 5: fig_w, fig_h = 5, 5.
fig_w, fig_h = 5, 5
Цвета фигур задаются двумя кортежами: colors и lightcolors. Последний включает чуть более светлые оттенки тех же цветов, что и colors – для создания псевдо 2.5 D эффекта.
colors
lightcolors
Pygame немилосердно нагружает процессор: можно столкнуться с ситуацией, когда небольшая игра с простейшей графикой использует CPU на 100% и нагревает достаточно мощный компьютер гораздо сильнее, чем 3D-шутер, написанный не на Python:). Проблема решается созданием объекта pygame.time.Clock(), который вызывается в основном цикле программы с нужной fps – кадровой частотой.
pygame.time.Clock()
Модуль Pygame поставляется с одним шрифтом – freesansbold.ttf. При этом Pygame способен использовать любые другие шрифты – как установленные в системе, так и используемые только в рамках конкретного проекта. Чтобы получить список всех шрифтов, установленных в системе, достаточно выполнить pygame.font.get_fonts().
pygame.font.get_fonts()
Подключить шрифт можно тремя способами:
Если шрифт установлен и находится в папке WindowsFonts, как, например, стандартный Arial – нужно воспользоваться методом pygame.font.SysFont: pygame.font.SysFont('arial', 15).
pygame.font.SysFont('arial', 15)
Если шрифт используется только в проекте – укажите к нему путь в pygame.font.Font('/User/Tetris/game.ttf', 18).
pygame.font.Font('/User/Tetris/game.ttf', 18).
Чтобы не указывать путь, можно поместить шрифт в одну папку с проектом: pygame.font.Font('game.ttf', 18)
pygame.font.Font('game.ttf', 18)
Пауза в нашей игре возникает при нажатии пробела event.key == K_SPACE. Чтобы показать «неактивность» программы во время паузы, нужно залить игровое поле цветом.
event.key == K_SPACE
Во время паузы экран заливается полупрозрачным синим цветом
Заливку сплошным цветом реализовать очень просто, но полупрозрачную заставку сделать сложнее – как ни странно, метод draw в Pygame до сих пор не поддерживает эту опцию. Есть несколько способов решения этой проблемы. Мы воспользуемся методом, который предусматривает создание дополнительной поверхности с попиксельным альфа-смешением, и последующую заливку экрана паузы цветом с наложением на поверхность окна игры:
draw
pause = pg.Surface((600, 500), pg.SRCALPHA) pause.fill((0, 0, 255, 127)) display_surf.blit(pause, (0, 0))
Экран паузы также активируется в случае проигрыша, вместе с сообщением Игра закончена.
Игра закончена
Эта функция отвечает за создание нескольких дополнительных глобальных констант, инициализирует модуль Pygame, рисует стартовое окно игры, вызывает запуск Тетриса runTetris() и в случае необходимости отображает сообщение о проигрыше:
runTetris()
def main(): global fps_clock, display_surf, basic_font, big_font pg.init() fps_clock = pg.time.Clock() display_surf = pg.display.set_mode((window_w, window_h)) basic_font = pg.font.Font('freesansbold.ttf', 18) big_font = pg.font.Font('freesansbold.ttf', 45) pg.display.set_caption('Тетрис Lite') showText('Тетрис Lite') while True: # начинаем игру runTetris() pauseScreen() showText('Игра закончена')
Код игры располагается в функции runTetris():
def runTetris(): cup = emptycup() last_move_down = time.time() last_side_move = time.time() last_fall = time.time() going_down = False going_left = False going_right = False points = 0 level, fall_speed = calcSpeed(points) fallingFig = getNewFig() nextFig = getNewFig()
При запуске вызывается функция рисования пустого стакана emptycup(), а возможности движения влево, вправо и вниз устанавливаются на False:
emptycup()
False
going_down = False going_left = False going_right = False
Эти значения будут изменяться на True во время обработки событий клавиатуры, если будет установлено, что движение в нужном направлении возможно:
True
for event in pg.event.get(): if event.type == KEYUP:
Основной цикл обрабатывает все основные события, связанные с генерацией фигур, движением вниз и показом следующей фигуры:
while True: if fallingFig == None: fallingFig = nextFig nextFig = getNewFig() last_fall = time.time() if not checkPos(cup, fallingFig): return quitGame()
После приземления каждой фигуры значение fallingFig устанавливается на None, после чего «следующая фигура» nextFig, уже показанная в превью, становится «падающей» fallingFig. Следующая фигура для превью генерируется функцией getNewFig(). Каждая новая падающая фигура генерируется в позиции, которая расположена чуть выше стакана. Функция checkPos() вернет False, если стакан уже заполнен настолько, что движение вниз невозможно, после чего появится сообщение Игра закончена. Эта же функция checkPos() проверяет, находится ли фигура в границах стакана и не натыкается ли на элементы других фигур.
fallingFig
None
nextFig
getNewFig()
checkPos()
Обработка всех событий происходит в уже упомянутом цикле:
Цикл обрабатывает паузу и определяет момент, когда пользователь нажимает и отпускает клавиши со стрелками. Если клавиши →, ← и ↓ не нажаты, значения соответствующих переменных меняются на False:
elif event.key == K_LEFT: going_left = False elif event.key == K_RIGHT: going_right = False elif event.key == K_DOWN: going_down = False
Управление движением фигур происходит в ветке elif event.type == KEYDOWN: если нажата клавиша со стрелкой и функция checkPos() возвращает True, положение фигуры изменяется на один блок в соответствующем направлении:
elif event.type == KEYDOWN
if event.key == K_LEFT and checkPos(cup, fallingFig, adjX=-1): fallingFig['x'] -= 1 going_left = True going_right = False last_side_move = time.time()
Если пользователь не отпускает клавишу, следующее перемещение на один блок произойдет в соответствии со значениями side_freq и down_freq. Случай, когда пользователь удерживает клавишу в течение нескольких секунд, мы рассмотрим ниже.
При нажатии ↑ происходит вращение фигуры – варианты берутся из словаря figures. Чтобы не получить ошибку IndexError: list index out of range, мы используем конструкцию, которая обнуляет индекс элемента, когда инкремент достигает максимального значения: fallingFig['rotation'] + 1) % len(figures[fallingFig['shape']]. Если функция checkPos() сообщает, что очередное вращение невозможно из-за того, что фигура натыкается на какой-то блок, нужно вернуться к предыдущему варианту из списка:
↑
fallingFig['rotation'] + 1) % len(figures[fallingFig['shape']]
if not checkPos(cup, fallingFig): fallingFig['rotation'] = (fallingFig['rotation'] - 1) % len(figures[fallingFig['shape']])
Для ускорения падения игрок нажимает и удерживает клавишу ↓:
elif event.key == K_DOWN: going_down = True if checkPos(cup, fallingFig, adjY=1): fallingFig['y'] += 1 last_move_down = time.time()
Если пользователь хочет мгновенно сбросить фигуру на дно, он может нажать Enter. Цикл for здесь определяет максимально низкую свободную позицию в стакане:
Enter.
for
elif event.key == K_RETURN: going_down = False going_left = False going_right = False for i in range(1, cup_h): if not checkPos(cup, fallingFig, adjY=i): break fallingFig['y'] += i - 1
Чтобы определить, удерживает ли пользователь клавишу движения, программа использует условия:
if (going_left or going_right) and time.time() - last_side_move > side_freq:
и
if going_down and time.time() - last_move_down > down_freq and checkPos(cup, fallingFig, adjY=1):
В этих условиях программа проверяет, нажимает ли пользователь клавишу дольше, чем 0.15 или 0.1 секунды – в этом случае условие соответствует True, и фигура продолжит движение в заданном направлении. Эти условия избавляют игрока от необходимости многократно нажимать клавиши передвижения – для продолжения движения достаточно их удерживать.
Если пользователь никак не вмешивается в управление фигурой, движение вниз происходит так:
if time.time() - last_fall > fall_speed: # свободное падение фигуры if not checkPos(cup, fallingFig, adjY=1): # проверка "приземления" фигуры addToCup(cup, fallingFig) # фигура приземлилась, добавляем ее в содержимое стакана points += clearCompleted(cup) level, fall_speed = calcSpeed(points) fallingFig = None else: # фигура пока не приземлилась, продолжаем движение вниз fallingFig['y'] += 1 last_fall = time.time()
Функцию runTetris() завершает набор функций, обеспечивающих отрисовку игрового поля, вывод названия игры, падающей и следующих фигур, а также информационных надписей:
display_surf.fill(bg_color) drawTitle() gamecup(cup) drawInfo(points, level) drawnextFig(nextFig) if fallingFig != None: drawFig(fallingFig) pg.display.update() fps_clock.tick(fps)
Функция txtObjects() принимает текст, шрифт и цвет, и с помощью метода render() возвращает готовые объекты Surface (поверхность) и Rect (прямоугольник). Эти объекты в дальнейшем обрабатываются методом blit в функции showText(), выводящей информационные надписи и название игры.
txtObjects()
render()
Surface
Rect
blit
showText()
Выход из игры обеспечивает функция stopGame(), в которой используется sys.exit() из импортированного в начале кода модуля sys.
stopGame()
sys.exit()
sys
За добавление фигур к содержимому стакана отвечает addToCup():
addToCup()
def addToCup(cup, fig): for x in range(fig_w): for y in range(fig_h): if figures[fig['shape']][fig['rotation']][y][x] != empty: cup[x + fig['x']][y + fig['y']] = fig['color']
Пока фигура двигается, ее блоки не принадлежат к содержимому стакана – добавление происходит после приземления. Этот процесс мы рассмотрим чуть ниже.
Пустой стакан создается функцией emptycup():
def emptycup(): cup = [] for i in range(cup_w): cup.append([empty] * cup_h) return cup
Пустой стакан представляет собой двумерный список, заполненный символами o. Занятые ячейки в дальнейшем принимают значения 0, 1, 2, 3 – в соответствии с индексами цветов фигур в кортеже colors. Так выглядит массив cup после приземления нескольких фигур:
['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 2, 2, 1, 1] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 3, 2, 2, 1, 1] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 2, 2, 'o', 'o', 'o', 1] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 3, 2, 2, 0, 2, 1] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 0, 2, 0, 0, 2, 'o'] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 0, 0, 0, 0, 0, 0, 1, 'o'] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 0, 0, 2, 1, 0, 1, 1] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 2, 2, 1, 1, 1, 'o'] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 1, 0, 0, 0, 0, 0] ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 1, 1, 1, 2, 2, 2, 0, 0]
Допустимое и недопустимое положение фигуры в стакане
Функция 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():
incup()
if not incup(x + fig['x'] + adjX, y + fig['y'] + adjY): return False if cup[x + fig['x'] + adjX][y + fig['y'] + adjY] != empty: return False
За обнаружение и удаление заполненных рядов отвечает функция clearCompleted() вместе со вспомогательной isCompleted(). Если isCompleted() возвращает True, программе нужно последовательно переместить вниз все ряды, располагающиеся над удаляемым, после чего заполнить нулевой ряд empty-значениями о:
clearCompleted()
isCompleted()
о
def clearCompleted(cup): # Удаление заполенных рядов и сдвиг верхних рядов вниз removed_lines = 0 y = cup_h - 1 while y >= 0: if isCompleted(cup, y): for pushDownY in range(y, 0, -1): for x in range(cup_w): cup[x][pushDownY] = cup[x][pushDownY-1] for x in range(cup_w): cup[x][0] = empty removed_lines += 1 else: y -= 1 return removed_lines
Переменная указывает на удаленный ряд
Переменная y после удаления одного ряда продолжает указывать на его номер – это нужно для того, чтобы перейти к удалению других заполненных рядов, если они смещаются на место только что удаленного. В случае если других заполненных рядов пока нет, происходит уменьшение у.
y
у
Каждая фигура состоит из 4 элементов – блоков. Блоки рисует функция drawBlock(), которая получает координаты из convertCoords():
drawBlock(),
convertCoords()
def drawBlock(block_x, block_y, color, pixelx=None, pixely=None): #отрисовка квадратных блоков, из которых состоят фигуры if color == empty: return if pixelx == None and pixely == None: pixelx, pixely = convertCoords(block_x, block_y) pg.draw.rect(display_surf, colors[color], (pixelx + 1, pixely + 1, block - 1, block - 1), 0, 3) pg.draw.rect(display_surf, lightcolors[color], (pixelx + 1, pixely + 1, block - 4, block - 4), 0, 3) pg.draw.circle(display_surf, colors[color], (pixelx + block / 2, pixely + block / 2), 5)
Для рисования блоков используются примитивы rect (прямоугольник) и circle (круг). При желании верхний квадрат можно конвертировать в поверхность (Surface), после чего наложить на эту поверхность изображение или текстовый символ. Функция drawBlock() также используется в drawnextFig() для вывода следующей фигуры справа от игрового поля.
rect
circle
drawBlock()
drawnextFig()
Напоминаем, что полный код игры можно скачать здесь. Это полностью функциональный Тетрис с простым интерфейсом. Pygame предоставляет немало дополнительных возможностей для дополнения программы: к примеру, в игру можно добавить звуковые эффекты, диалоговое окно для закрытия, фоновое изображение, запись рекордов в файл. Если какие-то моменты остались неясными – задавайте вопросы в комментариях.
ΠΠ°Ρ Π°Π΄ΡΠ΅Ρ email Π½Π΅ Π±ΡΠ΄Π΅Ρ ΠΎΠΏΡΠ±Π»ΠΈΠΊΠΎΠ²Π°Π½. ΠΠ±ΡΠ·Π°ΡΠ΅Π»ΡΠ½ΡΠ΅ ΠΏΠΎΠ»Ρ ΠΏΠΎΠΌΠ΅ΡΠ΅Π½Ρ *
Π‘ΠΎΡ ΡΠ°Π½ΠΈΡΡ ΠΌΠΎΡ ΠΈΠΌΡ, email ΠΈ Π°Π΄ΡΠ΅Ρ ΡΠ°ΠΉΡΠ° Π² ΡΡΠΎΠΌ Π±ΡΠ°ΡΠ·Π΅ΡΠ΅ Π΄Π»Ρ ΠΏΠΎΡΠ»Π΅Π΄ΡΡΡΠΈΡ ΠΌΠΎΠΈΡ ΠΊΠΎΠΌΠΌΠ΅Π½ΡΠ°ΡΠΈΠ΅Π².
Δ
ΠΡΠΎΡ ΡΠ°ΠΉΡ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅Ρ Akismet Π΄Π»Ρ Π±ΠΎΡΡΠ±Ρ ΡΠΎ ΡΠΏΠ°ΠΌΠΎΠΌ. Π£Π·Π½Π°ΠΉΡΠ΅, ΠΊΠ°ΠΊ ΠΎΠ±ΡΠ°Π±Π°ΡΡΠ²Π°ΡΡΡΡ Π²Π°ΡΠΈ Π΄Π°Π½Π½ΡΠ΅ ΠΊΠΎΠΌΠΌΠ΅Π½ΡΠ°ΡΠΈΠ΅Π².