В этом руководстве мы подробно познакомимся с библиотекой PyGame и посмотрим, как с ее помощью можно создавать простые игры.
Друзья, подписывайтесь на наш телеграм канал Pythonist. Там еще больше туториалов, задач и книг по Python
Содержание
- Основные аспекты перед началом
- Установка PyGame
- Простое PyGame-приложение
- Интерактивность
- Добавление функциональности
- Добавление изображений
- Работа со звуком
- Геометрические рисунки
- Шрифты и текст
- Модели ввода
- Логика сцены
- Заключение
Основные аспекты перед началом
Для создания игры необходимо ответить на 3 основных вопроса:
- Какую игру вы хотите создать?
- На каком языке вы хотите программировать?
- Для какой платформы вы хотите выпустить свою игру?
В большинстве случаев можно ответить на каждый из этих вопросов и найти идеальный фреймворк, соответствующий вашим требованиям. В других случаях это может оказаться невозможным.
PyGame вам подойдет, если на приведенные выше вопросы вы дали следующие ответы:
- Игра будет графической, но не 3D
- Вы хотите программировать на языке Python, который уже немного знаете
- Вы хотите создать клиентское приложение, которое потенциально может быть обернуто в отдельный исполняемый файл
Для других сценариев, особенно для 3D-игр, лучше подойдут другие языки и фреймворки.
Итак, давайте рассмотрим, как установить PyGame.
Установка PyGame
Установить 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))
Эта функция принимает три аргумента:
- Экземпляр поверхности, на которой нужно нарисовать прямоугольник.
- Кортеж
(red, green, blue)
, представляющий цвет для рисования. - Экземпляр
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».