Что такое Pygame? Каркас приложения, FPS

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

  • рисования графических объектов;
  • отслеживания различных событий (клавиатуры, мыши, таймера и т.п);
  • отслеживания и изменения состояний объектов (создание анимации, контроль столкновений);
  • быстрой отрисовки изменений на экране устройства пользователя;
  • работы со звуковыми эффектами.

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

Вообще, PyGame – это Python-обертка над С++ - библиотекой Simple Directmedia Layer (SDL) и имеет большое интернет-сообщество с официальным сайтом:

https://www.pygame.org

и подробной документацией:

https://www.pygame.org/docs/

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

Установка PyGame

Первым делом нам нужно установить PyGame. Делается это очень просто. Под ОС Windows следует выполнить команду:

pip install pygame

(похожим образом происходит установка и в других ОС). Далее, непосредственно в Python, создайте новый проект и запишите две строчки:

import pygame
pygame.init()

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

Каркас приложения

Давайте теперь реализуем простейшее минимальное приложение на PyGame. Первые две строчки у нас уже есть. Фактически, первая выполняет импорт основного модуля, а вторая импортирует массу других необходимых расширений (иначе бы нам каждый раз приходилось бы делать это вручную). То есть, мы здесь просто подключили саму библиотеку. Что дальше? Далее сформируем окно самого приложения с помощью следующей функции:

pygame.display.set_mode((600, 400))

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

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

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

while 1:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
           exit()

Смотрите, здесь задан «вечный цикл» и внутри еще один цикл for, который выбирает события из очереди событий. Сама очередь – это итерируемый объект, к которому мы обращаемся с помощью конструкции pygame.event.get(). В результате переменная event будет ссылаться на текущее событие из этой очереди. Каждое событие – это объект, который содержит наборы различных свойств. В частности, все события имеют свойство type, которое мы и проверяем. Если оно равно константе pygame.QUIT, значит, пришло событие закрытия приложения. И здесь, в качестве обработчика выступает стандартная Python-функция exit, которая завершает наше приложение. Теперь, при запуске программы мы видим окно на экране, пока не закроем его. При закрытии срабатывает событие pygame.QUIT и программа завершается. Если, в качестве примера, вместо exit написать pass, то наше окно закрываться не будет, т.к. мы фактически удалили обработчик и ничего не делаем при возникновении этого события.

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

while 1:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
 
print("Программа продолжает работу")

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

flRunning = True
while flRunning:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            flRunning = False
 
print("Программа продолжает работу")

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

Задержка и формирование нужного FPS

Однако в такой реализации есть один существенный недостаток. Главный цикл работает слишком быстро: каждые несколько миллисекунд проверяется очередь событий. Это избыточно нагружает процессор компьютера. События так быстро не генерируются. Даже когда выполняется анимация в динамических играх (типа арканоид), вполне достаточно 60 кадров в секунду, т.е. выполнять главный цикл с частотой:

1/60 сек = 17 миллисекунд

А в статических играх, типа пасьянс, минер и т.п., достаточно 30 кадров в секунду:

1/30 сек = 34 миллисекунд

Как создать нужные задержки в главном цикле приложения? Если действовать «в лоб», то можно просто прописать эти временные величины внутри цикла while:

while 1:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            exit()
 
    pygame.time.delay(20) # задержка на 20 мс

Но это не лучшее решение: на разных устройствах время выполнения итерации цикла будет разным (из-за разной скорости работы ЦП). Здесь задержка в 20 мс добавляется ко времени выполнения программы внутри цикла. То есть, мы не учитываем время обработки событий, а просто добавляем задержку в 20 мс. В результате цикл будет работать немного дольше. И это может негативно сказаться на игровом процессе: на одном устройстве игровой персонаж будет быстрее проходить уровень, а на другом – медленнее. Поэтому, правильнее использовать встроенный в PyGame инструмент тиков на основе класса Clock:

clock = pygame.time.Clock()
while 1:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            exit()
 
    clock.tick(60)

Здесь в методе tick указывается частота выполнения цикла за одну секунду. То есть, параметр 60 указывает выполнять цикл while 60 раз в секунду. При этом автоматически учитывается время, затраченное на выполнение других строчек кода на каждой текущей итерации и оно вычитается при формировании задержки. В результате, мы получаем время каждой итерации примерно 17 мс. И это время будет одинаковым на всех устройствах (разумеется, если оно будет способно обеспечивать такую скорость выполнения итераций цикла).

Вот эту величину (60) называют Frames Per Second (FPS) и, обычно, определяют вначале программы через переменную:

FPS = 60        # число кадров в секунду
clock = pygame.time.Clock()
 
while 1:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            exit()
 
    clock.tick(FPS)

Наконец, последний штрих. Изменим заголовок окна и его иконку. Это делается с помощью вот таких двух строчек:

pygame.display.set_caption("Моя первая программа на PyGame")
pygame.display.set_icon(pygame.image.load("app.bmp"))

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

  • pygame.FULLSCREEN – полноэкранный режим;
  • pygame.DOUBLEBUF – двойная буферизация (рекомендуется при совместном использовании HWSURFACE или OPENGL);
  • pygame.HWSURFACE – аппаратное ускорение отрисовки (только для режима FULLSCREEN);
  • pygame.OPENGL – обработка отображений  с помощью библиотеки OpenGL;
  • pygame.RESIZABLE – окно с изменяемыми размерами;
  • pygame.NOFRAME – окно без рамки и заголовка;
  • pygame.SCALED – разрешение, зависящее от размеров рабочего стола.

Например, если прописать флаг:

pygame.display.set_mode((600, 400), pygame.RESIZABLE)

то окно можно будет менять в размерах. Или так:

pygame.display.set_mode((600, 400), pygame.DOUBLEBUF | pygame.HWSURFACE | pygame.FULLSCREEN)

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

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