Руководство по PyGame

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

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

Содержание

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

Для создания игры необходимо ответить на 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))

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

  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».

Прокрутить вверх