Как написать свою игру на питоне

#статьи

  • 15 июл 2022

  • 0

Учимся программировать через разработку игр. Сегодня напишем знакомую всем «Змейку» — вспомним правила игры и реализуем их на Python.

Иллюстрация: Оля Ежак для Skillbox Media

Антон Яценко

Изучает Python, его библиотеки и занимается анализом данных. Любит путешествовать в горах.

Pygame — популярная библиотека для создания игр под различные устройства на Windows, macOS, Linux или Android. Она помогает разработчику не только описать геймплей, но и работать с клавиатурой, мышью, акселерометром, звуком и видео.

Первая версия Pygame была представлена Питом Шиннерсом в октябре 2000 года. За 22 года вокруг библиотеки сложилось большое комьюнити, а о работе с ней написано несколько десятков книг. Последняя стабильная версия на июль 2022 года — 2.1.2.

Давайте разберёмся в том, как устроена Pygame, и напишем свою первую игру — классическую «Змейку» на Python, которую студенты часто берут для курсовой работы по программированию.

Pygame — не самостоятельная библиотека. На самом деле это обёртка для библиотеки SDL, Simple DirectMedia Layer. Именно SDL позволяет задействовать любые внешние устройства — например, мышь или клавиатуру. А Pygame делает работу с ними удобной для Python-разработчика.

Установить Pygame просто. Для этого воспользуемся терминалом или командной строкой и командой pip:

pip install pygame

Если во время установки возникают ошибки, то можно воспользоваться официальной документацией. В ней описаны особенности установки на различные системы, а также пути решения распространённых проблем.

Библиотека Pygame состоит из конструкций на языке Python и включает в себя несколько модулей. Модули позволяют получить доступ к определённому устройству и содержат методы для работы с ним. Например, модуль display позволяет работать с экраном, а joystick — считывать движения с джойстика.

После того как вы импортировали Pygame, необходимо инициировать библиотеку с помощью команды pygame.int(). Это поможет нам использовать любые методы любых функций, включённых в библиотеку модулей. Без инициализации код может потерять кросс-платформенность и не запускаться в другой системе.

Помимо модулей, Pygame включает несколько классов Python, которые работают с концепциями, не зависящими от аппаратного обеспечения. Одна из таких концепций — Surface. Surface, можно сказать, определяет прямоугольную область, на которой можно рисовать. Если переносить на практику, то этот класс позволяет создать игровое поле. Он широко используется при работе с Pygame, и мы тоже поработаем с ним при создании «Змейки».

В Pygame вся информация выводится на игровое поле, которому в коде соответствует класс display. Игровое поле может быть полноэкранным или занимать часть экрана. Display создаётся с помощью функции .set_mode(), которая возвращает Surface, представляющий видимую часть окна. Именно эту область вы будете передавать в функции рисования, такие как pygame.draw.circle(), а содержимое этого Surface будет выводиться на дисплей при вызове pygame.display.flip(). Звучит сложно, но на практике будет проще. Оба класса мы будем использовать при создании «Змейки».

Работать с изображениями в Pygame можно двумя способами: создавать их с нуля на экране или использовать изображения с диска. И тот и другой тип можно перезаписывать, загружать и сохранять в различных форматах — например, в PNG и JPG.

Изображения — не самостоятельные компоненты: они загружаются в объекты Surface, которые можно изменять и выводить на дисплеи различными способами.

Выше мы писали, что объекты Surface представлены прямоугольниками, подобно многим другим объектам в Pygame, в том числе изображениям и окнам. Прямоугольники используются настолько активно, что существует даже специальный класс Rect, предназначенный для работы только с ними. Объекты Rect используются для различных задач: создания фигур игрока и врагов, управления взаимодействиями между ними и так далее.

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

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

Несмотря на то что «Змейка» — простая игра, нам предстоит написать довольно много кода. Мы разбили этапы создания игры на последовательные шаги, каждый из которых реализует отдельную часть геймплея.


Запускаем Python

Писать код на Python лучше всего в специальном редакторе. Есть несколько вариантов:

  • Воспользоваться специализированными IDE: IntelliJ IDEA или Visual Studio Code. Мы рекомендуем именно этот способ — например, весь код для этой статьи мы писали в Visual Studio Code.
  • Использовать терминал на macOS или Linux или воспользоваться командной строкой в Windows. Для этого предварительно потребуется установить Python в систему. Мы подробно писали об этом в отдельном материале.

После установки и запуска Python загружаем библиотеку Pygame:

pip install pygame

Теперь у нас всё готово к работе над игрой. Для дальнейшего написания кода необходимо создать один пустой Python-файл.


Создаём игровое поле

Чтобы создать окно с игрой с помощью Pygame, необходимо использовать функцию display.set_mode() и передать в неё желаемый размер окна в пикселях. Также необходимо использовать методы init() и quit() для инициализации библиотеки в начале кода и её деинициализации в конце кода.

Метод update() используется для обновления содержимого экрана. Существует ещё метод flip(), который работает аналогично update(). Разница в том, что метод update() обновляет только внесённые изменения, а метод flip() перерисовывает экран целиком. Но если в метод update() не передавать никакие параметры, то также обновится весь экран.

import pygame
 
pygame.init()
dis=pygame.display.set_mode((500,400))  #Задаём размер игрового поля.
 
pygame.display.update()
 
pygame.quit()
quit()

Если сейчас запустить этот код, то экран игры сразу же закроется. Это связано с тем, что код сразу переходит к следующей строчке pygame.quit(), отключающей библиотеку и наше игровое поле. Чтобы избежать этого, необходимо воспользоваться циклом while — он не позволит игровому экрану закрыться:

import pygame
 
pygame.init()
 
dis=pygame.display.set_mode((500,400))
pygame.display.update()
pygame.display.set_caption('Змейка от Skillbox') #Добавляем название игры.
 
game_over=False #Создаём переменную, которая поможет нам контролировать 
статус игры — завершена она или нет. Изначально присваиваем значение False,
 то есть игра продолжается.
 
while not game_over:
   for event in pygame.event.get():
       print(event)  #Выводить в терминал все произошедшие события.
 
pygame.quit()
quit()

Кроме этого, мы добавили в код ещё две сущности: название игры и функцию для отслеживания игровых событий. Чтобы у окна с игрой появилось название, мы используем pygame.display.set_caption(») (название пишем в кавычках). А функция event.get() возвращает в терминал все события, которые происходят с игрой.

Запустим код и посмотрим, что получилось:

Скриншот: Pygame / Skillbox Media

Теперь игровое окно не закрывается само по себе. Однако и закрыть его мы тоже не сможем — если нажать на кнопку «Выход», ничего не произойдёт. Исправляем это с помощью кода: добавляем событие QUIT, закрывающее окно.

import pygame
 
pygame.init()
 
dis=pygame.display.set_mode((500, 400))
pygame.display.update()
pygame.display.set_caption('Змейка от Skillbox')
 
game_over=False
while not game_over:
   for event in pygame.event.get():
       if event.type==pygame.QUIT:
           game_over=True
 
pygame.quit()
quit()

Теперь кнопка выхода работает как надо. Если нажать на неё, то окно с игрой закроется.


Создаём змейку

Для начала инициализируем переменные, которые задают цвет. Мы будем использовать их, чтобы присвоить цвет экрану, самой змейке и еде. В Pygame используется стандартная RGB-схема, то есть любой цвет представляет собой комбинацию красного, зелёного и синего цветов, интенсивность которых мы можем менять.

Наша змейка — прямоугольник, поэтому мы воспользуемся функцией создания прямоугольников draw.rect(). Она позволяет задать размер и цвет прямоугольника.

import pygame
 
pygame.init()
 
dis=pygame.display.set_mode((500, 400))
pygame.display.update()
pygame.display.set_caption('Змейка от Skillbox')
 
game_over=False
while not game_over:
   for event in pygame.event.get():
       if event.type==pygame.QUIT:
           game_over=True
 
pygame.quit()
quit()

Запустим код и посмотрим на результат.

Скриншот: Pygame / Skillbox Media

Всё получилось. Ближе к центру экрана появился синий квадрат, который и будет нашей змейкой.


Описываем движения змейки

Управлять перемещением змейки можно с помощью специального класса Pygame KEYDOWN. Класс позволяет использовать четыре стандартных события, получая их с клавиавтуры: K_UP, K_DOWN, K_LEFT и K_RIGHT — они соответствуют движениям змейки вверх, вниз, влево и вправо. Срабатывание любого события из класса KEYDOWN приводит к изменению положения змейки. Зададим шаг этого движения в 10 пикселей.

Кроме того, мы должны создать две переменные для хранения значений координат первой клетки нашей змейки по осям x и y. Назовём их x1_change и y1_change.

import pygame
 
pygame.init()
 
white = (255, 255, 255)
black = (0, 0, 0)
red = (255, 0, 0)
 
dis = pygame.display.set_mode((800, 600))
pygame.display.set_caption('Змейка от Skillbox')
 
game_over = False
x1 = 300 #Указываем начальное значение положения змейки по оси х.
y1 = 300 #Указываем начальное значение положения змейки по оси y.
x1_change = 0 #Создаём переменную, которой в цикле while будут
присваиваться значения изменения положения змейки по оси х.
y1_change = 0 #создаём переменную, которой в цикле while будут
присваиваться значения изменения положения змейки по оси y.
clock = pygame.time.Clock()
 
while not game_over:
   for event in pygame.event.get():
       if event.type == pygame.QUIT:
           game_over = True
       if event.type == pygame.KEYDOWN: #Добавляем считывание направления
движений с клавиатуры.
           if event.key == pygame.K_LEFT:
               x1_change = -10 #Указываем шаг изменения положения змейки
в 10 пикселей.
               y1_change = 0
           elif event.key == pygame.K_RIGHT:
               x1_change = 10
               y1_change = 0
           elif event.key == pygame.K_UP:
               y1_change = -10
               x1_change = 0
           elif event.key == pygame.K_DOWN:
               y1_change = 10
               x1_change = 0
   x1 += x1_change #Записываем новое значение положения змейки по оси х.
   y1 += y1_change #Записываем новое значение положения змейки по оси y.
   dis.fill(white)
   
   pygame.draw.rect(dis, black, [x1, y1, 10, 10])
   pygame.display.update()
   clock.tick(30)
 
pygame.quit()
quit()

Теперь змейка двигается по игровому полю:

Скриншот: Pygame / Skillbox Media

Учитываем препятствия — границы игрового поля

Если змейка попадает на границу экрана, то игрок терпит поражение, а игра заканчивается. Чтобы закодить это правило, можно воспользоваться оператором if, который определяет координаты x и y для змейки и анализирует, выходят ли они за границы игрового поля. Добавим необходимый код.

import pygame
import time
 
pygame.init()
 
white = (255, 255, 255)
black = (0, 0, 0)
red = (255, 0, 0)
 
dis_width = 800 #Зададим размер игрового поля через две переменные.
dis_height = 600
dis = pygame.display.set_mode((dis_width, dis_width))
pygame.display.set_caption('Змейка от Skillbox')
 
game_over = False
x1 = dis_width/2 #Стартовое положение змейки по осям рассчитывается
через переменные, указывающие размер игрового экрана.
y1 = dis_height/2
snake_block=10 #Укажем в переменной стандартную величину сдвига
положения змейки при нажатии на клавиши.
x1_change = 0
y1_change = 0
 
clock = pygame.time.Clock()
snake_speed=15 #Ограничим скорость движения змейки.
font_style = pygame.font.SysFont(None, 50)
 
def message(msg,color): #Создадим функцию, которая будет показывать
нам сообщения на игровом экране.
   mesg = font_style.render(msg, True, color)
   dis.blit(mesg, [dis_width/2, dis_height/2])
 
while not game_over:
   for event in pygame.event.get():
       if event.type == pygame.QUIT:
           game_over = True
       if event.type == pygame.KEYDOWN:
           if event.key == pygame.K_LEFT:
               x1_change = -snake_block
               y1_change = 0
           elif event.key == pygame.K_RIGHT:
               x1_change = snake_block
               y1_change = 0
           elif event.key == pygame.K_UP:
               y1_change = -snake_block
               x1_change = 0
           elif event.key == pygame.K_DOWN:
               y1_change = snake_block
               x1_change = 0
   
  if x1 >= dis_width or x1 < 0 or y1 >= dis_height or y1 < 0:
       game_over = True #Явно укажем, что если координаты змейки
выходят за рамки игрового поля, то игра должна закончиться.
   x1 += x1_change
   y1 += y1_change
   dis.fill(white)
   
   pygame.draw.rect(dis, black, [x1, y1, snake_block, snake_block])
   pygame.display.update()
   clock.tick(snake_speed)
 
message("Вы проиграли :(",red) #Сообщение, которое появляется при
проигрыше. В нашем случае — при выходе змейки за пределы игрового поля.
 
pygame.display.update()
time.sleep(2)
pygame.quit()
 
quit()

Теперь, если змейка достигнет края экрана, игра закончится, а на дисплее появится сообщение о проигрыше:

Скриншот: Pygame / Skillbox Media

Добавляем еду для змейки

Теперь добавим «еду». Используем библиотеку random, чтобы она появлялась в случайном месте на игровом поле. Когда наша змейка будет проходить через еду, то её длина будет увеличиваться. Это мы добавим на следующем шаге. Кроме того, дадим возможность игроку выйти из игры или начать игру заново после проигрыша.

import pygame
import time
import random
 
pygame.init()
white = (255, 255, 255)
black = (0, 0, 0)
red = (255, 0, 0)
blue = (0, 0, 255)
dis_width = 800
dis_height = 600
dis = pygame.display.set_mode((dis_width, dis_height))
pygame.display.set_caption('Змейка от Skillbox')
clock = pygame.time.Clock()
snake_block = 10
snake_speed = 15
font_style = pygame.font.SysFont(None, 30)
 
def message(msg, color):
   mesg = font_style.render(msg, True, color)
   dis.blit(mesg, [dis_width/10, dis_height/3])
 
def gameLoop(): #Описываем всю игровую логику в одной функции.
   game_over = False
   game_close = False
   x1 = dis_width / 2
   y1 = dis_height / 2
   x1_change = 0
   y1_change = 0
   foodx = round(random.randrange(0, dis_width - snake_block) / 10.0) * 10.0 
#Создаём переменную, которая будет указывать расположение еды по оси х.
   foody = round(random.randrange(0, dis_width - snake_block) / 10.0) * 10.0 
#Создаём переменную, которая будет указывать расположение еды по оси y.
   while not game_over:
       while game_close == True:
           dis.fill(white)
           message("Вы проиграли! Нажмите Q для выхода 
или C для повторной игры", red)
           pygame.display.update()
           for event in pygame.event.get():
               if event.type == pygame.KEYDOWN:
                   if event.key == pygame.K_q:
                       game_over = True
                       game_close = False
                   if event.key == pygame.K_c:
                       gameLoop()
       for event in pygame.event.get():
           if event.type == pygame.QUIT:
               game_over = True
           if event.type == pygame.KEYDOWN:
               if event.key == pygame.K_LEFT:
                   x1_change = -snake_block
                   y1_change = 0
               elif event.key == pygame.K_RIGHT:
                   x1_change = snake_block
                   y1_change = 0
               elif event.key == pygame.K_UP:
                   y1_change = -snake_block
                   x1_change = 0
               elif event.key == pygame.K_DOWN:
                   y1_change = snake_block
                   x1_change = 0
       if x1 >= dis_width or x1 < 0 or y1 >= dis_height or y1 < 0:
           game_close = True
       x1 += x1_change
       y1 += y1_change
       dis.fill(white)
       pygame.draw.rect(dis, blue, [foodx, foody, snake_block, snake_block])
       pygame.draw.rect(dis, black, [x1, y1, snake_block, snake_block])
       pygame.display.update()
   
   pygame.quit()
   quit()
 
gameLoop()

Теперь при запуске игры кроме самой змейки будет показана еда. В нашем случае — в виде чёрного квадрата.

Скриншот: Pygame / Skillbox Media

А если выполнить условие для завершения игры, то появится сообщение с предложением выйти из игры или начать её заново:

Скриншот: Pygame / Skillbox Media

Увеличиваем длину змейки

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

import pygame
import time
import random
 
pygame.init()
 
white = (255, 255, 255)
yellow = (255, 255, 102)
black = (0, 0, 0)
red = (213, 50, 80)
green = (0, 255, 0)
blue = (50, 153, 213)
 
dis_width = 800
dis_height = 600
dis = pygame.display.set_mode((dis_width, dis_height))
pygame.display.set_caption('Змейка от Skillbox')
clock = pygame.time.Clock()
snake_block = 10
snake_speed = 15
font_style = pygame.font.SysFont("bahnschrift", 25) #Укажем название
 шрифта и его размер для системных сообщений, например, при завершении игры.
score_font = pygame.font.SysFont("comicsansms", 35) #Укажем шрифт и 
его размер для отображения счёта. Это мы реализуем очень скоро.
 
def our_snake(snake_block, snake_list):
   for x in snake_list:
       pygame.draw.rect(dis, black, [x[0], x[1], snake_block, snake_block])
 
def message(msg, color):
   mesg = font_style.render(msg, True, color)
   dis.blit(mesg, [dis_width / 6, dis_height / 3])
 
 
def gameLoop():
   game_over = False
   game_close = False
   x1 = dis_width / 2
   y1 = dis_height / 2
   x1_change = 0
   y1_change = 0
   snake_List = [] #Создаём список, в котором будем хранить 
показатель текущей длины змейки.
   Length_of_snake = 1 
   foodx = round(random.randrange(0, dis_width - snake_block) / 10.0) * 10.0
   foody = round(random.randrange(0, dis_height - snake_block) / 10.0) * 10.0
   while not game_over:
       while game_close == True:
           dis.fill(blue)
           message("Вы проиграли! Нажмите Q для выхода 
или C для повторной игры", red)
           pygame.display.update()
           for event in pygame.event.get():
               if event.type == pygame.KEYDOWN:
                   if event.key == pygame.K_q:
                       game_over = True
                       game_close = False
                   if event.key == pygame.K_c:
                       gameLoop()
       for event in pygame.event.get():
           if event.type == pygame.QUIT:
               game_over = True
           if event.type == pygame.KEYDOWN:
               if event.key == pygame.K_LEFT:
                   x1_change = -snake_block
                   y1_change = 0
               elif event.key == pygame.K_RIGHT:
                   x1_change = snake_block
                   y1_change = 0
               elif event.key == pygame.K_UP:
                   y1_change = -snake_block
                   x1_change = 0
               elif event.key == pygame.K_DOWN:
                   y1_change = snake_block
                   x1_change = 0
       if x1 >= dis_width or x1 < 0 or y1 >= dis_height or y1 < 0:
           game_close = True
       x1 += x1_change
       y1 += y1_change
       dis.fill(blue)
       pygame.draw.rect(dis, green, [foodx, foody, snake_block, snake_block])
       snake_Head = [] #Создаём список, в котором будет храниться 
показатель длины змейки при движениях.
       snake_Head.append(x1) #Добавляем значения в список при 
изменении по оси х.
       snake_Head.append(y1) #Добавляем значения в список при 
изменении по оси y.
       snake_List.append(snake_Head)
       if len(snake_List) > Length_of_snake:
           del snake_List[0] #Удаляем первый элемент в списке 
длины змейки, чтобы она не увеличивалась сама по себе при движениях.
       for x in snake_List[:-1]:
           if x == snake_Head:
               game_close = True
       our_snake(snake_block, snake_List)
       pygame.display.update()
       if x1 == foodx and y1 == foody: #Указываем, что в случаях, 
если координаты головы змейки совпадают с координатами еды, еда появляется 
в новом месте, а длина змейки увеличивается на одну клетку.
           foodx = round(random.randrange(0, dis_width - snake_block) / 10.0) * 10.0
           foody = round(random.randrange(0, dis_height - snake_block) / 10.0) * 10.0
           Length_of_snake += 1
       clock.tick(snake_speed)
   pygame.quit()
   quit()
 
gameLoop()

Дополнительно мы изменили цвет игрового поля и змейки, увеличив их контрастность.

Скриншот: Pygame / Skillbox Media

Добавляем отображение счёта

Добавим отображение счёта текущей игры. Для этого создадим функцию Your_score. Она будет отображать длину змейки, вычитая из неё 1 (ведь 1 — это начальный размер змейки, и это не является достижением игрока).

def Your_score(score):
   value = score_font.render("Ваш счёт: " + str(score), True, yellow)
   dis.blit(value, [0, 0])

И отдельно пропишем правило определения длины змейки, вычитая из текущей длины змейки единицу.

Your_score(Length_of_snake - 1)

Теперь на игровом поле будет отображаться текущий счёт:

Скриншот: Pygame / Skillbox Media

Можно считать, что наша работа над «Змейкой» закончена. Мы полностью реализовали геймплей, который запланировали на старте работы.

Наш код полностью и без комментариев:

import pygame
import time
import random
pygame.init()
white = (255, 255, 255)
yellow = (255, 255, 102)
black = (0, 0, 0)
red = (213, 50, 80)
green = (0, 255, 0)
blue = (50, 153, 213)
dis_width = 800
dis_height = 600
dis = pygame.display.set_mode((dis_width, dis_height))
pygame.display.set_caption('Змейка от Skillbox')
clock = pygame.time.Clock()
snake_block = 10
snake_speed = 15
font_style = pygame.font.SysFont("bahnschrift", 25)
score_font = pygame.font.SysFont("comicsansms", 35)
 
def Your_score(score):
   value = score_font.render("Ваш счёт: " + str(score), True, yellow)
   dis.blit(value, [0, 0])
 
def our_snake(snake_block, snake_list):
   for x in snake_list:
       pygame.draw.rect(dis, black, [x[0], x[1], snake_block, snake_block])
 
def message(msg, color):
   mesg = font_style.render(msg, True, color)
   dis.blit(mesg, [dis_width / 6, dis_height / 3])
 
def gameLoop():
   game_over = False
   game_close = False
   x1 = dis_width / 2
   y1 = dis_height / 2
   x1_change = 0
   y1_change = 0
   snake_List = []
   Length_of_snake = 1
   foodx = round(random.randrange(0, dis_width - snake_block) / 10.0) * 10.0
   foody = round(random.randrange(0, dis_height - snake_block) / 10.0) * 10.0
   while not game_over:
       while game_close == True:
           dis.fill(blue)
           message("Вы проиграли! Нажмите Q для выхода или C для повторной игры", red)
           Your_score(Length_of_snake - 1)
           pygame.display.update()
           for event in pygame.event.get():
               if event.type == pygame.KEYDOWN:
                   if event.key == pygame.K_q:
                       game_over = True
                       game_close = False
                   if event.key == pygame.K_c:
                       gameLoop()
       for event in pygame.event.get():
           if event.type == pygame.QUIT:
               game_over = True
           if event.type == pygame.KEYDOWN:
               if event.key == pygame.K_LEFT:
                   x1_change = -snake_block
                   y1_change = 0
               elif event.key == pygame.K_RIGHT:
                   x1_change = snake_block
                   y1_change = 0
               elif event.key == pygame.K_UP:
                   y1_change = -snake_block
                   x1_change = 0
               elif event.key == pygame.K_DOWN:
                   y1_change = snake_block
                   x1_change = 0
       if x1 >= dis_width or x1 < 0 or y1 >= dis_height or y1 < 0:
           game_close = True
       x1 += x1_change
       y1 += y1_change
       dis.fill(blue)
       pygame.draw.rect(dis, green, [foodx, foody, snake_block, snake_block])
       snake_Head = []
       snake_Head.append(x1)
       snake_Head.append(y1)
       snake_List.append(snake_Head)
       if len(snake_List) > Length_of_snake:
           del snake_List[0]
       for x in snake_List[:-1]:
           if x == snake_Head:
               game_close = True
       our_snake(snake_block, snake_List)
       Your_score(Length_of_snake - 1)
       pygame.display.update()
       if x1 == foodx and y1 == foody:
           foodx = round(random.randrange(0, dis_width - snake_block) / 10.0) * 10.0
           foody = round(random.randrange(0, dis_height - snake_block) / 10.0) * 10.0
           Length_of_snake += 1
       clock.tick(snake_speed)
   pygame.quit()
   quit()
gameLoop()

Узнать об особенностях работы с Pygame и возможностях библиотеки можно в официальной документации. Углубиться в разработку и попробовать другие игры можно благодаря специализированным книгам:

  • «Учим Python, делая крутые игры» Эла Свейгарта;
  • «Beginning Game Development with Python and Pygame: From Novice to Professional» Уилла Макгугана;
  • «Program Arcade Games: With Python and Pygame» Пола Винсента Крэйвена.

Учись бесплатно:
вебинары по программированию, маркетингу и дизайну.

Участвовать

Прежде чем мы начнём программировать что-то полезное на Python, давайте закодим что-нибудь интересное. Например, свою игру, где нужно не дать шарику упасть, типа Арканоида. Вы, скорее всего, играли в детстве во что-то подобное, поэтому освоиться будет просто.

Логика игры

Есть игровое поле — простой прямоугольник с твёрдыми границами. Когда шарик касается стенки или потолка, он отскакивает в другую сторону. Если он упадёт на пол — вы проиграли. Чтобы этого не случилось, внизу вдоль пола летает платформа, а вы ей управляете с помощью стрелок. Ваша задача — подставлять платформу под шарик как можно дольше. За каждое удачное спасение шарика вы получаете одно очко.

Алгоритм

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

  • игра начинается;
  • шарик начинает двигаться;
  • если нажаты стрелки влево или вправо — двигаем платформу;
  • если шарик коснулся стенок, потолка или платформы — делаем отскок;
  • если шарик коснулся платформы — увеличиваем счёт на единицу;
  • если шарик упал на пол — выводим сообщение и заканчиваем игру.

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

Получается, что нам нужно определить три класса — платформу, сам шарик и счёт, и определить, как они реагируют на действия друг друга. Поле нам самим определять не нужно — для этого есть уже готовая библиотека. А потом в этих классах мы пропишем методы — они как раз и будут отвечать за поведение наших объектов.

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

По коням, пишем на Python

Для этого проекта вам потребуется установить и запустить среду Python. Как это сделать — читайте в нашей статье.

Начало программы

Чтобы у нас появилась графика в игре, используем библиотеку Tkinter. Она входит в набор стандартных библиотек Python и позволяет рисовать простейшие объекты — линии, прямоугольники, круги и красить их в разные цвета. Такой простой Paint, только для Python.

Чтобы создать окно, где будет видна графика, используют класс Tk(). Он просто делает окно, но без содержимого. Чтобы появилось содержимое, создают холст — видимую часть окна. Именно на нём мы будем рисовать нашу игру. За холст отвечает класс Canvas(), поэтому нам нужно будет создать свой объект из этого класса и дальше уже работать с этим объектом.

Если мы принудительно не ограничим скорость платформы, то она будет перемещаться мгновенно, ведь компьютер считает очень быстро и моментально передвинет её к другому краю. Поэтому мы будем искусственно ограничивать время движения, а для этого нам понадобится модуль Time — он тоже стандартный.

Последнее, что нам глобально нужно, — задавать случайным образом начальное положение шарика и платформы, чтобы было интереснее играть. За это отвечает модуль Random — он помогает генерировать случайные числа и перемешивать данные.

Запишем всё это в виде кода на Python:

# подключаем графическую библиотеку
from tkinter import *
# подключаем модули, которые отвечают за время и случайные числа
import time
import random
# создаём новый объект — окно с игровым полем. В нашем случае переменная окна называется tk, и мы его сделали из класса Tk() — он есть в графической библиотеке 
tk = Tk()
# делаем заголовок окна — Games с помощью свойства объекта title
tk.title('Game')
# запрещаем менять размеры окна, для этого используем свойство resizable 
tk.resizable(0, 0)
# помещаем наше игровое окно выше остальных окон на компьютере, чтобы другие окна не могли его заслонить
tk.wm_attributes('-topmost', 1)
# создаём новый холст — 400 на 500 пикселей, где и будем рисовать игру
canvas = Canvas(tk, width=500, height=400, highlightthickness=0)
# говорим холсту, что у каждого видимого элемента будут свои отдельные координаты 
canvas.pack()
# обновляем окно с холстом
tk.update()

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

Шарик

Сначала проговорим словами, что нам нужно от шарика. Он должен уметь:

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

Этого достаточно, чтобы шарик жил своей жизнью и умел взаимодействовать с окружающей средой. При этом нужно не забыть о том, что каждый класс должен содержать конструктор — код, который отвечает за создание нового объекта. Без этого сделать шарик не получится. Запишем это на Python:

# Описываем класс Ball, который будет отвечать за шарик 
class Ball:
    # конструктор — он вызывается в момент создания нового объекта на основе этого класса
    def __init__(self, canvas, paddle, score, color):
        # задаём параметры объекта, которые нам передают в скобках в момент создания
        self.canvas = canvas
        self.paddle = paddle
        self.score = score
        # цвет нужен был для того, чтобы мы им закрасили весь шарик
        # здесь появляется новое свойство id, в котором хранится внутреннее название шарика
        # а ещё командой create_oval мы создаём круг радиусом 15 пикселей и закрашиваем нужным цветом
        self.id = canvas.create_oval(10,10, 25, 25, fill=color)
        # помещаем шарик в точку с координатами 245,100
        self.canvas.move(self.id, 245, 100)
        # задаём список возможных направлений для старта
        starts = [-2, -1, 1, 2]
        # перемешиваем его 
        random.shuffle(starts)
        # выбираем первый из перемешанного — это будет вектор движения шарика
        self.x = starts[0]
        # в самом начале он всегда падает вниз, поэтому уменьшаем значение по оси y
        self.y = -2
        # шарик узнаёт свою высоту и ширину
        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()
        # свойство, которое отвечает за то, достиг шарик дна или нет. Пока не достиг, значение будет False
        self.hit_bottom = False
    # обрабатываем касание платформы, для этого получаем 4 координаты шарика в переменной pos (левая верхняя и правая нижняя точки)
    def hit_paddle(self, pos):
        # получаем кординаты платформы через объект paddle (платформа)
        paddle_pos = self.canvas.coords(self.paddle.id)
        # если координаты касания совпадают с координатами платформы
        if pos[2] >= paddle_pos[0] and pos[0] <= paddle_pos[2]:
            if pos[3] >= paddle_pos[1] and pos[3] <= paddle_pos[3]:
                # увеличиваем счёт (обработчик этого события будет описан ниже)
                self.score.hit()
                # возвращаем метку о том, что мы успешно коснулись
                return True
        # возвращаем False — касания не было
        return False
    # обрабатываем отрисовку шарика
    def draw(self):
        # передвигаем шарик на заданные координаты x и y
        self.canvas.move(self.id, self.x, self.y)
        # запоминаем новые координаты шарика
        pos = self.canvas.coords(self.id)
        # если шарик падает сверху  
        if pos[1] <= 0:
            # задаём падение на следующем шаге = 2
            self.y = 2
        # если шарик правым нижним углом коснулся дна
        if pos[3] >= self.canvas_height:
            # помечаем это в отдельной переменной
            self.hit_bottom = True
            # выводим сообщение и количество очков
            canvas.create_text(250, 120, text='Вы проиграли', font=('Courier', 30), fill='red')
        # если было касание платформы
        if self.hit_paddle(pos) == True:
            # отправляем шарик наверх
            self.y = -2
        # если коснулись левой стенки
        if pos[0] <= 0:
            # движемся вправо
            self.x = 2
        # если коснулись правой стенки
        if pos[2] >= self.canvas_width:
            # движемся влево
            self.x = -2

Платформа

Сделаем то же самое для платформы — сначала опишем её поведение словами, а потом переведём в код. Итак, вот что должна уметь платформа:

  • двигаться влево или вправо в зависимости от нажатой стрелки;
  • понимать, когда игра началась и можно двигаться.

А вот как это будет в виде кода:

#  Описываем класс Paddle, который отвечает за платформы
class Paddle:
    # конструктор
    def __init__(self, canvas, color):
        # canvas означает, что платформа будет нарисована на нашем изначальном холсте
        self.canvas = canvas
        # создаём прямоугольную платформу 10 на 100 пикселей, закрашиваем выбранным цветом и получаем её внутреннее имя 
        self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)
        # задаём список возможных стартовых положений платформы
        start_1 = [40, 60, 90, 120, 150, 180, 200]
        # перемешиваем их
        random.shuffle(start_1)
        # выбираем первое из перемешанных
        self.starting_point_x = start_1[0]
        # перемещаем платформу в стартовое положение
        self.canvas.move(self.id, self.starting_point_x, 300)
        # пока платформа никуда не движется, поэтому изменений по оси х нет
        self.x = 0
        # платформа узнаёт свою ширину
        self.canvas_width = self.canvas.winfo_width()
        # задаём обработчик нажатий
        # если нажата стрелка вправо — выполняется метод turn_right()
        self.canvas.bind_all('<KeyPress-Right>', self.turn_right)
        # если стрелка влево — turn_left()
        self.canvas.bind_all('<KeyPress-Left>', self.turn_left)
        # пока игра не началась, поэтому ждём
        self.started = False
        # как только игрок нажмёт Enter — всё стартует
        self.canvas.bind_all('<KeyPress-Return>', self.start_game)
    # движемся вправо 
    def turn_right(self, event):
        # будем смещаться правее на 2 пикселя по оси х
        self.x = 2
    # движемся влево
    def turn_left(self, event):
        # будем смещаться левее на 2 пикселя по оси х
        self.x = -2
    # игра начинается
    def start_game(self, event):
        # меняем значение переменной, которая отвечает за старт
        self.started = True
    # метод, который отвечает за движение платформы
    def draw(self):
        # сдвигаем нашу платформу на заданное количество пикселей
        self.canvas.move(self.id, self.x, 0)
        # получаем координаты холста
        pos = self.canvas.coords(self.id)
        # если мы упёрлись в левую границу 
        if pos[0] <= 0:
            # останавливаемся
            self.x = 0
        # если упёрлись в правую границу 
        elif pos[2] >= self.canvas_width:
            # останавливаемся
            self.x = 0

Счёт

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

От счёта нам нужно только одно (кроме конструктора) — чтобы он правильно реагировал на касание платформы, увеличивал число очков и выводил их на экран:

#  Описываем класс Score, который отвечает за отображение счетов
class Score:
    # конструктор
    def __init__(self, canvas, color):
        # в самом начале счёт равен нулю
        self.score = 0
        # будем использовать наш холст
        self.canvas = canvas
        # создаём надпись, которая показывает текущий счёт, делаем его нужно цвета и запоминаем внутреннее имя этой надписи
        self.id = canvas.create_text(450, 10, text=self.score, font=('Courier', 15), fill=color)
    # обрабатываем касание платформы
    def hit(self):
        # увеличиваем счёт на единицу
        self.score += 1
        # пишем новое значение счёта 
        self.canvas.itemconfig(self.id, text=self.score)
        

Игра

У нас всё готово для того, чтобы написать саму игру. Мы уже провели необходимую подготовку всех элементов, и нам остаётся только создать конкретные объекты шарика, платформы и счёта и сказать им, в каком порядке мы будем что делать.

Смысл игры в том, чтобы не уронить шарик. Пока этого не произошло — всё движется, но как только шарик упал — нужно показать сообщение о конце игры и остановить программу.

Посмотрите, как лаконично выглядит код непосредственно самой игры:

# создаём объект — зелёный счёт 
score = Score(canvas, 'green')
# создаём объект — белую платформу
paddle = Paddle(canvas, 'White')
# создаём объект — красный шарик 
ball = Ball(canvas, paddle, score, 'red')
# пока шарик не коснулся дна 
while not ball.hit_bottom:
    # если игра началась и платформа может двигаться
    if paddle.started == True:
        # двигаем шарик
        ball.draw()
        # двигаем платформу
        paddle.draw()
    # обновляем наше игровое поле, чтобы всё, что нужно, закончило рисоваться
    tk.update_idletasks()
    # обновляем игровое поле, и смотрим за тем, чтобы всё, что должно было быть сделано — было сделано
    tk.update()
    # замираем на одну сотую секунды, чтобы движение элементов выглядело плавно
    time.sleep(0.01)
# если программа дошла досюда, значит, шарик коснулся дна. Ждём 3 секунды, пока игрок прочитает финальную надпись, и завершаем игру
time.sleep(3)
# подключаем графическую библиотеку
from tkinter import *
# подключаем модули, которые отвечают за время и случайные числа
import time
import random
# создаём новый объект — окно с игровым полем. В нашем случае переменная окна называется tk, и мы его сделали из класса Tk() — он есть в графической библиотеке 
tk = Tk()
# делаем заголовок окна — Games с помощью свойства объекта title
tk.title('Game')
# запрещаем менять размеры окна, для этого используем свойство resizable 
tk.resizable(0, 0)
# помещаем наше игровое окно выше остальных окон на компьютере, чтобы другие окна не могли его заслонить. Попробуйте 🙂
tk.wm_attributes('-topmost', 1)
# создаём новый холст — 400 на 500 пикселей, где и будем рисовать игру
canvas = Canvas(tk, width=500, height=400, highlightthickness=0)
# говорим холсту, что у каждого видимого элемента будут свои отдельные координаты 
canvas.pack()
# обновляем окно с холстом
tk.update()
# Описываем класс Ball, который будет отвечать за шарик 
class Ball:
    # конструктор — он вызывается в момент создания нового объекта на основе этого класса
    def __init__(self, canvas, paddle, score, color):
        # задаём параметры объекта, которые нам передают в скобках в момент создания
        self.canvas = canvas
        self.paddle = paddle
        self.score = score
        # цвет нужен был для того, чтобы мы им закрасили весь шарик
        # здесь появляется новое свойство id, в котором хранится внутреннее название шарика
        # а ещё командой create_oval мы создаём круг радиусом 15 пикселей и закрашиваем нужным цветом
        self.id = canvas.create_oval(10,10, 25, 25, fill=color)
        # помещаем шарик в точку с координатами 245,100
        self.canvas.move(self.id, 245, 100)
        # задаём список возможных направлений для старта
        starts = [-2, -1, 1, 2]
        # перемешиваем его 
        random.shuffle(starts)
        # выбираем первый из перемешанного — это будет вектор движения шарика
        self.x = starts[0]
        # в самом начале он всегда падает вниз, поэтому уменьшаем значение по оси y
        self.y = -2
        # шарик узнаёт свою высоту и ширину
        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()
        # свойство, которое отвечает за то, достиг шарик дна или нет. Пока не достиг, значение будет False
        self.hit_bottom = False
    # обрабатываем касание платформы, для этого получаем 4 координаты шарика в переменной pos (левая верхняя и правая нижняя точки)
    def hit_paddle(self, pos):
        # получаем кординаты платформы через объект paddle (платформа)
        paddle_pos = self.canvas.coords(self.paddle.id)
        # если координаты касания совпадают с координатами платформы
        if pos[2] >= paddle_pos[0] and pos[0] <= paddle_pos[2]:
            if pos[3] >= paddle_pos[1] and pos[3] <= paddle_pos[3]:
                # увеличиваем счёт (обработчик этого события будет описан ниже)
                self.score.hit()
                # возвращаем метку о том, что мы успешно коснулись
                return True
        # возвращаем False — касания не было
        return False
    # метод, который отвечает за движение шарика
    def draw(self):
        # передвигаем шарик на заданный вектор x и y
        self.canvas.move(self.id, self.x, self.y)
        # запоминаем новые координаты шарика
        pos = self.canvas.coords(self.id)
        # если шарик падает сверху  
        if pos[1] <= 0:
            # задаём падение на следующем шаге = 2
            self.y = 2
        # если шарик правым нижним углом коснулся дна
        if pos[3] >= self.canvas_height:
            # помечаем это в отдельной переменной
            self.hit_bottom = True
            # выводим сообщение и количество очков
            canvas.create_text(250, 120, text='Вы проиграли', font=('Courier', 30), fill='red')
        # если было касание платформы
        if self.hit_paddle(pos) == True:
            # отправляем шарик наверх
            self.y = -2
        # если коснулись левой стенки
        if pos[0] <= 0:
            # движемся вправо
            self.x = 2
        # если коснулись правой стенки
        if pos[2] >= self.canvas_width:
            # движемся влево
            self.x = -2
#  Описываем класс Paddle, который отвечает за платформы
class Paddle:
    # конструктор
    def __init__(self, canvas, color):
        # canvas означает, что платформа будет нарисована на нашем изначальном холсте
        self.canvas = canvas
        # создаём прямоугольную платформу 10 на 100 пикселей, закрашиваем выбранным цветом и получаем её внутреннее имя 
        self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)
        # задаём список возможных стартовых положений платформы
        start_1 = [40, 60, 90, 120, 150, 180, 200]
        # перемешиваем их
        random.shuffle(start_1)
        # выбираем первое из перемешанных
        self.starting_point_x = start_1[0]
        # перемещаем платформу в стартовое положение
        self.canvas.move(self.id, self.starting_point_x, 300)
        # пока платформа никуда не движется, поэтому изменений по оси х нет
        self.x = 0
        # платформа узнаёт свою ширину
        self.canvas_width = self.canvas.winfo_width()
        # задаём обработчик нажатий
        # если нажата стрелка вправо — выполняется метод turn_right()
        self.canvas.bind_all('<KeyPress-Right>', self.turn_right)
        # если стрелка влево — turn_left()
        self.canvas.bind_all('<KeyPress-Left>', self.turn_left)
        # пока платформа не двигается, поэтому ждём
        self.started = False
        # как только игрок нажмёт Enter — всё стартует
        self.canvas.bind_all('<KeyPress-Return>', self.start_game)
    # движемся вправо 
    def turn_right(self, event):
        # будем смещаться правее на 2 пикселя по оси х
        self.x = 2
    # движемся влево
    def turn_left(self, event):
        # будем смещаться левее на 2 пикселя по оси х
        self.x = -2
    # игра начинается
    def start_game(self, event):
        # меняем значение переменной, которая отвечает за старт движения платформы
        self.started = True
    # метод, который отвечает за движение платформы
    def draw(self):
        # сдвигаем нашу платформу на заданное количество пикселей
        self.canvas.move(self.id, self.x, 0)
        # получаем координаты холста
        pos = self.canvas.coords(self.id)
        # если мы упёрлись в левую границу 
        if pos[0] <= 0:
            # останавливаемся
            self.x = 0
        # если упёрлись в правую границу 
        elif pos[2] >= self.canvas_width:
            # останавливаемся
            self.x = 0
#  Описываем класс Score, который отвечает за отображение счетов
class Score:
    # конструктор
    def __init__(self, canvas, color):
        # в самом начале счёт равен нулю
        self.score = 0
        # будем использовать наш холст
        self.canvas = canvas
        # создаём надпись, которая показывает текущий счёт, делаем его нужно цвета и запоминаем внутреннее имя этой надписи
        self.id = canvas.create_text(450, 10, text=self.score, font=('Courier', 15), fill=color)
    # обрабатываем касание платформы
    def hit(self):
        # увеличиваем счёт на единицу
        self.score += 1
        # пишем новое значение счёта 
        self.canvas.itemconfig(self.id, text=self.score)
# создаём объект — зелёный счёт 
score = Score(canvas, 'green')
# создаём объект — белую платформу
paddle = Paddle(canvas, 'White')
# создаём объект — красный шарик 
ball = Ball(canvas, paddle, score, 'red')
# пока шарик не коснулся дна 
while not ball.hit_bottom:
    # если игра началась и платформа может двигаться
    if paddle.started == True:
        # двигаем шарик
        ball.draw()
        # двигаем платформу
        paddle.draw()
    # обновляем наше игровое поле, чтобы всё, что нужно, закончило рисоваться
    tk.update_idletasks()
    # обновляем игровое поле и смотрим за тем, чтобы всё, что должно было быть сделано — было сделано
    tk.update()
    # замираем на одну сотую секунды, чтобы движение элементов выглядело плавно
    time.sleep(0.01)
# если программа дошла досюда, значит, шарик коснулся дна. Ждём 3 секунды, пока игрок прочитает финальную надпись, и завершаем игру
time.sleep(3)

Пишем игру на Python

Что дальше

На основе этого кода вы можете сделать свою модификацию игры:

  • добавить второй шарик;
  • раскрасить элементы в другой цвет;
  • поменять размеры шарика; поменять скорость платформы;
  • сделать всё это сразу;
  • поменять логику программы на свою.

Время на прочтение
8 мин

Количество просмотров 327K

Многие разработчики приходят в разработку ПО, потому что хотят создавать игры. Не все могут стать профессиональными разработчиками игр, но любой может создавать собственные игры из интереса (а может быть, и с выгодой). В этом туториале, состоящем из пяти частей, я расскажу вам, как создавать двухмерные однопользовательские игры с помощью Python 3 и замечательного фреймворка PyGame.

(Остальные части туториала: вторая, третья, четвёртая, пятая.)

Мы создадим версию классической игры Breakout. Освоив этот туториал, вы будете чётко понимать, что необходимо для создания игры, познакомитесь с возможностями Pygame и напишете собственный пример игры.

Мы реализуем следующие функции и возможности:

  • простые стандартные GameObject и TextObject
  • простой стандартный Game object
  • простая стандартная кнопка
  • файл конфигурации
  • обработка событий клавиатуры и мыши
  • кирпичи, ракетка и мяч
  • управление движением ракетки
  • обработка коллизий мяча с объектами игры
  • фоновое изображение
  • звуковые эффекты
  • расширяемая система спецэффектов

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

Готовый исходный код выложен здесь.

Краткое введение в программирование игр

Главное в играх — перемещение пикселей на экране и издаваемый шум. Почти во всех видеоиграх есть эти элементы. В этой статье мы не будем рассматривать клиент-серверные и многопользовательские игры, для которых требуется много сетевого программирования.

Основной цикл

Основной цикл (main loop) игры выполняется и обновляет экран через фиксированные интервалы времени. Они называются частотой кадров и определяют плавность перемещения. Обычно игры обновляют экран 30-60 раз в секунду. Если частота будет меньше, то покажется, что объекты на экране дёргаются.

Внутри основного цикла есть три основных операции: обработка событий, обновление состояния игры и отрисовка текущего состояния на экране.

Обработка событий

События в игре состоят из всего, что происходит за пределами управления кода игры, но относится к выполнению игры. Например, если в Breakout игрок нажимает клавишу «стрелка влево», то игре нужно переместить ракетку влево. Стандартными событиями являются нажатия (и отжатия) клавиш, движение мыши, нажатия кнопок мыши (особенно в меню) и события таймера (например, действие спецэффекта может длиться 10 секунд).

Обновление состояния

Сердце любой игры — это её состояние: всё то, что она отслеживает и отрисовывает на экране. В случае Breakout к состоянию относятся положение всех кирпичей, позиция и скорость мяча, положение ракетки, а также жизни и очки.

Существует также вспомогательное состояние, позволяющее управлять игрой:

  • Отображается ли сейчас меню?
  • Закончена ли игра?
  • Победил ли игрок?

Отрисовка

Игре нужно отображать своё состояние на экране, в том числе отрисовывать геометрические фигуры, изображения и текст.

Игровая физика

В большинстве игр симулируется физическое окружение. В Breakout мяч отскакивает от объектов и имеет очень приблизительную систему физики твёрдого тела (если это можно так назвать).

В более сложных играх могут использоваться более изощрённые и реалистичные физические системы (особенно в 3D-играх). Стоит также отметить, что в некоторых играх, например, в карточных, физики почти нет, и это совершенно нормально.

ИИ (искусственный интеллект)

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

Например, враги преследуют игрока и знают о его местоположении. В Breakout нет никакого ИИ. Игрок сражается с холодными и твёрдыми кирпичами. Однако ИИ в играх часто очень прост и всего лишь следует простым (или сложным) правилам, обеспечивающим псевдоразумные результаты.

Воспроизведение звука

Воспроизведение звука — ещё один важный аспект игр. В общем случае существует два типа звука: фоновая музыка и звуковые эффекты. В Breakout я реализую только звуковые эффекты, которые воспроизводятся при различных событиях.

Фоновая музыка — это просто музыка, постоянно играющая на фоне. В некоторых играх она не используется, а в некоторых меняется на каждом уровне.

Жизни, очки и уровни

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

Знакомство с Pygame

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

Что такое Pygame?

Pygame — это фреймворк языка Python для программирования игр. Он создан поверх SDL и обладает всем необходимым:

  • зрелостью
  • хорошим сообществом
  • открытым исходным кодом
  • кроссплатформенностью
  • качественной документацией
  • множеством примеров игр
  • простотой изучения

Установка Pygame

Введите pip install pygame, чтобы установить фреймворк. Если вам нужно что-то ещё, то следуйте инструкциям из раздела Getting Started в Wiki проекта. Если у вас, как и у меня, macOS Sierra, то могут возникнуть проблемы. Мне удалось установить Pygame без сложностей, и код работает отлично, но окно игры никогда не появляется.

Это станет серьёзным препятствием при запуске игры. В конце концов мне пришлось запускать её в Windows внутри VirtualBox VM. Надеюсь, ко времени прочтения этой статьи проблема будет решена.

Архитектура игры

Играм нужно управлять кучей информации и выполнять почти одинаковые операции со множеством объектов. Breakout — это небольшая игра, однако попытка управлять всем в одном файле может оказаться слишком утомительной. Поэтому я решил создать файловую структуру и архитектуру, которая подойдёт и для гораздо более крупных игр.

Структура папок и файлов

├── Pipfile
├── Pipfile.lock
├── README.md
├── ball.py
├── breakout.py
├── brick.py
├── button.py
├── colors.py
├── config.py
├── game.py
├── game_object.py
├── images
│   └── background.jpg
├── paddle.py
├── sound_effects
│   ├── brick_hit.wav
│   ├── effect_done.wav
│   ├── level_complete.wav
│   └── paddle_hit.wav
└── text_object.py

Pipfile и Pipfile.lock — это современный способ управления зависимостями в Python. Папка images содержит изображения, используемые игрой (в нашей версии будет только фоновое изображение), а в папке sound_effects directory лежат короткие звуковые клипы, используемые (как можно догадаться) в качестве звуковых эффектов.

Файлы ball.py, paddle.py и brick.py содержат код, относящийся к каждому из этих объектов Breakout. Подробнее я рассмотрю их в следующих частях туториала. Файл text_object.py содержит код отображения текста на экране, а в файле background.py содержится игровая логика Breakout.

Однако существует несколько модулей, создающих произвольный «скелет» общего назначения. Определённые в них классы можно будет использовать в других играх на основе Pygame.

Класс GameObject

GameObject представляет собой визуальный объект, знающий о том, как себя рендерить, сохранять свои границы и перемещаться. В Pygame есть и класс Sprite, исполняющий похожую роль, но в этом туториале я хочу показать вам, как всё работает на низком уровне, а не полагаться слишком активно на готовую магию. Вот как выглядит класс GameObject:

from pygame.rect import Rect


class GameObject:
    def __init__(self, x, y, w, h, speed=(0,0)):
        self.bounds = Rect(x, y, w, h)
        self.speed = speed

    @property
    def left(self):
        return self.bounds.left

    @property
    def right(self):
        return self.bounds.right

    @property
    def top(self):
        return self.bounds.top

    @property
    def bottom(self):
        return self.bounds.bottom

    @property
    def width(self):
        return self.bounds.width

    @property
    def height(self):
        return self.bounds.height

    @property
    def center(self):
        return self.bounds.center

    @property
    def centerx(self):
        return self.bounds.centerx

    @property
    def centery(self):
        return self.bounds.centery

    def draw(self, surface):
        pass

    def move(self, dx, dy):
        self.bounds = self.bounds.move(dx, dy)

    def update(self):
        if self.speed == [0, 0]:
            return

        self.move(*self.speed)

GameObject предназначен для того, чтобы быть базовым классом для других объектов. Он непосредственно раскрывает множество свойств его прямоугольника self.bounds, а в своём методе update() он перемещает объект в соответствии с его текущей скоростью. Он ничего не делает в своём методе draw(), который должен быть переопределён подклассами.

Класс Game

Класс Game — это ядро игры. Он выполняется в основном цикле. В нём есть множество полезных возможностей. Давайте разберём его метод за методом.

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

Элемент self.objects хранит все игровые объекты, которые должны рендериться и обновляться. Различные обработчики управляют списками функций-обработчиков, которые должны выполняться при определённых событиях.

import pygame
import sys

from collections import defaultdict


class Game:
    def __init__(self, 
                 caption, 
                 width, 
                 height, 
                 back_image_filename, 
                 frame_rate):
        self.background_image = 
            pygame.image.load(back_image_filename)
        self.frame_rate = frame_rate
        self.game_over = False
        self.objects = []
        pygame.mixer.pre_init(44100, 16, 2, 4096)
        pygame.init()
        pygame.font.init()
        self.surface = pygame.display.set_mode((width, height))
        pygame.display.set_caption(caption)
        self.clock = pygame.time.Clock()
        self.keydown_handlers = defaultdict(list)
        self.keyup_handlers = defaultdict(list)
        self.mouse_handlers = []

Методы update() и draw() очень просты. Они обходят все управляемые игровые объекты и вызывают соответствующие им методы. Если два объекта накладываются друг на друга на экране, то порядок списка объектов определяет, какой из них будет рендериться первым, а остальные будут частично или полностью его перекрывать.

    def update(self):
        for o in self.objects:
            o.update()

    def draw(self):
        for o in self.objects:
            o.draw(self.surface)

Метод handle_events() слушает события, генерируемые Pygame, такие как события клавиш и мыши. Для каждого события он вызывает все функции-обработчики, которые должны обрабатывать события соответствующих типов.

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                for handler in self.keydown_handlers[event.key]:
                    handler(event.key)
            elif event.type == pygame.KEYUP:
                for handler in self.keydown_handlers[event.key]:
                    handler(event.key)
            elif event.type in (pygame.MOUSEBUTTONDOWN, 
                                pygame.MOUSEBUTTONUP, 
                                pygame.MOUSEMOTION):
                for handler in self.mouse_handlers:
                    handler(event.type, event.pos)

Наконец, метод run() выполняет основной цикл. Он выполняется до тех пор, пока элемент game_over не принимает значение True. В каждой итерации он рендерит фоновое изображение и вызывает по порядку методы handle_events(), update() и draw().

Затем он обновляет экран, то есть записывает на физический дисплей всё содержимое, которое было отрендерено на текущей итерации. И последнее, но не менее важное — он вызывает метод clock.tick() для управления тем, когда будет вызвана следующая итерация.

    def run(self):
        while not self.game_over:
            self.surface.blit(self.background_image, (0, 0))

            self.handle_events()
            self.update()
            self.draw()

            pygame.display.update()
            self.clock.tick(self.frame_rate)

Заключение

В этой части мы изучили основы программирования игр и все компоненты, участвующие в создании игр. Также мы рассмотрели сам Pygame и узнали, как его установить. Наконец, мы погрузились в архитектуру игры и изучили структуру папок, классы GameObject и Game.

Во второй части мы рассмотрим класс TextObject, используемый для рендеринга текста на экране. Мы создадим основное окно, в том числе и фоновое изображение, а затем узнаем, как отрисовывать объекты (мяч и ракетку).

Один из популярных мотивов обучения программированию — желание написать собственную игру. В этой статье мы сравним несколько игровых движков на практике.

Чтобы извлечь пользу из этого руководства, нужно уметь программировать на Python, а также знать, как в языке реализуется объектно-ориентированное программирование. Для независимой установки движков мы советуем использовать виртуальные окружения.

Игровые движки для Python обычно являются библиотеками, которые можно установить с помощью менеджера pip или загрузить с площадок распространения кода. В противовес к библиотекам существуют автономные среды, предназначенные исключительно для написания игр:

  • Unreal Engine
  • Unity
  • Godot

Такие программы отличаются от игровых движков Python во многих аспектах:

  • Поддерживаемые языки. Среды программирования игр обычно написаны на C-подобных языках и предлагают использовать соответствующие языки для написания игр: в Unity это C#, в Unreal Engine — C++.
  • Поддержка платформ. Автономные среды позволяют без дополнительных усилий создавать игры для различных платформ, включая мобильные устройства. Напротив, перенос Python-игры на мобильные устройства — задача не из лёгких.
  • Лицензирование. Игры, написанные с использованием автономного игрового движка, имеют особые условия лицензирования и дальнейшего распространения.

Зачем же вообще использовать Python для написания игр? Использование GameDev-сред требует изучения документации и обычно — овладения новым языком программирования. В то же время при работе с игровыми движками на Python питонисты применяют в основном уже имеющиеся знания. Это помогает быстрее двигаться вперед и получить первый результат.

Критерии отбора Python-движков в этой статье:

  • популярность,
  • актуальная поддержка,
  • качественная документация.

Для каждого движка мы рассмотрим способ установки, базовую терминологию и основные возможности для реализации игр.

Pygame

Pygame — первое, что приходит на ум, когда кто-нибудь начинает разговор об играх на Python.

Pygame расширяет собой библиотеку SDL (сокр. от Simple DirectMedia Layer), предназначенную для межплатформенного доступа к мультимедийным аппаратным компонентам системы: мыши, клавиатуре, джойстику, аудио- и видеоустройствам.

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

Установка Pygame

После создания и активации виртуального окружения установите библиотеку с помощью pip :

        (venv) $ python -m pip install pygame

    

Чтобы проверить результат установки, запустите пример, поставляемый вместе с библиотекой:

        (venv) $ python -m pygame.examples.aliens

    

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

Базовые концепции Pygame

Game loop. Для управления ходом игры используется понятие игрового цикла. Функциональность игрового цикла реализует автор, а Pygame предоставляет необходимые методы и функции. Каждая итерация игрового цикла называется кадром (frame). За один кадр игра выполняет четыре действия:

  1. Обработка пользовательского ввода от мыши, клавиатуры или джойстика с помощью модели событий.
  2. Обновление состояния игровых объектов: спрайтов (образы героев и предметов), изображений, шрифтов и цветов. Объекты описываются подходящими структурами данных или с помощью классов Pygame.
  3. Обновление дисплея и аудиовыхода. Pygame обеспечивает абстрактный доступ к оборудованию для отображения картинки и передачи звука с помощью внутренних модулей display, mixer и music.
  4. Сохранение или изменение скорости игры. Модуль pygame.time позволяет авторам игр контролировать скорость игры. За счёт этого игра работает с одинаковой скоростью на различном оборудовании — библиотека гарантирует завершение каждого кадра в течение заданного периода времени.

Базовый пример Pygame

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

🐍🕹️ Как написать игру на Python: 5 игровых движков

        # Импортируем и инициализируем библиотеку
import pygame

pygame.init()

# Устанавливаем ширину и высоту окна в пикселях
WIDTH = 800
HEIGHT = 600

# Настраиваем окно отрисовки
screen = pygame.display.set_mode([WIDTH, HEIGHT])

# Игровой цикл выполняется, пока пользователь не захочет выйти
running = True
while running:

    # Нажал ли пользователь кнопку зыкрытия окна?
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Заполняем фон белым цветом
    screen.fill((255, 255, 255))

    # Рисуем синий круг в центре экрана радиусом 50
    pygame.draw.circle(screen, (0, 0, 255), (WIDTH // 2, HEIGHT // 2), 50)

    # Рисуем красный контурный квадрат в верхнем левом углу экрана
    red_square = pygame.Rect((50, 50), (100, 100))
    pygame.draw.rect(screen, (200, 0, 0), red_square, 1)

    # Рисуем оранжевый текст с кеглем 60
    text_font = pygame.font.SysFont("any_font", 60)
    text_block = text_font.render(
        "Hello, World! From Pygame", False, (200, 100, 0)
    )
    screen.blit(text_block, (50, HEIGHT - 50))

		# Обновляем экран
    pygame.display.flip()

# Цикл завершился! Уходим.
pygame.quit()

    

Для запуска кода используйте команду:

        (venv) $ python pygame/pygame_basic.py

    

Игровой цикл есть даже в такой скромной программе. В примере он управляется переменной running. Её установка в значение False завершает выполнение программы.

Обработка событий. События хранятся в виде очереди, из неё события извлекаются с помощью pygame.event.get(). В рассматриваемом случае обрабатывается только событие pygame.QUIT, генерируемое при закрытии пользователем окна программы. При обработке этого события мы устанавливаем running = False.

Отрисовка фигур и текста. В то время как для отрисовки фигур просто используются специальные методы, отрисовка текста выглядит несколько сложнее. Сначала выбираем шрифт и создаем объект шрифта. Далее вызываем метод .render() и передаем ему текст, шрифт и цвет. В ответ метод создает объект класса Surface. Этот объект мы копируем на экран screen, используя его метод screen.blit().

В конце игрового цикла мы даем команду отобразить на дисплее все, что было ранее нарисовано.

Продвинутый вариант игры на Pygame

Чтобы лучше изучить возможности Pygame, напишем настоящую игру.

Геймплей. Главный герой представлен одиночным спрайтом, управляемым перемещением мыши. Через равные промежутки времени на экране одна за другой появляются монеты. Когда персонаж перемещается поверх монеты или сталкивается с ней, монета исчезает, а игрок получает 10 очков. По ходу игры монеты начинают добавляться быстрее. Игра заканчивается, когда на экране присутствует более десяти монет или игрок закрыл окно.

При запуске игра выглядит примерно вот так

При запуске игра выглядит примерно вот так
        # Импорт и инициализация
import pygame

# Для случайного размещения монет
from random import randint

# Для поиска ресурсов
from pathlib import Path

# Для аннотации типов
from typing import Tuple

# Устанавливаем размеры окна
WIDTH = 800
HEIGHT = 600

# Как часто должны генерироваться монеты (мс)
coin_countdown = 2500
coin_interval = 100

# Сколько монет должно быть на экране, чтобы игра закончилась
COIN_COUNT = 10

# Определяем спрайт для игрока
class Player(pygame.sprite.Sprite):
    def __init__(self):
        """Инициализирует спрайт игрока"""
        super(Player, self).__init__()

        # Получаем изображение персонажа
        player_image = str(
            Path.cwd() / "pygame" / "images" / "alien_green_stand.png"
        )
        # Загружаем изображение, настраиваем альфа канал для прозрачности
        self.surf = pygame.image.load(player_image).convert_alpha()
        # Сохраняем в прямоугольнике, чтобы перемещать объект
        self.rect = self.surf.get_rect()

    def update(self, pos: Tuple):
        """Обновляет позицию персонажа

        Аргументы:
            pos {Tuple} -- (X,Y) позиция для движения персонажа
        """
        self.rect.center = pos

# Определяем спрайт для монет
class Coin(pygame.sprite.Sprite):
    def __init__(self):
        """Инициализирует спрайт монеты"""
        super(Coin, self).__init__()

        # Получаем изображение монеты
        coin_image = str(Path.cwd() / "pygame" / "images" / "coin_gold.png")

        # Загружаем изображение, настраиваем альфа канал для прозрачности
        self.surf = pygame.image.load(coin_image).convert_alpha()

        # Задаем стартовую позицию, сгенерированную случайным образом
        self.rect = self.surf.get_rect(
            center=(
                randint(10, WIDTH - 10),
                randint(10, HEIGHT - 10),
            )
        )

# Инициализируем движок
pygame.init()

# Настраиваем окно
screen = pygame.display.set_mode(size=[WIDTH, HEIGHT])

# Скрываем курсор мыши
pygame.mouse.set_visible(False)

# Запускаем часы для фиксации времени фрейма
clock = pygame.time.Clock()

# Создаем событие для добавления монеты
ADDCOIN = pygame.USEREVENT + 1
pygame.time.set_timer(ADDCOIN, coin_countdown)

# Настраиваем список монет
coin_list = pygame.sprite.Group()

# Инициализируем счет
score = 0

# Определяем звук для столкновения с монетой 
coin_pickup_sound = pygame.mixer.Sound(
    str(Path.cwd() / "pygame" / "sounds" / "coin_pickup.wav")
)

# Создаем спрайт героя и устанавливаем на заданную позицию
player = Player()
player.update(pygame.mouse.get_pos())

# Цикл событий
running = True
while running:

    # Проверяем, нажал ли пользователь кнопку закрытия окна
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

        # Определяем, нужно ли добавлять новую монету
        elif event.type == ADDCOIN:
            # Добавляем новую монету
            new_coin = Coin()
            coin_list.add(new_coin)

            # Ускоряем игру, если на экранее менее 3 монет
            if len(coin_list) < 3:
                coin_countdown -= coin_interval
            # Ограничиваем скорость
            if coin_countdown < 100:
                coin_countdown = 100

            # Останавливаем предыдущий таймер
            pygame.time.set_timer(ADDCOIN, 0)

            # Запускаем новый таймер
            pygame.time.set_timer(ADDCOIN, coin_countdown)

    # Обновляем позицию персонажа
    player.update(pygame.mouse.get_pos())

    # Проверяем, столкнулся ли игрок с монетой и удаляем, если это так
    coins_collected = pygame.sprite.spritecollide(
        sprite=player, group=coin_list, dokill=True
    )
    for coin in coins_collected:
        # Каждая монета стоит 10 очков
        score += 10
        # Воспроизводим звук для монеты
        coin_pickup_sound.play()

    # Проверяем, не слишком ли много монет
    if len(coin_list) >= COIN_COUNT:
        # Если монет много, останавливаем игру
        running = False

    # Указываем цвет фона
    screen.fill((255, 170, 164))

    # Рисуем следующие монеты
    for coin in coin_list:
        screen.blit(coin.surf, coin.rect)

    # Отрисовываем персонажа
    screen.blit(player.surf, player.rect)

    # Выводим текущий счет
    score_font = pygame.font.SysFont("any_font", 36)
    score_block = score_font.render(f"Score: {score}", False, (0, 0, 0))
    screen.blit(score_block, (50, HEIGHT - 50))

    # Отображаем всё на экране
    pygame.display.flip()

    # Скорость обновления - 30 кадров в секунду
    clock.tick(30)

# Готово! Печатаем итоговый результат
print(f"Game over! Final score: {score}")

# Делаем курсор мыши вновь видимым
pygame.mouse.set_visible(True)

# Выходим из игры
pygame.quit()

    

Спрайты в Pygame предоставляют лишь базовую функциональность — в нашем коде мы их расширяем, создавая подклассы для персонажа (Player) и монет (Coin). Объекты спрайтов сохраняются в self.surf и позиционируются с помощью свойства self.rect.

Вывод монет. Чтобы добавлять монеты на экран через равные промежутки времени, мы используем таймер time.set_timer(), отсчитывающий время до события в миллисекундах (coin_countdown). Добавлению монет соответствует событие ADDCOIN. Событие создает новый объект Coin и добавляет его в coin_list. Далее проверяется количество монет на экране. Если монет меньше трех, то coin_countdown уменьшается. Предыдущий таймер останавливается и запускается новый.

Движение персонажа. При движении игрок сталкивается с монетами и собирает их. При этом мы удаляем соответствующий объект из coin_list, обновляем счёт и воспроизводим звук. Автор игры также должен помнить об очистке экрана, отрисовке спрайтов в правильном порядке и отображении счёта.

Заключение по Pygame

Pygame — мощная и хорошо зарекомендовавшая себя библиотека, но у неё есть недостатки. Автор игры должен самостоятельно оформить базовое поведение спрайтов, реализовать игровые циклы и основные обработчики событий. Как вы увидите далее, более современные игровые движки позволяют достичь аналогичных результатов при меньшем объёме выполняемой работы.

Pygame Zero

С одним задачами Pygame справляется хорошо, в других — сказывается возраст библиотеки. Для новичков в написании игр есть вариант получше — Pygame Zero. Эта библиотека разработана для образовательных задач, поэтому текст документации будет понятен даже для новичков в программировании. Кроме того, есть подробное пошаговое руководство.

Установка Pygame Zero

Pygame Zero можно установить, как и любую другую библиотеку Python:

        (venv) $ python -m pip install pgzero

    

Базовый пример на Pygame Zero

Pygame Zero автоматизирует многие вещи, которые программистам приходится описывать вручную при использовании стандартного движка Pygame. По умолчанию Pygame Zero предоставляет создателю игры:

  • игровой цикл — его не нужно писать;
  • модель событий для отрисовки, обновлений и обработки ввода;
  • единую обработку изображений, текста и звука;
  • класс спрайтов и методы анимации для пользовательских спрайтов.

В результате код базовой программы Pygame Zero оказывается более кратким, чем на Pygame.

💡 Чтобы не «раздувать» текст статьи, мы отсылаем читателей посмотреть этот и следующие примеры кода мы в упомянутому выше репозиторию.

Отрисовка окна. Pygame Zero автоматически распознает, что константы WIDTH и HEIGHT относятся к размеру окна. Pygame Zero также предоставляет базовый код для закрытия окна, так что закрытие окна не нужно описывать в обработчике.

Отрисовка фигур и текста. Поскольку Pygame Zero основан на Pygame, он наследует часть кода для отрисовки фигур, но вывод текста выглядит проще и занимает один вызов вместо трех для обычного Pygame.

        screen.draw.text(
        f"Score: {score}",
        (50, HEIGHT - 50),
        fontsize=48,
        color="black",
    )

    

Запуск программ Pygame Zero осуществляется из командной строки с помощью команды:

        (venv) $ python pygame_zero/pygame_zero_basic.py

    

Продвинутый вариант игры на Pygame Zero

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

🐍🕹️ Как написать игру на Python: 5 игровых движков

Спрайты в Pygame Zero называются Actors. Их характеристики требуют некоторых пояснений, так как эти сведения используются в коде примера:

  1. Для каждого Actor задаются, как минимум, изображение и положение.
  2. Все изображения должны располагаться во вложенной папке с именем ./images/. Названия файлов должны содержать лишь строчные буквы, цифры или символы подчеркивания.
  3. При ссылке на изображение используется имя файла без расширения. Например, если изображение называется alien.png, то в программе на него ссылаются строкой "alien" .

Вывод монет через равные промежутки времени производится с помощью метода clock.schedule(). Метод принимает вызываемую функцию (в нашем случае add_coin) и количество секунд перед вызовом самой функции. Запускаемая функция add_coin() создает объект класса Actor и добавляет спрайт в глобальный список видимых монет.

Перемещение персонажа задействует обработчик события on_mouse_move(). В исходном положении спрайт персонажа находится в центре экрана. Положение мыши хранится в глобальной переменной.

Столкновение с монетой приводит к воспроизведению звука и увеличению счёта. Удаляемая монета добавляется в список coin_remove_list. При обработке столкновений мы проверяем, не слишком ли много монет сейчас находится на экране. Если это так, мы заканчиваем игру, прекращаем создавать новые монеты и выводим окончательный счёт игры.

Обновление состояний. Функция update() вызывается Pygame Zero один раз за кадр. Мы используем её, чтобы перемещать объекты класса Actor и обновлять состояния игровых объектов. Также один раз за кадр для отрисовки вызывается функция draw().

Заключение по Pygame Zero

Реализация игры на Pygame Zero заняла 152 строки кода вместо 182 строк на обычном Pygame. Хотя количество строк сопоставимо, версия на Pygame Zero получилась более ясной в плане кода и проще для понимания и дополнения.

Arcade

Arcade — движок Python для создания игр с современными графикой и звуком, разработанный профессором Полом Крэйвеном из Симпсон-колледжа (Айова, США).

Arcade базируется на мультимедийной библиотеке pyglet и выгодно отличается, как от Pygame, так и от Pygame Zero:

  • поддерживает современную OpenGL-графику;
  • поддерживает аннотации типов Python 3;
  • умеет анимировать с анимированными спрайтами;
  • имеет согласованные имена команд, функций и параметров;
  • поощряет отделение игровой логики от кода, обеспечивающего отображение;
  • сокращает использование шаблонного кода;
  • имеет поддерживаемую и актуальную документацию, в том числе несколько учебных пособий и полные примеры игр на Python;
  • имеет встроенные физические движки для игр с видом сверху и игр-платформеров.

Arcade продолжает развиваться и хорошо поддерживается сообществом, а автор быстро реагирует на сообщения об ошибках.

Установка Arcade

Чтобы установить Arcade и его зависимости, используйте соответствующую команду pip:

        (venv) $ python -m pip install arcade

    

Есть также инструкции по установке для Windows, macOS и Linux. При желании можно собрать движок из исходного кода.

Базовый пример на Arcade

🐍🕹️ Как написать игру на Python: 5 игровых движков

Начнем с базового примера с фигурами и текстом. Чтобы запустить код, используйте следующую команду:

        (venv) $ python arcade/arcade_basic.py

    

Начало координат (0, 0) расположено в левом нижнем углу экрана. Это отличает Arcade от большинства игровых движков, в которых начало координат расположено в левом верхнем углу.

Arcade — это объектно-ориентированная библиотека. Для подклассов игр используется класс arcade.Window, для настройки игры вызывается super().__init().

Отрисовка всего что есть на экране производится обработчиком событий .on_draw() . Обработчик стартует с вызова .start_render(), который сообщает Arcade подготовить окно для рисования. Напоминает pygame.flip() для отрисовки в Pygame.

Фигуры и цвета. Каждый из основных методов рисования фигур в Arcade начинается с draw_*. Arcade умеет отрисовывать множество разнообразных фигур и сотни именованных цветов из пакета arcade.color. Также можно использовать кортежи RGB или RGBA.

Продвинутый вариант игры на Arcade

Чтобы показать, чем Arcade отличается от других игровых движков, запрограммируем ту же игру с монетами, что и раньше.

Инициализация. Объектно-ориентированный характер Arcade позволяет отделить инициализацию игры от инициализации отдельного уровня: игра инициализируется методом .__init__(), а уровни настраиваются и перезапускаются с помощью .setup(). Это отличный шаблон для использования даже в играх с одним уровнем, как в нашем случае.

Спрайты создаются в виде объекта класса arcade.Sprite, которому задан путь к изображению — Arcade поддерживает pathlib-пути.

Создание монет планируется с помощью arcade.schedule() и вызова self.add_coin() через равные промежутки времени. Метод .add_coin() создает новый спрайт монеты в случайном месте и добавляет его в список.

Перемещение персонажа мышью реализуется с помощью метода .on_mouse_motion(). Метод arcade.clamp() гарантирует, что координаты центра страйпа не выйдут за пределы экрана.

Столкновение с монетой обрабатывается методом .on_update(). Метод arcade.check_for_collision_with_list() возвращает список всех спрайтов, которые столкнулись с указанным спрайтом. Код проходит по этому списку, увеличивает счет и воспроизводит звук. Метод .on_update() проверяет, не слишком ли много монет сейчас есть на экране.

Заключение по Arcade

Реализация Arcade так же удобна для чтения и хорошо структурирована, как и код для Pygame Zero. Однако программный код занимает больше места — 194 строки. Причина в том, что в Arcade заложены возможности, которые лучше реализуются в более крупных играх:

  • анимированные спрайты;
  • встроенные физические движки;
  • поддержка сторонних игровых карт;
  • системы работы с частицами и шейдирование.

Авторы игр, пришедшие в Arcade из Pygame Zero обнаружат здесь знакомую структуру, но более мощные и продвинутые функции.

adventurelib

Мир игр полон самых различных жанров. Бывают и такие игры, как например, Zork, в которых основой игрового повествования является текст. Такой тип игр ещё называют Interactive Fiction. Для создания текстовых игр на Python существует движок adventurelib. Библиотека отлично подойдет для тех, кто хочет создать текстовую игру без необходимости писать парсер языка.

Установка adventurelib

adventurelib доступен на PyPI и может быть установлен с помощью соответствующей команды pip :

        (venv) $ python -m pip install adventurelib

    

Библиотеку также можно установить из репозитория GitHub, сохранив в той же папке, что и ваша игра, и использовать напрямую.

Базовые концепции adventurelib

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

Чтобы запустить код из примера, используйте следующую команду:

        (venv) $ python adventurelib/adventurelib_basic.py

    

Декоратор @when. Текстовые игры в значительной степени полагаются на синтаксический анализ пользовательского ввода. Библиотека определяет текст, который вводит игрок, как команду (command) **и предоставляет для описания команд декоратор @when(). Например, декоратор @when("look") добавляет команду в список допустимых команд и связывает с ней функцией look(). Всякий раз, когда игрок набирает look, adventurelib вызывает соответствующую функцию. Для удобства игроков и разработчиков команды нечувствительны к регистру.

Несколько команд могут использовать одну и ту же функцию. Функция go() декорирована девятью различными командами, чтобы игрок мог перемещаться по игровому миру. В скриншоте ниже есть три из них: south, east, north.

🐍🕹️ Как написать игру на Python: 5 игровых движков

Предметы. В текстовых играх часто есть предметы, которые необходимо собирать, чтобы открывать новые области игры или решать головоломки. Или это могут быть персонажи, с которыми игрок может взаимодействовать. Движок adventurelib предоставляет класс Item для определения и предметов, и персонажей по их именам и псевдонимам.

Конструктор Item()принимает одну или несколько строк. Первая строка — имя предмета, которое используется при печати. Остальные строки используются в качестве псевдонимов, чтобы игроку не приходилось вводить полное имя объекта.

Взаимодействие с предметами. Часто команды, которые вводит игрок, направлены на конкретный предмет. Разработчик игры может задать дополнительный контекст команды. Для этого в описании декоратора @when() используются слова, написанные заглавными буквами.

💡 Пример контекста команды можно увидеть в функции look_at(), которая принимает строковый параметр с именем item. В декораторах этой функции, определяющих команды look at и inspect, слово ITEM выступает в качестве переменной-заполнителя текста, следующего за командой. Этот текст и передается функции look_at() в качестве входного значения. Например, если игрок введет look at book, то item внутри функции получит строковое значение "book".

Реплики. Для вывода реплик используйте функцию say(), которая отлично справляется с выводом многострочного текста. Пример использования есть в теле функции look() — всякий раз, когда игрок набирает look, функция say() выводит в консоль описание текущей комнаты.

Комнаты. Для определения различных областей игрового мира библиотека adventurelib предоставляет класс Room. При создании комнаты конструктору Room() передается описание комнаты и связь с другими комнатами с помощью свойств .north, .south, .east и .west.

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

Ключ имеет не только имя и псевдонимы, но и метод, с помощью которого используется в игре. Метод key.use_item вызывается, когда игрок пытается использовать предмет, набрав строку use key.

Коллекции предметов, таких как инвентарь игрока или предметы на полу комнаты, хранятся в объекте класса Bag. Эти объекты-коллекции позволяют добавлять, удалять предметы или проверять содержимое.

В нашем коде определены четыре Bag-объекта: три для каждой комнаты и один для инвентаря, собираемого игроком. Для добавления предметов в инвентарь используется функция get(), Чтобы взять какой-либо предмет из имеющегося инвентаря — функция take(). При переносе предмета в инвентарь, он удаляется из Bag-объекта комнаты.

Продвинутый вариант игры с использованием adventurelib

Чтобы продемонстрировать другие возможности движка, мы создадим более увлекательное текстовое приключение.

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

В игре есть несколько областей:

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

Итак, есть несколько предметов для сбора, включая еду и оружие. И есть персонажи, с которыми можно взаимодействовать. Ещё нужно запрограммировать систему боя, чтобы игрок мог сразиться с великаном.

Программный код игры разбит на несколько файлов:

  • adventurelib_game_rooms.py описывает комнаты и области;
  • adventurelib_game_items.py определяет предметы и их атрибуты;
  • adventurelib_game_characters.py описывает персонажей, с которыми может взаимодействовать игрок;
  • adventurelib_game.py собирает всё вместе, добавляет команды и запускает игру.

Игру можно запустить с помощью следующей команды:

        (venv) $ python adventurelib/adventurelib_game.py

    

В предыстории мы наметили игровые области и пути, по которым может перемещаться игрок. Они показаны на блок-схеме ниже.

🐍🕹️ Как написать игру на Python: 5 игровых движков

Каждая область имеет свои свойства:

  • название, краткое и подробное описания;
  • предметы и персонажи;
  • заблокированные выходы;
  • указание на то, был ли уже игрок в этой области или нет.

Используем ООП. Чтобы быть уверенными, что каждая область имеет собственный экземпляр каждого из этих свойств, мы создадим в файле у подкласс Room с именем GameArea. Предметы в каждой комнате хранятся в объекте класса Bag с именем items, а персонажи — в characters.

Игровые предметы определены в adventurelib_game_items.py как объекты типа Item(). Одни игровые предметы необходимы для завершения игры, в то время как другие разнообразят геймплей. Некоторые элементы имеют определенные свойства, уникальные для данного элемента. Например, у мечей wooden_sword и steel_sword есть свойство, показывающее наносимый ими урон и поддерживаемые магические бонусы.

Взаимодействие с персонажами помогает продвигать игровой сюжет. Персонажи определены в adventurelib_game_characters.py. У каждого героя, как и у каждого предмета, есть связанные с ним универсальные свойства, такие как описание и приветствие, используемое, когда игрок встречает персонажа в первый раз.

Контекст. Для того, чтобы отличать взаимодействие с персонажем или простое нахождение с ним в одной зоне, движок использует понятие контекста. Контекст позволяет для разных ситуаций запускать разные команды. Или вести себя по-разному разным командам и отслеживать дополнительную информацию о действиях, которые мог предпринять игрок.

Примеры контекста. В какой-то момент игрок впервые сталкивается со старейшиной Бэрроном (Elder Barron). Когда игрок набирает talk to elder, контекст устанавливается по свойству elder.context. Приветствие старейшины заканчивается вопросом требующим ответа «да» или «нет». Если игрок вводит yes, то в adventurelib_game.py запускается обработчик команды, заданный декоратором @when("yes", context="elder").

🐍🕹️ Как написать игру на Python: 5 игровых движков

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

Запрет действий в контексте. Вы также можете проверить, как помогает контекст в обработчике команд. Например, игрок не может просто выйти из боя с великаном, закончив разговор. Обработчик команды "goodbye" проверяет, находится ли игрок в контексте "giant", который вводится, когда он начинает сражаться. Если контекст в силе, прекращать разговор нельзя, — это смертельный бой!

Команды, не имеющие совпадений, обрабатываются функцией no_command_matches(). Её можно использовать для диалогов, требующих конкретного ответа. Так, когда волшебник просит игрока разгадать загадку, создается контекст wizard.riddle. Неправильный ответ приводит к прекращению разговора.

Заключение по adventurelib

Создать полноценную текстовую приключенческую игру непросто, но adventurelib берёт на себя основную рутину, связанную с обработкой локаций, предметов, героев, а также их поведением в зависимости от истории взаимодействий.

Ren’Py

Наследником текстовых игр являются современные игры в стиле визуальных романов (новелл). В таких играх наибольшее значение также играют повествование и сюжет, но игровой опыт разнообразнее за счет картинок, визуальных эффектов и звуков. Для создания подобных игр на Python используется Ren’Py. Название движка образовано от японского слова, означающего романтическую любовь.

Строго говоря, Ren’Py не является классической библиотекой Python, которую можно установить посредством pip install. Игры Ren’Py создаются и запускаются с помощью лаунчера, входящего в состав SDK Ren’Py, в котором есть и редактор игр, и собственный язык сценариев. Однако так как Ren’Py основан на Pygame, с ним можно работать и с помощью Python.

Установка Ren’Py

Ren’Py SDK доступен на Windows, Mac и Linux, пакет для соответствующей платформы можно скачать на официальном сайте. После установки перейдите в папку, содержащую SDK, и запустите Ren’Py Launcher.

🐍🕹️ Как написать игру на Python: 5 игровых движков

Базовые концепции Ren’Py

В той же программе можно начать новый проект, это создаст необходимую структуру файлов и папок. Хотя для запуска игр требуется Ren’Py Launcher, для редактирования кода можно использовать любой удобный редактор.

🐍🕹️ Как написать игру на Python: 5 игровых движков

Сценарии игр Ren’Py хранятся в файлах с расширением .rpy , написанных на специальном языке Ren’Py. Файлы хранятся в папке game/ внутри папки проекта.

Для нового проекта Ren’Py создаёт следующие сценарии, которые можно сразу же использовать и редактировать:

  • gui.rpy определяет внешний вид всех элементов пользовательского интерфейса;
  • options.rpy определяет изменяемые параметры для настройки игры;
  • screens.rpy описывает стили диалогов, меню и других элементов вывода информации;
  • script.rpy — место, где вы начинаете писать игру.

Чтобы запустить примеры игры из материалов этого руководства, следуйте инструкции:

  1. Запустите Ren’Py Launcher.
  2. Нажмите Preferences, затем Projects Directory.
  3. Измените Projects Directory на папку renpy из загруженного репозитория с примерами.
  4. Нажмите Return, чтобы вернуться на главную страницу Ren’Py Launcher.

В списке проектов слева вы увидите basic_game и giant_quest_game. Выберите, что хотите запустить и нажмите Launch Project.

Ниже мы рассмотрим script.rpy для basic_game.

Метки (labels) **определяют точки входа в историю, а также используются для запуска новых сцен и альтернативных путей прохождения истории. Все игры Ren’Py начинают работать с метки start:, которая может появляться в любом сценарии.

💡 Вы также можете использовать метки для определения фоновых изображений, настройки переходов между сценами и управления внешним видом персонажей. В примере вторая сцена начинается со строки с меткой check_room:.

Персонажей можно определить просто назвав их в истории или указав в начале сценария. Так мы определили главного персонажа, его маму и брата по имени Кевин. Оператор define инициализирует три переменные класса Characters.

Медиафайлы. Как и Pygame Zero, Ren’Py требует, чтобы все изображения и звуки, используемые в игре, находились в определенных папках — соответственно game/images/ и game/audio/. В сценарии игры к ним можно обращаться по имени файла без расширения.

💡 Пример. Когда ваш персонаж открывает глаза и впервые видит спальню, ключевое слово scene очищает экран, а затем показывает изображение спальни, хранящееся в day.png. Ren’Py поддерживает формы файлов изображений JPG, WEBP и PNG.

Ветвления сюжета. Игра не была бы игрой, если бы в ней нельзя было принимать решения. В Ren’Py возможности для выбора оформляются в виде меню. В ответ на выбор игра переходит к заданной метке, изменяет изображение и воспроизводит заданные звуки. В примере такой выбор возникает, когда главный персонаж понимает, что забыл свой телефон.

Продвинутый пример игры на Ren’Py

Теперь мы немного знакомы с движком. Давайте реализуем продвинутый вариант с тем же сюжетом, что использовали для adventurelib.

Персонажи и локации. В игре есть несколько персонажей: кузнец, волшебник и великан. И несколько локаций: родная деревушка, деревня оружейника, ведущая к волшебнику дорожка и пещера с грабителем-великаном.

В материалах к туториалу вы найдете четыре сценария:

  • script.rpy, где начинается игра;
  • town.rpy с историей близлежащей деревни;
  • path.rpy , который описывает тропу между деревнями;
  • giant.rpy, содержащий логику битвы с великаном.

Встреча с волшебником оставлена в качестве упражнения.

Описание объектов. Как и в предыдущем примере, в начале script.rpy мы определяем объекты с помощью Character(). Далее мы задаем несколько изображений, на которые ссылаемся позже для использования в качестве фона и отображения объектов. Использование специального синтаксиса позволяет назначать изображениям короткие внутренние имена.

Активный инвентарь. Чтобы показать, какое оружие сейчас активно, мы показываем его изображение в углу экрана. Для этого мы используем команду show с модификатором with moveinleft. Важно помнить, что при смене сцены экран очищается, поэтому нужно запускать команду повторно.

Смена оружия. При входе в город в town.rpy, вы встречаете приветствующего вас кузнеца:

🐍🕹️ Как написать игру на Python: 5 игровых движков

Кузнец предлагает улучшить ваше оружие. Если вы решите это сделать, вы обновите значения для current_weapon и характеристики оружия.

Операторы Python. Строки, начинающиеся с символа $, интерпретируются Ren’Py как операторы Python. Это позволяет прописывать в сценарии произвольный код Python. Обновление current_weapon и статистики оружия выполняется с помощью трех операторов Python, которые изменяют значения переменных по умолчанию, определенных в начале script.rpy.

Вы также можете определить большой блок кода Python, используя слово python:, как показано в файле giant.rpy, начиная со строки 41.

Сцена битвы управляется функцией fight_giant() и игровым циклом с переменной battle_over. Выбор игрока сражаться или бежать отображается с помощью метода renpy.display_menu(). Если игрок сражается, то великану наносится случайное количество урона и корректируются его очки здоровья. Если великан остается в живых, он может атаковать в ответ аналогичным образом. Обратите внимание, что у великана есть шанс промахнуться, в то время как игрок всегда попадает в цель. Бой продолжается до тех пор, пока у игрока или великана не закончится здоровье, либо пока игрок не сбежит.

Используемый код очень похож на тот, который мы использовали для описания битвы в adventurelib. Пример демонстирирует, как вы можете интегрировать код Python в Ren’Py без необходимости переводить его в сценарий Ren’Py.

Если вы заинтересовались движком, обратитесь к документации Ren’Py для получения более подробной информации.

Другие популярные игровые движки на Python

Описанная в статье пятерка библиотек — лишь небольшая выборка из множества доступных игровых движков на Python. Среди десятков доступных мы отметим также следующие:

  • Wasabi 2D разработан командой Pygame Zero. Это современная среда, построенная на moderngl , которая автоматизирует рендеринг, предоставляет готовые решения для анимационных эффектов, имеет встроенные эффекты и использует собственную модель игровых событий.
  • Panda 3D — платформа с открытым исходным кодом для создания 3D-игр и трехмерной визуализации. Panda 3D переносится на разные платформы, поддерживает несколько типов ресурсов, интегрируется с многочисленными сторонними библиотеками и обеспечивает встроенное профилирование.
  • Ursina построена на основе Panda 3D и предоставляет специальный движок для разработки игр, который упрощает многие аспекты Panda 3D. На момент написания статьи Ursina хорошо поддерживается и документируется.
  • PursuedPyBear позиционируется как образовательная библиотека с системой управления сценами, анимированными спрайтами и низким входным барьером.

Если вы знаете о других хороших движках на Python, не стесняйтесь рассказать в комментариях!

Источники контента для игр

Наполнение игр. Обычно самой трудной частью разработки игры является создание игровых ресурсов. Крупные компании, занимающиеся видеоиграми, нанимают команды художников, аниматоров и музыкантов для разработки внешнего вида и саунд-дизайна своих игр.

Разработчики-одиночки могут посчитать этот аспект разработки игр пугающим. Но к счастью, существует множество различных источников игровых ресурсов:

  • OpenGameArt.org предлагает широкий спектр артов, музыки, фонов, значков и других ресурсов для двумерных и трехмерных игр. Большинство файлов находятся в свободном доступе.
  • Kenney.nl содержит набор разнообразных бесплатных и платных ресурсов.
  • Itch.io — торговая площадка для создателей цифровых продуктов, ориентированных на независимую разработку игр. Здесь можно найти ресурсы практически для любых целей: и бесплатные, и платные, и даже готовые игры.

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

Заключение

Поздравляем — теперь вы знакомы с основами дизайна игр на Python! Благодаря стараниям GameDev-сообщества писать качественные компьютерные игры на Python сегодня намного проще, чем раньше.

Надеемся, что теперь вы можете выбрать подходящий игровой движок и написать собственную игру.

***

Материалы по теме

  • 🐍 Пишем Тетрис на Python с помощью библиотеки Pygame
  • 9 идей для начинающих программистов по созданию игр-клонов

Blog

Этот туториал поможет вам сделать заготовку для простой игры, в которой вы будете перемещать своего персонажа, чтобы избежать столкновения с препятствиями. Вы будете использовать Pygame — набор модулей Python, предназначенных для написания видеоигр. Pygame добавляет функциональность поверх превосходной библиотеки SDL. В последней версии используется Pygame SDL2.

Как Писать Игры На Python С Pygame

Установка библиотеки

Pygame состоит из нескольких модулей с функциями для рисования графики, воспроизведения звуков, обработки ввода с помощью мыши и других вещей, которые вам понадобятся при разработке своей первой игры на Python 3

Для начала работы с библиотекой необходимо добавить ее в локальную среду разработки. На Python Pygame-установка выполняется через pip:

 python -m pip install -U pygame --user

Чтобы убедиться в том, что библиотека работает, запустите встроенный пример игры:

python -m pygame.examples.aliens

Должна запуститься игра, в которой нужно уничтожать летающие тарелки инопланетян.

Есть и другие способы установки для разных операционных систем, компиляция библиотеки из исходников. Смотрите их в документации Pygame.

Игровой цикл

Игровой цикл — это место, где все игровые события происходят, обновляются и отображаются на экране. Как только первоначальная настройка и инициализация переменных завершены, начинается игровой цикл, в котором программа продолжает повторяться снова и снова, пока не произойдет событие типа QUIT.

Ниже приведен тип цикла Game, который мы будем использовать в нашей программе Pygame.

while True:
      # Code
      pygame.display.update()

Изменения в игре не вступают в силу до тех пор, пока не будет выполнена команда display.update(). Поскольку в играх всё безостановочно меняется, функция обновления находится в игровом цикле и тоже выполняется постоянно.

Выход из игрового цикла

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

while True:
    pygame.display.update()
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()

Мы вызываем pygame.quit() и sys.exit(), чтобы закрыть окно Pygame и завершить скрипт Python соответственно. Простое использование sys.exit() может привести к зависанию IDE.

Игровые события

Событие происходит, когда пользователь выполняет определенное действие. Например, щелкает мышью или нажимает кнопку на клавиатуре. Pygame записывает каждое происходящее событие.

Вы можете узнать, какие события произошли, вызвав функцию pygame.event.get(). Она возвращает список объектов pygame.event.Event. Для краткости будем называть это объектами событий.

Одним из многих атрибутов (или свойств), которыми обладают объекты событий, является тип — type. Атрибут type сообщает, какое событие представляет объект.

В примере выше использован event.type == QUIT. Он помогает определить, должна игра быть закрыта или нет. Пока пользователь не закроет игру (говоря языком Pygame, не вызовет событие ‘QUIT’), она будет работать в бесконечном цикле.

Игровой экран

Для игры нужно создать окно фиксированного размера, внутри которого будут происходить все события. Ширина и высота этого окна в пикселях передаются в функцию pygame set mode в виде кортежа.

DISPLAYSURF = pygame.display.set_mode((300,300))

Пиксель — наименьшая возможная область на экране. Не существует половины пикселя или четверти пикселя. Поэтому ширина и высота должны быть целыми числами.

Кадровая частота

Если не задать ограничение на частоту кадров, компьютер будет выполнять игровой цикл столько раз, сколько может в течение секунды. Чтобы ограничить его, используйте метод ‘tick(fps)’, где fps — целое число. Метод ‘tick(fps)’ принадлежит классу ‘pygame.time.Clock’ и должен использоваться с объектом этого класса.

FPS = pygame.time.Clock()
FPS.tick(60)

Это может варьироваться от игры к игре, в зависимости от того, как она была разработана, но вы должны стремиться к значению от 30 до 60. Имейте в виду, что, если вы создаете довольно сложную и тяжелую игру, компьютер может не справиться с ней на более высоких кадрах.

Цвета

Pygame использует систему цветов RGB (Red, Green, Blue). Значения для каждого цвета находятся в диапазоне от 0 до 255, всего 256 значений. Сочетание этих трех цветов используется для создания всех цветов, которые вы видите на компьютерах или на любом устройстве с экраном.

Чтобы использовать цвета в Pygame, создают объекты Color, используя значения RGB. Значения RGB должны быть в формате кортежа с тремя значениями для каждого основного цвета.

BLACK = pygame.Color(0, 0, 0)
WHITE = pygame.Color(255, 255, 255)
GREY = pygame.Color(128, 128, 128)
RED = pygame.Color(255, 0, 0)

В работе с цветами есть одна особенность. Например, если вы нарисуете квадрат и зададите ему зеленый цвет, то по умолчанию зелеными станут только границы. Для полной заливки объектов используйте метод ‘fill’. Это сделает квадрат полностью зеленым.

Создание игры

Теперь пришло время заняться самым интересным — разработать первую игру.

Вот так выглядит полный код проекта, который получится у вас после прохождения этого туториала.

# Импортируем библиотеки
import pygame, sys
from pygame.locals import *
import random

# Инициализируем Pygame
pygame.init()

# Ограничиваем количество кадров в секунду
FPS = 60
FramePerSec = pygame.time.Clock()

# Задаем цвет
WHITE = (255, 255, 255)

# Задаем размеры игрового экрана
SCREEN_WIDTH = 400
SCREEN_HEIGHT = 600 

# Отрисовываем экран и заливаем его белым цветом
DISPLAYSURF = pygame.display.set_mode((SCREEN_WIDTH,SCREEN_HEIGHT)))
DISPLAYSURF.fill(WHITE)
pygame.display.set_caption("Game")

# Определяем класс игрока
class Player(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__() 
        self.image = pygame.image.load("Player.png")
        self.rect = self.image.get_rect()
        self.rect.center = (160, 520) 

    def update(self):
        pressed_keys = pygame.key.get_pressed()            
        if self.rect.left > 0:
              if pressed_keys[K_LEFT]:
                  self.rect.move_ip(-5, 0)
        if self.rect.right < SCREEN_WIDTH:        
              if pressed_keys[K_RIGHT]:
                  self.rect.move_ip(5, 0) 

    def draw(self, surface):
        surface.blit(self.image, self.rect)     

# Определяем класс врага
class Enemy(pygame.sprite.Sprite):
      def __init__(self):
        super().__init__() 
        self.image = pygame.image.load("Enemy.png")
        self.rect = self.image.get_rect()
        self.rect.center=(random.randint(40,SCREEN_WIDTH-40),0) 

      def move(self):
        self.rect.move_ip(0,10)
        if (self.rect.bottom > 600):
            self.rect.top = 0
            self.rect.center = (random.randint(30, 370), 0) 

      def draw(self, surface):
        surface.blit(self.image, self.rect)          

# Создаем персонажей
P1 = Player()
E1 = Enemy()

# Определяем игровой цикл
while True:     
    for event in pygame.event.get():              
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
    P1.update()
    E1.move()     

    DISPLAYSURF.fill(WHITE)
    P1.draw(DISPLAYSURF)
    E1.draw(DISPLAYSURF)         

    pygame.display.update()
    FramePerSec.tick(FPS)

Теперь давайте разбирать, какие Pygame Python-команды здесь используются.

Инициализация игры

Создание игры на Python с Pygame начинается с импорта библиотеки. Откройте файл в любой IDE или редакторе кода и добавьте две строки:

import pygame
from pygame.locals import *

Первой строкой вы импортировали модуль Pygame Python 3. Второй строкой вы импортировали все переменные из pygame.locals. 

Можно обойтись без второго импорта. Но он заметно сокращает количество кода. Например, событие выхода из игры без импорта переменных из pygame.locals придется записывать так: ‘pygame.locals.QUIT’. С импортом же синтаксис гораздо короче — для добавления события достаточно написать ‘QUIT’. 

Чтобы использовать случайные значения, импортируйте библиотеку random. Без нее игра будет предсказуемой:

 import random

В начале файла должна быть ещё одна важная строка:

pygame.init()

Эта строка обязательна при использовании библиотеки Pygame. Ее нужно добавить перед любой другой функцией библиотеки, иначе возникнут проблемы с инициализацией. Поэтому вы добавляете init() в начале файла, чтобы исключить появление ошибок.

Определение персонажей

В проекте используется объектно-ориентированный подход. Это позволяет создавать классы, которые становятся основой для разных персонажей. Например, у вас будет один класс врагов (препятствий в игре), а по мере прохождения раундов они будут видоизменяться и получать дополнительные способности.

Сначала определим класс игрока:

class Player(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__() 
        self.image = pygame.image.load("Player.png")
        self.rect = self.image.get_rect()
        self.rect.center = (160, 520)

Функция image.load() принимает на вход файл изображения. Чтобы определить границы спрайта игрока, достаточно вызвать функцию get_rect(). Она автоматически создаст прямоугольник того же размера, что и изображение. Это будет полезно в дальнейшей разработке, когда вы, например, будете реализовывать логику столкновения объектов. Последняя строка self.rect.center определяет начальную позицию персонажа на игровом поле.

def update(self):
    pressed_keys = pygame.key.get_pressed()  
    if self.rect.left > 0:
          if pressed_keys[K_LEFT]:
              self.rect.move_ip(-5, 0)
    if self.rect.left > 0:       
          if pressed_keys[K_RIGHT]:
              self.rect.move_ip(5, 0)

Метод update управляет движением игрока. При его вызове выполняется проверка, нажаты ли клавиши.  

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

Метод move_ip() принимает два параметра. Первый параметр — это расстояние в пикселях, на которое необходимо переместить объект по горизонтали. Второй параметр — расстояние, на которое необходимо переместить объект по вертикали.

Операторы if self.rect.left > 0 и if self.rect.left > 0 гарантируют, что игрок не выйдет за пределы игрового поля.

def draw(self, surface):
    surface.blit(self.image, self.rect)     

Функция blit() принимает параметры. Первый — объект для отрисовки, а второй — его спрайт. Поскольку вы используете изображение, то передаете self.image в качестве объекта для отрисовки. 

Класс врага устроен очень похоже. Единственное отличие — использование генератора случайных чисел:

self.rect.center=(random.randint(40,SCREEN_WIDTH-40),0) 

Это нужно для того, чтобы препятствие каждый раз появлялось в новом месте. Иначе игра будет предсказуемой.

Игровой цикл

И самая важная часть — игровой цикл:

while True:     
    for event in pygame.event.get():              
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
    P1.update()
    E1.move()     

    DISPLAYSURF.fill(WHITE)
    P1.draw(DISPLAYSURF)
    E1.draw(DISPLAYSURF)

    pygame.display.update()
    FramePerSec.tick(FPS)

Цикл выполняется бесконечно, пока пользователь не закончит игру. Это самая простая реализация. На практике к завершению игры могут приводить разные события — например, исчерпание лимита жизней персонажа.

Сначала вызываются функции update для классов Enemy и Player. Далее цикл обновляет экран с помощью DISPLAY.fill(WHITE). Наконец, вызывается функция draw для отрисовки объектов на экране.

Наконец, команда pygame.display.update() обновляет экран, выводя все изменения, которые произошли до этого момента. Функция tick() гарантирует, что количество кадров в секунду при этом не превышает установленного ранее значения FPS.

Заключение

Игра готова. Она пока не настолько хороша, чтобы арендовать сервер для хостинга на cloud.timeweb.com и ждать наплыва игроков. Но это ваша первая игра на Питон и теперь вы знаете, как сделать ее лучше. Например, как добавить персонажу движение вверх и вниз или изменить логику появления препятствий.

Возможностей для совершенствования проекта очень много. Например, можно сделать двумерную игру трехмерной, используя Pygame raycast. Да, на Pygame не получится разработать клона Cyberpunk 2077, но свою версию Wolfenstein 3D сделать реально. 

Есть большое количество туториалов и гайдов по использованию Pygame на русском языке. Они помогут изучить не только особенности библиотеки, но и некоторые универсальные концепции геймдева. Если позже вы переключитесь на более продвинутый игровой движок, многие из этих концепций останутся актуальными.

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Make a 2D Side-Scroller Game With PyGame

When I started learning computer programming late in the last millennium, it was driven by my desire to write computer games. I tried to figure out how to write games in every language and on every platform I learned, including Python. That’s how I discovered pygame and learned how to use it to write games and other graphical programs. At the time, I really wanted a primer on pygame.

By the end of this article, you’ll be able to:

  • Draw items on your screen
  • Play sound effects and music
  • Handle user input
  • Implement event loops
  • Describe how game programming differs from standard procedural Python programming

This primer assumes you have a basic understanding of writing Python programs, including user-defined functions, imports, loops, and conditionals. You should also be familiar with how to open files on your platform. A basic understanding of object-oriented Python is helpful as well. pygame works with most versions of Python, but Python 3.6 is recommended and used throughout this article.

You can get all of the code in this article to follow along:

Background and Setup

pygame is a Python wrapper for the SDL library, which stands for Simple DirectMedia Layer. SDL provides cross-platform access to your system’s underlying multimedia hardware components, such as sound, video, mouse, keyboard, and joystick. pygame started life as a replacement for the stalled PySDL project. The cross-platform nature of both SDL and pygame means you can write games and rich multimedia Python programs for every platform that supports them!

To install pygame on your platform, use the appropriate pip command:

You can verify the install by loading one of the examples that comes with the library:

$ python3 -m pygame.examples.aliens

If a game window appears, then pygame is installed properly! If you run into problems, then the Getting Started guide outlines some known issues and caveats for all platforms.

Basic PyGame Program

Before getting down to specifics, let’s take a look at a basic pygame program. This program creates a window, fills the background with white, and draws a blue circle in the middle of it:

 1# Simple pygame program
 2
 3# Import and initialize the pygame library
 4import pygame
 5pygame.init()
 6
 7# Set up the drawing window
 8screen = pygame.display.set_mode([500, 500])
 9
10# Run until the user asks to quit
11running = True
12while running:
13
14    # Did the user click the window close button?
15    for event in pygame.event.get():
16        if event.type == pygame.QUIT:
17            running = False
18
19    # Fill the background with white
20    screen.fill((255, 255, 255))
21
22    # Draw a solid blue circle in the center
23    pygame.draw.circle(screen, (0, 0, 255), (250, 250), 75)
24
25    # Flip the display
26    pygame.display.flip()
27
28# Done! Time to quit.
29pygame.quit()

When you run this program, you’ll see a window that looks like this:

A simple pygame program

Let’s break this code down, section by section:

  • Lines 4 and 5 import and initialize the pygame library. Without these lines, there is no pygame.

  • Line 8 sets up your program’s display window. You provide either a list or a tuple that specifies the width and height of the window to create. This program uses a list to create a square window with 500 pixels on each side.

  • Lines 11 and 12 set up a game loop to control when the program ends. You’ll cover game loops later on in this tutorial.

  • Lines 15 to 17 scan and handle events within the game loop. You’ll get to events a bit later as well. In this case, the only event handled is pygame.QUIT, which occurs when the user clicks the window close button.

  • Line 20 fills the window with a solid color. screen.fill() accepts either a list or tuple specifying the RGB values for the color. Since (255, 255, 255) was provided, the window is filled with white.

  • Line 23 draws a circle in the window, using the following parameters:

    • screen: the window on which to draw
    • (0, 0, 255): a tuple containing RGB color values
    • (250, 250): a tuple specifying the center coordinates of the circle
    • 75: the radius of the circle to draw in pixels
  • Line 26 updates the contents of the display to the screen. Without this call, nothing appears in the window!

  • Line 29 exits pygame. This only happens once the loop finishes.

That’s the pygame version of “Hello, World.” Now let’s dig a little deeper into the concepts behind this code.

PyGame Concepts

As pygame and the SDL library are portable across different platforms and devices, they both need to define and work with abstractions for various hardware realities. Understanding those concepts and abstractions will help you design and develop your own games.

Initialization and Modules

The pygame library is composed of a number of Python constructs, which include several different modules. These modules provide abstract access to specific hardware on your system, as well as uniform methods to work with that hardware. For example, display allows uniform access to your video display, while joystick allows abstract control of your joystick.

After importing the pygame library in the example above, the first thing you did was initialize PyGame using pygame.init(). This function calls the separate init() functions of all the included pygame modules. Since these modules are abstractions for specific hardware, this initialization step is required so that you can work with the same code on Linux, Windows, and Mac.

Displays and Surfaces

In addition to the modules, pygame also includes several Python classes, which encapsulate non-hardware dependent concepts. One of these is the Surface which, at its most basic, defines a rectangular area on which you can draw. Surface objects are used in many contexts in pygame. Later you’ll see how to load an image into a Surface and display it on the screen.

In pygame, everything is viewed on a single user-created display, which can be a window or a full screen. The display is created using .set_mode(), which returns a Surface representing the visible part of the window. It is this Surface that you pass into drawing functions like pygame.draw.circle(), and the contents of that Surface are pushed to the display when you call pygame.display.flip().

Images and Rects

Your basic pygame program drew a shape directly onto the display’s Surface, but you can also work with images on the disk. The image module allows you to load and save images in a variety of popular formats. Images are loaded into Surface objects, which can then be manipulated and displayed in numerous ways.

As mentioned above, Surface objects are represented by rectangles, as are many other objects in pygame, such as images and windows. Rectangles are so heavily used that there is a special Rect class just to handle them. You’ll be using Rect objects and images in your game to draw players and enemies, and to manage collisions between them.

Okay, that’s enough theory. Let’s design and write a game!

Basic Game Design

Before you start writing any code, it’s always a good idea to have some design in place. Since this is a tutorial game, let’s design some basic gameplay for it as well:

  • The goal of the game is to avoid incoming obstacles:
    • The player starts on the left side of the screen.
    • The obstacles enter randomly from the right and move left in a straight line.
  • The player can move left, right, up, or down to avoid the obstacles.
  • The player cannot move off the screen.
  • The game ends either when the player is hit by an obstacle or when the user closes the window.

When he was describing software projects, a former colleague of mine used to say, “You don’t know what you do until you know what you don’t do.” With that in mind, here are some things that won’t be covered in this tutorial:

  • No multiple lives
  • No scorekeeping
  • No player attack capabilities
  • No advancing levels
  • No boss characters

You’re free to try your hand at adding these and other features to your own program.

Let’s get started!

Importing and Initializing PyGame

After you import pygame, you’ll also need to initialize it. This allows pygame to connect its abstractions to your specific hardware:

 1# Import the pygame module
 2import pygame
 3
 4# Import pygame.locals for easier access to key coordinates
 5# Updated to conform to flake8 and black standards
 6from pygame.locals import (
 7    K_UP,
 8    K_DOWN,
 9    K_LEFT,
10    K_RIGHT,
11    K_ESCAPE,
12    KEYDOWN,
13    QUIT,
14)
15
16# Initialize pygame
17pygame.init()

The pygame library defines many things besides modules and classes. It also defines some local constants for things like keystrokes, mouse movements, and display attributes. You reference these constants using the syntax pygame.<CONSTANT>. By importing specific constants from pygame.locals, you can use the syntax <CONSTANT> instead. This will save you some keystrokes and improve overall readability.

Setting Up the Display

Now you need something to draw on! Create a screen to be the overall canvas:

 1# Import the pygame module
 2import pygame
 3
 4# Import pygame.locals for easier access to key coordinates
 5# Updated to conform to flake8 and black standards
 6from pygame.locals import (
 7    K_UP,
 8    K_DOWN,
 9    K_LEFT,
10    K_RIGHT,
11    K_ESCAPE,
12    KEYDOWN,
13    QUIT,
14)
15
16# Initialize pygame
17pygame.init()
18
19# Define constants for the screen width and height
20SCREEN_WIDTH = 800
21SCREEN_HEIGHT = 600
22
23# Create the screen object
24# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
25screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

You create the screen to use by calling pygame.display.set_mode() and passing a tuple or list with the desired width and height. In this case, the window is 800×600, as defined by the constants SCREEN_WIDTH and SCREEN_HEIGHT on lines 20 and 21. This returns a Surface which represents the inside dimensions of the window. This is the portion of the window you can control, while the OS controls the window borders and title bar.

If you run this program now, then you’ll see a window pop up briefly and then immediately disappear as the program exits. Don’t blink or you might miss it! In the next section, you’ll focus on the main game loop to ensure that your program exits only when given the correct input.

Setting Up the Game Loop

Every game from Pong to Fortnite uses a game loop to control gameplay. The game loop does four very important things:

  1. Processes user input
  2. Updates the state of all game objects
  3. Updates the display and audio output
  4. Maintains the speed of the game

Every cycle of the game loop is called a frame, and the quicker you can do things each cycle, the faster your game will run. Frames continue to occur until some condition to exit the game is met. In your design, there are two conditions that can end the game loop:

  1. The player collides with an obstacle. (You’ll cover collision detection later.)
  2. The player closes the window.

The first thing the game loop does is process user input to allow the player to move around the screen. Therefore, you need some way to capture and process a variety of input. You do this using the pygame event system.

Processing Events

Key presses, mouse movements, and even joystick movements are some of the ways in which a user can provide input. All user input results in an event being generated. Events can happen at any time and often (but not always) originate outside the program. All events in pygame are placed in the event queue, which can then be accessed and manipulated. Dealing with events is referred to as handling them, and the code to do so is called an event handler.

Every event in pygame has an event type associated with it. For your game, the event types you’ll focus on are keypresses and window closure. Keypress events have the event type KEYDOWN, and the window closure event has the type QUIT. Different event types may also have other data associated with them. For example, the KEYDOWN event type also has a variable called key to indicate which key was pressed.

You access the list of all active events in the queue by calling pygame.event.get(). You then loop through this list, inspect each event type, and respond accordingly:

27# Variable to keep the main loop running
28running = True
29
30# Main loop
31while running:
32    # Look at every event in the queue
33    for event in pygame.event.get():
34        # Did the user hit a key?
35        if event.type == KEYDOWN:
36            # Was it the Escape key? If so, stop the loop.
37            if event.key == K_ESCAPE:
38                running = False
39
40        # Did the user click the window close button? If so, stop the loop.
41        elif event.type == QUIT:
42            running = False

Let’s take a closer look at this game loop:

  • Line 28 sets up a control variable for the game loop. To exit the loop and the game, you set running = False. The game loop starts on line 29.

  • Line 31 starts the event handler, walking through every event currently in the event queue. If there are no events, then the list is empty, and the handler won’t do anything.

  • Lines 35 to 38 check if the current event.type is a KEYDOWN event. If it is, then the program checks which key was pressed by looking at the event.key attribute. If the key is the Esc key, indicated by K_ESCAPE, then it exits the game loop by setting running = False.

  • Lines 41 and 42 do a similar check for the event type called QUIT. This event only occurs when the user clicks the window close button. The user may also use any other operating system action to close the window.

When you add these lines to the previous code and run it, you’ll see a window with a blank or black screen:

An empty, but persistent, pygame window

The window won’t disappear until you press the Esc key, or otherwise trigger a QUIT event by closing the window.

Drawing on the Screen

In the sample program, you drew on the screen using two commands:

  1. screen.fill() to fill the background
  2. pygame.draw.circle() to draw a circle

Now you’ll learn about a third way to draw to the screen: using a Surface.

Recall that a Surface is a rectangular object on which you can draw, like a blank sheet of paper. The screen object is a Surface, and you can create your own Surface objects separate from the display screen. Let’s see how that works:

44# Fill the screen with white
45screen.fill((255, 255, 255))
46
47# Create a surface and pass in a tuple containing its length and width
48surf = pygame.Surface((50, 50))
49
50# Give the surface a color to separate it from the background
51surf.fill((0, 0, 0))
52rect = surf.get_rect()

After the screen is filled with white on line 45, a new Surface is created on line 48. This Surface is 50 pixels wide, 50 pixels tall, and assigned to surf. At this point, you treat it just like the screen. So on line, 51 you fill it with black. You can also access its underlying Rect using .get_rect(). This is stored as rect for later use.

Using .blit() and .flip()

Just creating a new Surface isn’t enough to see it on the screen. To do that, you need to blit the Surface onto another Surface. The term blit stands for Block Transfer, and .blit() is how you copy the contents of one Surface to another. You can only .blit() from one Surface to another, but since the screen is just another Surface, that’s not a problem. Here’s how you draw surf on the screen:

54# This line says "Draw surf onto the screen at the center"
55screen.blit(surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
56pygame.display.flip()

The .blit() call on line 55 takes two arguments:

  1. The Surface to draw
  2. The location at which to draw it on the source Surface

The coordinates (SCREEN_WIDTH/2, SCREEN_HEIGHT/2) tell your program to place surf in the exact center of the screen, but it doesn’t quite look that way:

Blitting a surface onto the screen

The reason why the image looks off-center is that .blit() puts the top-left corner of surf at the location given. If you want surf to be centered, then you’ll have to do some math to shift it up and to the left. You can do this by subtracting the width and height of surf from the width and height of the screen, dividing each by 2 to locate the center, and then passing those numbers as arguments to screen.blit():

54# Put the center of surf at the center of the display
55surf_center = (
56    (SCREEN_WIDTH-surf.get_width())/2,
57    (SCREEN_HEIGHT-surf.get_height())/2
58)
59
60# Draw surf at the new coordinates
61screen.blit(surf, surf_center)
62pygame.display.flip()

Notice the call to pygame.display.flip() after the call to blit(). This updates the entire screen with everything that’s been drawn since the last flip. Without the call to .flip(), nothing is shown.

Sprites

In your game design, the player starts on the left, and obstacles come in from the right. You can represent all the obstacles with Surface objects to make drawing everything easier, but how do you know where to draw them? How do you know if an obstacle has collided with the player? What happens when the obstacle flies off the screen? What if you want to draw background images that also move? What if you want your images to be animated? You can handle all these situations and more with sprites.

In programming terms, a sprite is a 2D representation of something on the screen. Essentially, it’s a picture. pygame provides a Sprite class, which is designed to hold one or several graphical representations of any game object that you want to display on the screen. To use it, you create a new class that extends Sprite. This allows you to use its built-in methods.

Players

Here’s how you use Sprite objects with the current game to define the player. Insert this code after line 18:

20# Define a Player object by extending pygame.sprite.Sprite
21# The surface drawn on the screen is now an attribute of 'player'
22class Player(pygame.sprite.Sprite):
23    def __init__(self):
24        super(Player, self).__init__()
25        self.surf = pygame.Surface((75, 25))
26        self.surf.fill((255, 255, 255))
27        self.rect = self.surf.get_rect()

You first define Player by extending pygame.sprite.Sprite on line 22. Then .__init__() uses .super() to call the .__init__() method of Sprite. For more info on why this is necessary, you can read Supercharge Your Classes With Python super().

Next, you define and initialize .surf to hold the image to display, which is currently a white box. You also define and initialize .rect, which you’ll use to draw the player later. To use this new class, you need to create a new object and change the drawing code as well. Expand the code block below to see it all together:

 1# Import the pygame module
 2import pygame
 3
 4# Import pygame.locals for easier access to key coordinates
 5# Updated to conform to flake8 and black standards
 6from pygame.locals import (
 7    K_UP,
 8    K_DOWN,
 9    K_LEFT,
10    K_RIGHT,
11    K_ESCAPE,
12    KEYDOWN,
13    QUIT,
14)
15
16# Define constants for the screen width and height
17SCREEN_WIDTH = 800
18SCREEN_HEIGHT = 600
19
20# Define a player object by extending pygame.sprite.Sprite
21# The surface drawn on the screen is now an attribute of 'player'
22class Player(pygame.sprite.Sprite):
23    def __init__(self):
24        super(Player, self).__init__()
25        self.surf = pygame.Surface((75, 25))
26        self.surf.fill((255, 255, 255))
27        self.rect = self.surf.get_rect()
28
29# Initialize pygame
30pygame.init()
31
32# Create the screen object
33# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
34screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
35
36# Instantiate player. Right now, this is just a rectangle.
37player = Player()
38
39# Variable to keep the main loop running
40running = True
41
42# Main loop
43while running:
44    # for loop through the event queue
45    for event in pygame.event.get():
46        # Check for KEYDOWN event
47        if event.type == KEYDOWN:
48            # If the Esc key is pressed, then exit the main loop
49            if event.key == K_ESCAPE:
50                running = False
51        # Check for QUIT event. If QUIT, then set running to false.
52        elif event.type == QUIT:
53            running = False
54
55    # Fill the screen with black
56    screen.fill((0, 0, 0))
57
58    # Draw the player on the screen
59    screen.blit(player.surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
60
61    # Update the display
62    pygame.display.flip()

Run this code. You’ll see a white rectangle at roughly the middle of the screen:

Basic player sprite being drawn

What do you think would happen if you changed line 59 to screen.blit(player.surf, player.rect)? Try it and see:

55# Fill the screen with black
56screen.fill((0, 0, 0))
57
58# Draw the player on the screen
59screen.blit(player.surf, player.rect)
60
61# Update the display
62pygame.display.flip()

When you pass a Rect to .blit(), it uses the coordinates of the top left corner to draw the surface. You’ll use this later to make your player move!

User Input

So far, you’ve learned how to set up pygame and draw objects on the screen. Now, the real fun starts! You’ll make the player controllable using the keyboard.

Earlier, you saw that pygame.event.get() returns a list of the events in the event queue, which you scan for KEYDOWN event types. Well, that’s not the only way to read keypresses. pygame also provides pygame.event.get_pressed(), which returns a dictionary containing all the current KEYDOWN events in the queue.

Put this in your game loop right after the event handling loop. This returns a dictionary containing the keys pressed at the beginning of every frame:

54# Get the set of keys pressed and check for user input
55pressed_keys = pygame.key.get_pressed()

Next, you write a method in Player to accepts that dictionary. This will define the behavior of the sprite based off the keys that are pressed. Here’s what that might look like:

29# Move the sprite based on user keypresses
30def update(self, pressed_keys):
31    if pressed_keys[K_UP]:
32        self.rect.move_ip(0, -5)
33    if pressed_keys[K_DOWN]:
34        self.rect.move_ip(0, 5)
35    if pressed_keys[K_LEFT]:
36        self.rect.move_ip(-5, 0)
37    if pressed_keys[K_RIGHT]:
38        self.rect.move_ip(5, 0)

K_UP, K_DOWN, K_LEFT, and K_RIGHT correspond to the arrow keys on the keyboard. If the dictionary entry for that key is True, then that key is down, and you move the player .rect in the proper direction. Here you use .move_ip(), which stands for move in place, to move the current Rect.

Then you can call .update() every frame to move the player sprite in response to keypresses. Add this call right after the call to .get_pressed():

52# Main loop
53while running:
54    # for loop through the event queue
55    for event in pygame.event.get():
56        # Check for KEYDOWN event
57        if event.type == KEYDOWN:
58            # If the Esc key is pressed, then exit the main loop
59            if event.key == K_ESCAPE:
60                running = False
61        # Check for QUIT event. If QUIT, then set running to false.
62        elif event.type == QUIT:
63            running = False
64
65    # Get all the keys currently pressed
66    pressed_keys = pygame.key.get_pressed()
67
68    # Update the player sprite based on user keypresses
69    player.update(pressed_keys)
70
71    # Fill the screen with black
72    screen.fill((0, 0, 0))

Now you can move your player rectangle around the screen with the arrow keys:

Keypresses moving a sprite in pygame

You may notice two small problems:

  1. The player rectangle can move very fast if a key is held down. You’ll work on that later.
  2. The player rectangle can move off the screen. Let’s solve that one now.

To keep the player on the screen, you need to add some logic to detect if the rect is going to move off screen. To do that, you check whether the rect coordinates have moved beyond the screen’s boundary. If so, then you instruct the program to move it back to the edge:

25# Move the sprite based on user keypresses
26def update(self, pressed_keys):
27    if pressed_keys[K_UP]:
28        self.rect.move_ip(0, -5)
29    if pressed_keys[K_DOWN]:
30        self.rect.move_ip(0, 5)
31    if pressed_keys[K_LEFT]:
32        self.rect.move_ip(-5, 0)
33    if pressed_keys[K_RIGHT]:
34        self.rect.move_ip(5, 0)
35
36    # Keep player on the screen
37    if self.rect.left < 0:
38        self.rect.left = 0
39    if self.rect.right > SCREEN_WIDTH:
40        self.rect.right = SCREEN_WIDTH
41    if self.rect.top <= 0:
42        self.rect.top = 0
43    if self.rect.bottom >= SCREEN_HEIGHT:
44        self.rect.bottom = SCREEN_HEIGHT

Here, instead of using .move(), you just change the corresponding coordinates of .top, .bottom, .left, or .right directly. Test this, and you’ll find the player rectangle can no longer move off the screen.

Now let’s add some enemies!

Enemies

What’s a game without enemies? You’ll use the techniques you’ve already learned to create a basic enemy class, then create a lot of them for your player to avoid. First, import the random library:

 4# Import random for random numbers
 5import random

Then create a new sprite class called Enemy, following the same pattern you used for Player:

55# Define the enemy object by extending pygame.sprite.Sprite
56# The surface you draw on the screen is now an attribute of 'enemy'
57class Enemy(pygame.sprite.Sprite):
58    def __init__(self):
59        super(Enemy, self).__init__()
60        self.surf = pygame.Surface((20, 10))
61        self.surf.fill((255, 255, 255))
62        self.rect = self.surf.get_rect(
63            center=(
64                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
65                random.randint(0, SCREEN_HEIGHT),
66            )
67        )
68        self.speed = random.randint(5, 20)
69
70    # Move the sprite based on speed
71    # Remove the sprite when it passes the left edge of the screen
72    def update(self):
73        self.rect.move_ip(-self.speed, 0)
74        if self.rect.right < 0:
75            self.kill()

There are four notable differences between Enemy and Player:

  1. On lines 62 to 67, you update rect to be a random location along the right edge of the screen. The center of the rectangle is just off the screen. It’s located at some position between 20 and 100 pixels away from the right edge, and somewhere between the top and bottom edges.

  2. On line 68, you define .speed as a random number between 5 and 20. This specifies how fast this enemy moves towards the player.

  3. On lines 73 to 76, you define .update(). It takes no arguments since enemies move automatically. Instead, .update() moves the enemy toward the left side of the screen at the .speed defined when it was created.

  4. On line 74, you check whether the enemy has moved off-screen. To make sure the Enemy is fully off the screen and won’t just disappear while it’s still visible, you check that the right side of the .rect has gone past the left side of the screen. Once the enemy is off-screen, you call .kill() to prevent it from being processed further.

So, what does .kill() do? To figure this out, you have to know about Sprite Groups.

Sprite Groups

Another super useful class that pygame provides is the Sprite Group. This is an object that holds a group of Sprite objects. So why use it? Can’t you just track your Sprite objects in a list instead? Well, you can, but the advantage of using a Group lies in the methods it exposes. These methods help to detect whether any Enemy has collided with the Player, which makes updates much easier.

Let’s see how to create sprite groups. You’ll create two different Group objects:

  1. The first Group will hold every Sprite in the game.
  2. The second Group will hold just the Enemy objects.

Here’s what that looks like in code:

82# Create the 'player'
83player = Player()
84
85# Create groups to hold enemy sprites and all sprites
86# - enemies is used for collision detection and position updates
87# - all_sprites is used for rendering
88enemies = pygame.sprite.Group()
89all_sprites = pygame.sprite.Group()
90all_sprites.add(player)
91
92# Variable to keep the main loop running
93running = True

When you call .kill(), the Sprite is removed from every Group to which it belongs. This removes the references to the Sprite as well, which allows Python’s garbage collector to reclaim the memory as necessary.

Now that you have an all_sprites group, you can change how objects are drawn. Instead of calling .blit() on just Player, you can iterate over everything in all_sprites:

117# Fill the screen with black
118screen.fill((0, 0, 0))
119
120# Draw all sprites
121for entity in all_sprites:
122    screen.blit(entity.surf, entity.rect)
123
124# Flip everything to the display
125pygame.display.flip()

Now, anything put into all_sprites will be drawn with every frame, whether it’s an enemy or the player.

There’s just one problem… You don’t have any enemies! You could create a bunch of enemies at the beginning of the game, but the game would quickly become boring when they all left the screen a few seconds later. Instead, let’s explore how to keep a steady supply of enemies coming as the game progresses.

Custom Events

The design calls for enemies to appear at regular intervals. This means that at set intervals, you need to do two things:

  1. Create a new Enemy.
  2. Add it to all_sprites and enemies.

You already have code that handles random events. The event loop is designed to look for random events occurring every frame and deal with them appropriately. Luckily, pygame doesn’t restrict you to using only the event types it has defined. You can define your own events to handle as you see fit.

Let’s see how to create a custom event that’s generated every few seconds. You can create a custom event by naming it:

78# Create the screen object
79# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
80screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
81
82# Create a custom event for adding a new enemy
83ADDENEMY = pygame.USEREVENT + 1
84pygame.time.set_timer(ADDENEMY, 250)
85
86# Instantiate player. Right now, this is just a rectangle.
87player = Player()

pygame defines events internally as integers, so you need to define a new event with a unique integer. The last event pygame reserves is called USEREVENT, so defining ADDENEMY = pygame.USEREVENT + 1 on line 83 ensures it’s unique.

Next, you need to insert this new event into the event queue at regular intervals throughout the game. That’s where the time module comes in. Line 84 fires the new ADDENEMY event every 250 milliseconds, or four times per second. You call .set_timer() outside the game loop since you only need one timer, but it will fire throughout the entire game.

Add the code to handle your new event:

100# Main loop
101while running:
102    # Look at every event in the queue
103    for event in pygame.event.get():
104        # Did the user hit a key?
105        if event.type == KEYDOWN:
106            # Was it the Escape key? If so, stop the loop.
107            if event.key == K_ESCAPE:
108                running = False
109
110        # Did the user click the window close button? If so, stop the loop.
111        elif event.type == QUIT:
112            running = False
113
114        # Add a new enemy?
115        elif event.type == ADDENEMY:
116            # Create the new enemy and add it to sprite groups
117            new_enemy = Enemy()
118            enemies.add(new_enemy)
119            all_sprites.add(new_enemy)
120
121    # Get the set of keys pressed and check for user input
122    pressed_keys = pygame.key.get_pressed()
123    player.update(pressed_keys)
124
125    # Update enemy position
126    enemies.update()

Whenever the event handler sees the new ADDENEMY event on line 115, it creates an Enemy and adds it to enemies and all_sprites. Since Enemy is in all_sprites, it will get drawn every frame. You also need to call enemies.update() on line 126, which updates everything in enemies, to ensure they move properly:

Enemies flying by in pygame

However, that’s not the only reason there’s a group for just enemies.

Collision Detection

Your game design calls for the game to end whenever an enemy collides with the player. Checking for collisions is a basic technique of game programming, and usually requires some non-trivial math to determine whether two sprites will overlap each other.

This is where a framework like pygame comes in handy! Writing collision detection code is tedious, but pygame has a LOT of collision detection methods available for you to use.

For this tutorial, you’ll use a method called .spritecollideany(), which is read as “sprite collide any.” This method accepts a Sprite and a Group as parameters. It looks at every object in the Group and checks if its .rect intersects with the .rect of the Sprite. If so, then it returns True. Otherwise, it returns False. This is perfect for this game since you need to check if the single player collides with one of a Group of enemies.

Here’s what that looks like in code:

130# Draw all sprites
131for entity in all_sprites:
132    screen.blit(entity.surf, entity.rect)
133
134# Check if any enemies have collided with the player
135if pygame.sprite.spritecollideany(player, enemies):
136    # If so, then remove the player and stop the loop
137    player.kill()
138    running = False

Line 135 tests whether player has collided with any of the objects in enemies. If so, then player.kill() is called to remove it from every group to which it belongs. Since the only objects being rendered are in all_sprites, the player will no longer be rendered. Once the player has been killed, you need to exit the game as well, so you set running = False to break out of the game loop on line 138.

At this point, you’ve got the basic elements of a game in place:

Pygame window

Now, let’s dress it up a bit, make it more playable, and add some advanced capabilities to help it stand out.

Sprite Images

Alright, you have a game, but let’s be honest… It’s kind of ugly. The player and enemies are just white blocks on a black background. That was state-of-the-art when Pong was new, but it just doesn’t cut it anymore. Let’s replace all those boring white rectangles with some cooler images that will make the game feel like an actual game.

Earlier, you learned that images on disk can be loaded into a Surface with some help from the image module. For this tutorial, we made a little jet for the player and some missiles for the enemies. You’re welcome to use this art, draw your own, or download some free game art assets to use. You can click the link below to download the art used in this tutorial:

Altering the Object Constructors

Before you use images to represent the player and enemy sprites, you need to make some changes to their constructors. The code below replaces the code used previously:

 7# Import pygame.locals for easier access to key coordinates
 8# Updated to conform to flake8 and black standards
 9# from pygame.locals import *
10from pygame.locals import (
11    RLEACCEL,
12    K_UP,
13    K_DOWN,
14    K_LEFT,
15    K_RIGHT,
16    K_ESCAPE,
17    KEYDOWN,
18    QUIT,
19)
20
21# Define constants for the screen width and height
22SCREEN_WIDTH = 800
23SCREEN_HEIGHT = 600
24
25
26# Define the Player object by extending pygame.sprite.Sprite
27# Instead of a surface, use an image for a better-looking sprite
28class Player(pygame.sprite.Sprite):
29    def __init__(self):
30        super(Player, self).__init__()
31        self.surf = pygame.image.load("jet.png").convert()
32        self.surf.set_colorkey((255, 255, 255), RLEACCEL)
33        self.rect = self.surf.get_rect()

Let’s unpack line 31 a bit. pygame.image.load() loads an image from the disk. You pass it a path to the file. It returns a Surface, and the .convert() call optimizes the Surface, making future .blit() calls faster.

Line 32 uses .set_colorkey() to indicate the color pygame will render as transparent. In this case, you choose white, because that’s the background color of the jet image. The RLEACCEL constant is an optional parameter that helps pygame render more quickly on non-accelerated displays. This is added to the pygame.locals import statement on line 11.

Nothing else needs to change. The image is still a Surface, except now it has a picture painted on it. You still use it in the same way.

Here’s what similar changes to the Enemy look like:

59# Define the enemy object by extending pygame.sprite.Sprite
60# Instead of a surface, use an image for a better-looking sprite
61class Enemy(pygame.sprite.Sprite):
62    def __init__(self):
63        super(Enemy, self).__init__()
64        self.surf = pygame.image.load("missile.png").convert()
65        self.surf.set_colorkey((255, 255, 255), RLEACCEL)
66        # The starting position is randomly generated, as is the speed
67        self.rect = self.surf.get_rect(
68            center=(
69                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
70                random.randint(0, SCREEN_HEIGHT),
71            )
72        )
73        self.speed = random.randint(5, 20)

Running the program now should show that this is the same game you had before, except now you’ve added some nice graphics skins with images. But why stop at just making the player and enemy sprites look nice? Let’s add a few clouds going past to give the impression of a jet flying through the sky.

Adding Background Images

For background clouds, you use the same principles as you did for Player and Enemy:

  1. Create the Cloud class.
  2. Add an image of a cloud to it.
  3. Create a method .update() that moves the cloud toward the left side of the screen.
  4. Create a custom event and handler to create new cloud objects at a set time interval.
  5. Add the newly created cloud objects to a new Group called clouds.
  6. Update and draw the clouds in your game loop.

Here’s what Cloud looks like:

 83# Define the cloud object by extending pygame.sprite.Sprite
 84# Use an image for a better-looking sprite
 85class Cloud(pygame.sprite.Sprite):
 86    def __init__(self):
 87        super(Cloud, self).__init__()
 88        self.surf = pygame.image.load("cloud.png").convert()
 89        self.surf.set_colorkey((0, 0, 0), RLEACCEL)
 90        # The starting position is randomly generated
 91        self.rect = self.surf.get_rect(
 92            center=(
 93                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
 94                random.randint(0, SCREEN_HEIGHT),
 95            )
 96        )
 97
 98    # Move the cloud based on a constant speed
 99    # Remove the cloud when it passes the left edge of the screen
100    def update(self):
101        self.rect.move_ip(-5, 0)
102        if self.rect.right < 0:
103            self.kill()

That should all look very familiar. It’s pretty much the same as Enemy.

To have clouds appear at certain intervals, you’ll use event creation code similar to what you used to create new enemies. Put it right below the enemy creation event:

116# Create custom events for adding a new enemy and a cloud
117ADDENEMY = pygame.USEREVENT + 1
118pygame.time.set_timer(ADDENEMY, 250)
119ADDCLOUD = pygame.USEREVENT + 2
120pygame.time.set_timer(ADDCLOUD, 1000)

This says to wait 1000 milliseconds, or one second, before creating the next cloud.

Next, create a new Group to hold each newly created cloud:

125# Create groups to hold enemy sprites, cloud sprites, and all sprites
126# - enemies is used for collision detection and position updates
127# - clouds is used for position updates
128# - all_sprites is used for rendering
129enemies = pygame.sprite.Group()
130clouds = pygame.sprite.Group()
131all_sprites = pygame.sprite.Group()
132all_sprites.add(player)

Next, add a handler for the new ADDCLOUD event in the event handler:

137# Main loop
138while running:
139    # Look at every event in the queue
140    for event in pygame.event.get():
141        # Did the user hit a key?
142        if event.type == KEYDOWN:
143            # Was it the Escape key? If so, then stop the loop.
144            if event.key == K_ESCAPE:
145                running = False
146
147        # Did the user click the window close button? If so, stop the loop.
148        elif event.type == QUIT:
149            running = False
150
151        # Add a new enemy?
152        elif event.type == ADDENEMY:
153            # Create the new enemy and add it to sprite groups
154            new_enemy = Enemy()
155            enemies.add(new_enemy)
156            all_sprites.add(new_enemy)
157
158        # Add a new cloud?
159        elif event.type == ADDCLOUD:
160            # Create the new cloud and add it to sprite groups
161            new_cloud = Cloud()
162            clouds.add(new_cloud)
163            all_sprites.add(new_cloud)

Finally, make sure the clouds are updated every frame:

167# Update the position of enemies and clouds
168enemies.update()
169clouds.update()
170
171# Fill the screen with sky blue
172screen.fill((135, 206, 250))

Line 172 updates the original screen.fill() to fill the screen with a pleasant sky blue color. You can change this color to something else. Maybe you want an alien world with a purple sky, a toxic wasteland in neon green, or the surface of Mars in red!

Note that each new Cloud and Enemy are added to all_sprites as well as clouds and enemies. This is done because each group is used for a separate purpose:

  • Rendering is done using all_sprites.
  • Position updates are done using clouds and enemies.
  • Collision detection is done using enemies.

You create multiple groups so that you can change the way sprites move or behave without impacting the movement or behavior of other sprites.

Game Speed

While testing the game you may have noticed that the enemies move a little fast. If not, then that’s okay, as different machines will see different results at this point.

The reason for this is that the game loop processes frames as fast as the processor and environment will allow. Since all the sprites move once per frame, they can move hundreds of times each second. The number of frames handled each second is called the frame rate, and getting this right is the difference between a playable game and a forgettable one.

Normally, you want as high a frame rate as possible, but for this game, you need to slow it down a bit for the game to be playable. Fortunately, the module time contains a Clock which is designed exactly for this purpose.

Using Clock to establish a playable frame rate requires just two lines of code. The first creates a new Clock before the game loop begins:

106# Setup the clock for a decent framerate
107clock = pygame.time.Clock()

The second calls .tick() to inform pygame that the program has reached the end of the frame:

188# Flip everything to the display
189pygame.display.flip()
190
191# Ensure program maintains a rate of 30 frames per second
192clock.tick(30)

The argument passed to .tick() establishes the desired frame rate. To do this, .tick() calculates the number of milliseconds each frame should take, based on the desired frame rate. Then, it compares that number to the number of milliseconds that have passed since the last time .tick() was called. If not enough time has passed, then .tick() delays processing to ensure that it never exceeds the specified frame rate.

Passing in a smaller frame rate will result in more time in each frame for calculations, while a larger frame rate provides smoother (and possibly faster) gameplay:

Setting the frame rate in pygame

Play around with this number to see what feels best for you!

Sound Effects

So far, you’ve focused on gameplay and the visual aspects of your game. Now let’s explore giving your game some auditory flavor as well. pygame provides mixer to handle all sound-related activities. You’ll use this module’s classes and methods to provide background music and sound effects for various actions.

The name mixer refers to the fact that the module mixes various sounds into a cohesive whole. Using the music sub-module, you can stream individual sound files in a variety of formats, such as MP3, Ogg, and Mod. You can also use Sound to hold a single sound effect to be played, in either Ogg or uncompressed WAV formats. All playback happens in the background, so when you play a Sound, the method returns immediately as the sound plays.

As with most things pygame, using mixer starts with an initialization step. Luckily, this is already handled by pygame.init(). You only need to call pygame.mixer.init() if you want to change the defaults:

106# Setup for sounds. Defaults are good.
107pygame.mixer.init()
108
109# Initialize pygame
110pygame.init()
111
112# Set up the clock for a decent framerate
113clock = pygame.time.Clock()

pygame.mixer.init() accepts a number of arguments, but the defaults work fine in most cases. Note that if you want to change the defaults, you need to call pygame.mixer.init() before calling pygame.init(). Otherwise, the defaults will be in effect regardless of your changes.

After the system is initialized, you can get your sounds and background music setup:

135# Load and play background music
136# Sound source: http://ccmixter.org/files/Apoxode/59262
137# License: https://creativecommons.org/licenses/by/3.0/
138pygame.mixer.music.load("Apoxode_-_Electric_1.mp3")
139pygame.mixer.music.play(loops=-1)
140
141# Load all sound files
142# Sound sources: Jon Fincher
143move_up_sound = pygame.mixer.Sound("Rising_putter.ogg")
144move_down_sound = pygame.mixer.Sound("Falling_putter.ogg")
145collision_sound = pygame.mixer.Sound("Collision.ogg")

Lines 138 and 139 load a background sound clip and begin playing it. You can tell the sound clip to loop and never end by setting the named parameter loops=-1.

Lines 143 to 145 load three sounds you’ll use for various sound effects. The first two are rising and falling sounds, which are played when the player moves up or down. The last is the sound used whenever there is a collision. You can add other sounds as well, such as a sound for whenever an Enemy is created, or a final sound for when the game ends.

So, how do you use the sound effects? You want to play each sound when a certain event occurs. For example, when the ship moves up, you want to play move_up_sound. Therefore, you add a call to .play() whenever you handle that event. In the design, that means adding the following calls to .update() for Player:

26# Define the Player object by extending pygame.sprite.Sprite
27# Instead of a surface, use an image for a better-looking sprite
28class Player(pygame.sprite.Sprite):
29    def __init__(self):
30        super(Player, self).__init__()
31        self.surf = pygame.image.load("jet.png").convert()
32        self.surf.set_colorkey((255, 255, 255), RLEACCEL)
33        self.rect = self.surf.get_rect()
34
35    # Move the sprite based on keypresses
36    def update(self, pressed_keys):
37        if pressed_keys[K_UP]:
38            self.rect.move_ip(0, -5)
39            move_up_sound.play()
40        if pressed_keys[K_DOWN]:
41            self.rect.move_ip(0, 5)
42            move_down_sound.play()

For a collision between the player and an enemy, you play the sound for when collisions are detected:

201# Check if any enemies have collided with the player
202if pygame.sprite.spritecollideany(player, enemies):
203    # If so, then remove the player
204    player.kill()
205
206    # Stop any moving sounds and play the collision sound
207    move_up_sound.stop()
208    move_down_sound.stop()
209    collision_sound.play()
210
211    # Stop the loop
212    running = False

Here, you stop any other sound effects first, because in a collision the player is no longer moving. Then you play the collision sound and continue execution from there.

Finally, when the game is over, all sounds should stop. This is true whether the game ends due to a collision or the user exits manually. To do this, add the following lines at the end of the program after the loop:

220# All done! Stop and quit the mixer.
221pygame.mixer.music.stop()
222pygame.mixer.quit()

Technically, these last few lines are not required, as the program ends right after this. However, if you decide later on to add an intro screen or an exit screen to your game, then there may be more code running after the game ends.

That’s it! Test it again, and you should see something like this:

Pygame window

A Note on Sources

You may have noticed the comment on lines 136-137 when the background music was loaded, listing the source of the music and a link to the Creative Commons license. This was done because the creator of that sound required it. The license requirements stated that in order to use the sound, both proper attribution and a link to the license must be provided.

Here are some sources for music, sound, and art that you can search for useful content:

  • OpenGameArt.org: sounds, sound effects, sprites, and other artwork
  • Kenney.nl: sounds, sound effects, sprites, and other artwork
  • Gamer Art 2D: sprites and other artwork
  • CC Mixter: sounds and sound effects
  • Freesound: sounds and sound effects

As you make your games and use downloaded content such as art, music, or code from other sources, please be sure that you are complying with the licensing terms of those sources.

Conclusion

Throughout this tutorial, you’ve learned how game programming with pygame differs from standard procedural programming. You’ve also learned how to:

  • Implement event loops
  • Draw items on the screen
  • Play sound effects and music
  • Handle user input

To do this, you used a subset of the pygame modules, including the display, mixer and music, time, image, event, and key modules. You also used several pygame classes, including Rect, Surface, Sound, and Sprite. But these only scratch the surface of what pygame can do! Check out the official pygame documentation for a full list of available modules and classes.

You can find all of the code, graphics, and sound files for this article by clicking the link below:

Feel free to leave comments below as well. Happy Pythoning!

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Make a 2D Side-Scroller Game With PyGame

Когда я начал изучать компьютерное программирование в конце прошлого тысячелетия, это было связано с моим желанием писать компьютерные игры. Я пытался понять, как писать игры на каждом языке и на каждой изученной платформе, включая Python. Так я открыл для себяpygame и научился использовать его для написания игр и других графических программ. В то время мне очень хотелось получить учебник поpygame.

К концу этой статьи вы сможете:

  • Нарисуйте предметы на вашем экране

  • Воспроизведение звуковых эффектов и музыки

  • Обрабатывать пользовательский ввод

  • Реализация циклов событий

  • Опишите, чем игровое программирование отличается от стандартного процедурного программирования на Python

Вы можете получить весь код в этой статье, чтобы следовать:

Фон и настройка

pygame — это оболочка Python дляSDL library, что означаетSimple DirectMedia Layer. SDL обеспечивает кроссплатформенный доступ к базовым компонентам мультимедийного оборудования вашей системы, таким как звук, видео, мышь, клавиатура и джойстик. pygame начал свою жизнь как замена остановившемусяPySDL project. Кросс-платформенный характер как SDL, так иpygame означает, что вы можете писать игры и мультимедийные программы Python для любой платформы, которая их поддерживает!

Чтобы установитьpygame на вашу платформу, используйте соответствующую командуpip:

Вы можете проверить установку, загрузив один из примеров, поставляемых с библиотекой:

$ python3 -m pygame.examples.aliens

Если появляется окно игры, значитpygame установлен правильно! Если у вас возникнут проблемы, тоGetting Started guide описывает некоторые известные проблемы и предостережения для всех платформ.

Основная программа PyGame

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

 1 # Simple pygame program
 2
 3 # Import and initialize the pygame library
 4 import pygame
 5 pygame.init()
 6
 7 # Set up the drawing window
 8 screen = pygame.display.set_mode([500, 500])
 9
10 # Run until the user asks to quit
11 running = True
12 while running:
13
14     # Did the user click the window close button?
15     for event in pygame.event.get():
16         if event.type == pygame.QUIT:
17             running = False
18
19     # Fill the background with white
20     screen.fill((255, 255, 255))
21
22     # Draw a solid blue circle in the center
23     pygame.draw.circle(screen, (0, 0, 255), (250, 250), 75)
24
25     # Flip the display
26     pygame.display.flip()
27
28 # Done! Time to quit.
29 pygame.quit()

Когда вы запустите эту программу, вы увидите окно, которое выглядит так:

A simple pygame program

Давайте разберем этот код, раздел за разделом:

  • Lines 4 and 5 импортирует и инициализирует библиотекуpygame. Без этих строк нетpygame.

  • Line 8 устанавливает окно отображения вашей программы. Вы предоставляете либо список, либо кортеж, который определяет ширину и высоту создаваемого окна. Эта программа использует список для создания квадратного окна с 500 пикселями на каждой стороне.

  • Lines 11 and 12 настраиваетgame loop для контроля завершения программы. Об этом вы узнаете позже в этом уроке.

  • Lines 15 to 17 просматривает и обрабатываетevents в игровом цикле. Вы придете к событиям чуть позже. В этом случае обрабатывается только событиеpygame.QUIT, которое происходит, когда пользователь нажимает кнопку закрытия окна.

  • Line 20 заполняет окно сплошным цветом. screen.fill() принимает либо список, либо кортеж, определяющий значения RGB для цвета. Поскольку(255, 255, 255) предоставлен, окно заполняется белым цветом.

  • Line 23 рисует круг в окне, используя следующие параметры:

    • screen: окно, в котором нужно рисовать

    • (0, 0, 255): кортеж, содержащий значения цвета RGB

    • (250, 250): кортеж, определяющий координаты центра круга

    • 75: радиус нарисованного круга в пикселях

  • Line 26 обновляет содержимое дисплея на экране. Без этого звонка в окне ничего не появляется!

  • Line 29 выходит изpygame. Это происходит только после завершения цикла.

Этоpygame версия «Hello, World». Теперь давайте углубимся в концепции, лежащие в основе этого кода.

PyGame Concepts

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

Инициализация и Модули

Библиотекаpygame — этоcomposed of a number of Python constructs, которая включает несколько разныхmodules. Эти модули обеспечивают абстрактный доступ к конкретному оборудованию в вашей системе, а также унифицированные методы для работы с этим оборудованием. Например,display обеспечивает единообразный доступ к вашему видео дисплею, аjoystick позволяет абстрактно управлять вашим джойстиком.

После импорта библиотекиpygame в приведенном выше примере первое, что вы сделали, — этоinitialize PyGame с использованиемpygame.init(). Эта функцияcalls the separate init() functions всех включенных модулейpygame. Поскольку эти модули являются абстракциями для конкретного оборудования, этот шаг инициализации необходим для того, чтобы вы могли работать с одним и тем же кодом в Linux, Windows и Mac.

Дисплеи и поверхности

В дополнение к модулямpygame также включает несколько Pythonclasses, которые инкапсулируют концепции, не зависящие от оборудования. Один из них —Surface, который, по сути, определяет прямоугольную область, на которой вы можете рисовать. ОбъектыSurface используются во многих контекстах вpygame. Позже вы увидите, как загрузить изображение вSurface и отобразить его на экране.

Вpygame все просматривается на одном созданном пользователемdisplay, который может быть окном или полноэкранным. Отображение создается с использованием.set_mode(), который возвращаетSurface, представляющий видимую часть окна. Именно этотSurface вы передаете в функции рисования, такие какpygame.draw.circle(), и содержимое этогоSurface выводится на дисплей, когда вы вызываетеpygame.display.flip().

Изображения и Rects

Ваша базовая программаpygame нарисовала фигуру прямо на экранеSurface, но вы также можете работать с изображениями на диске. image module позволяет создавать изображенияload иsave в различных популярных форматах. Изображения загружаются в объектыSurface, которыми затем можно управлять и отображать различными способами.

Как упоминалось выше, объектыSurface представлены прямоугольниками, как и многие другие объекты вpygame, такие как изображения и окна. Прямоугольники настолько широко используются, что существуетspecial Rect class только для их обработки. Вы будете использовать объекты и изображенияRect в своей игре, чтобы рисовать игроков и врагов и управлять столкновениями между ними.

Ладно, достаточно теории. Давайте разработаем и напишем игру!

Базовый дизайн игры

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

  • Цель игры — избежать препятствий:

    • Плеер запускается с левой стороны экрана.

    • Препятствия входят случайным образом справа и движутся влево по прямой линии.

  • Игрок может двигаться влево, вправо, вверх или вниз, чтобы избежать препятствий.

  • Плеер не может отойти от экрана.

  • Игра заканчивается либо тогда, когда игрок сталкивается с препятствием, либо когда пользователь закрывает окно.

Когда он описывал программные проекты,former colleague of mine говорил: «Вы не знаете, что делаете, пока не узнаете, чего не делаете». Имея это в виду, вот некоторые вещи, которые не будут рассмотрены в этом руководстве:

  • Нет нескольких жизней

  • Нет счета

  • Нет возможности атаки игрока

  • Нет продвигающихся уровней

  • Нет боссов

Вы можете сами попробовать добавить эти и другие функции в свою программу.

Давайте начнем!

Импорт и инициализация PyGame

После импортаpygame вам также потребуется его инициализировать. Это позволяетpygame подключать свои абстракции к вашему конкретному оборудованию:

 1 # Import the pygame module
 2 import pygame
 3
 4 # Import pygame.locals for easier access to key coordinates
 5 # Updated to conform to flake8 and black standards
 6 from pygame.locals import (
 7     K_UP,
 8     K_DOWN,
 9     K_LEFT,
10     K_RIGHT,
11     K_ESCAPE,
12     KEYDOWN,
13     QUIT,
14 )
15
16 # Initialize pygame
17 pygame.init()

Библиотекаpygame определяет многие вещи помимо модулей и классов. Он также определяет некоторыеlocal constants для таких вещей, как нажатия клавиш, движения мыши и атрибуты отображения. Вы ссылаетесь на эти константы, используя синтаксисpygame.<CONSTANT>. Импортируя определенные константы изpygame.locals, вы можете вместо этого использовать синтаксис<CONSTANT>. Это сэкономит вам несколько нажатий клавиш и улучшит общую читабельность.

Настройка дисплея

Теперь вам нужно что-то нарисовать! Создайтеscreen как общий холст:

 1 # Import the pygame module
 2 import pygame
 3
 4 # Import pygame.locals for easier access to key coordinates
 5 # Updated to conform to flake8 and black standards
 6 from pygame.locals import (
 7     K_UP,
 8     K_DOWN,
 9     K_LEFT,
10     K_RIGHT,
11     K_ESCAPE,
12     KEYDOWN,
13     QUIT,
14 )
15
16 # Initialize pygame
17 pygame.init()
18
19 # Define constants for the screen width and height
20 SCREEN_WIDTH = 800
21 SCREEN_HEIGHT = 600
22
23 # Create the screen object
24 # The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
25 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

Вы создаете экран для использования, вызываяpygame.display.set_mode() и передавая кортеж или список с желаемой шириной и высотой. В этом случае размер окна составляет 800×600, что определяется константамиSCREEN_WIDTH иSCREEN_HEIGHT в строках 20 и 21. Это возвращаетSurface, который представляет внутренние размеры окна. Это часть окна, которой вы можете управлять, в то время как ОС контролирует границы окна и строку заголовка.

Если вы запустите эту программу сейчас, вы увидите, что окно ненадолго всплывет, а затем сразу исчезнет при выходе из программы. Не моргайте, иначе вы можете пропустить это! В следующем разделе вы сосредоточитесь на основном игровом цикле, чтобы гарантировать, что ваша программа завершает работу только при правильном вводе.

Настройка игрового цикла

В каждой игре от Pong до Fortnite используетсяgame loop для управления игровым процессом. Игровой цикл выполняет четыре очень важных вещи:

  1. Обрабатывает пользовательский ввод

  2. Обновляет состояние всех игровых объектов

  3. Обновляет дисплей и аудио выход

  4. Поддерживает скорость игры

Каждый цикл игрового цикла называетсяframe, и чем быстрее вы сможете выполнять действия в каждом цикле, тем быстрее будет работать ваша игра. Кадры продолжают появляться до тех пор, пока не будет выполнено какое-либо условие для выхода из игры. В вашем дизайне есть два условия, которые могут завершить игровой цикл:

  1. Игрок сталкивается с препятствием. (Вы узнаете об обнаружении столкновений позже.)

  2. Игрок закрывает окно.

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

Обработка событий

Нажатие клавиш, движения мыши и даже движения джойстика являются одними из способов, которыми пользователь может обеспечить ввод. Результатом всего пользовательского ввода является созданиеevent. События могут происходить в любое время и часто (но не всегда) происходят вне программы. Все события вpygame помещаются в очередь событий, к которой затем можно получить доступ и управлять ими. Работа с событиями обозначаетсяhandling них, а код для этого называетсяevent handler.

Каждое событие вpygame имеет связанное с ним событиеtype. Для вашей игры типы событий, на которых вы сосредоточитесь, — это нажатия клавиш и закрытие окна. События нажатия клавиш имеют тип событияKEYDOWN, а событие закрытия окна имеет типQUIT. Различные типы событий могут также иметь другие данные, связанные с ними. Например, тип событияKEYDOWN также имеет переменную с именемkey, чтобы указать, какая клавиша была нажата.

Вы получаете доступ к списку всех активных событий в очереди, вызываяpygame.event.get(). Затем вы просматриваете этот список, проверяете каждый тип события и отвечаете соответственно:

27 # Variable to keep the main loop running
28 running = True
29
30 # Main loop
31 while running:
32     # Look at every event in the queue
33     for event in pygame.event.get():
34         # Did the user hit a key?
35         if event.type == KEYDOWN:
36             # Was it the Escape key? If so, stop the loop.
37             if event.key == K_ESCAPE:
38                 running = False
39
40         # Did the user click the window close button? If so, stop the loop.
41         elif event.type == QUIT:
42             running = False

Давайте внимательнее посмотрим на этот игровой цикл:

  • Line 28 устанавливает управляющую переменную для игрового цикла. Чтобы выйти из цикла и игры, вы устанавливаетеrunning = False. Игровой цикл начинается в строке 29.

  • Line 31 запускает обработчик событий, просматривая все события в очереди событий. Если событий нет, список пуст, и обработчик ничего не сделает.

  • Lines 35 to 38 проверяет, является ли текущийevent.type событиемKEYDOWN. Если это так, то программа проверяет, какая клавиша была нажата, глядя на атрибутevent.key. Если ключ является ключом[.kbd .key-escape]#Esc #, обозначеннымK_ESCAPE, то он выходит из игрового цикла, устанавливаяrunning = False.

  • Lines 41 and 42 выполняет аналогичную проверку для типа события с именемQUIT. Это событие происходит только тогда, когда пользователь нажимает кнопку закрытия окна. Пользователь также может использовать любое другое действие операционной системы, чтобы закрыть окно.

Когда вы добавите эти строки в предыдущий код и запустите его, вы увидите окно с пустым или черным экраном:

An empty

Окно не исчезнет, ​​пока вы не нажмете клавишу[.kbd .key-escape]#Esc # или иным образом не вызовете событиеQUIT, закрыв окно.

Рисование на экране

В примере программы вы рисовали на экране с помощью двух команд:

  1. screen.fill() для заливки фона

  2. pygame.draw.circle(), чтобы нарисовать круг

Теперь вы узнаете о третьем способе рисования на экране: с помощьюSurface.

Напомним, чтоSurface — это прямоугольный объект, на котором вы можете рисовать, как чистый лист бумаги. Объектscreen — этоSurface, и вы можете создавать свои собственные объектыSurface отдельно от экрана дисплея. Давайте посмотрим, как это работает:

44 # Fill the screen with white
45 screen.fill((255, 255, 255))
46
47 # Create a surface and pass in a tuple containing its length and width
48 surf = pygame.Surface((50, 50))
49
50 # Give the surface a color to separate it from the background
51 surf.fill((0, 0, 0))
52 rect = surf.get_rect()

После заполнения экрана белым цветом в строке 45 создается новыйSurface в строке 48. ЭтотSurface имеет ширину 50 пикселей, высоту 50 пикселей и назначенsurf. На этом этапе вы относитесь к нему так же, как кscreen. Итак, на линии 51 вы заполняете его черным. Вы также можете получить доступ к его базовомуRect, используя.get_rect(). Он сохраняется какrect для дальнейшего использования.

Используя.blit() и.flip()

Просто создать новыйSurface недостаточно, чтобы увидеть его на экране. Для этого вам нужноblitSurface на другойSurface. Терминblit означаетBlock Transfer, а.blit() — это то, как вы копируете содержимое одногоSurface в другой. Вы можете только.blit() от одногоSurface к другому, но поскольку экран — это просто еще одинSurface, это не проблема. Вот как вы рисуетеsurf на экране:

54 # This line says "Draw surf onto the screen at the center"
55 screen.blit(surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
56 pygame.display.flip()

Вызов.blit() в строке 55 принимает два аргумента:

  1. Surface для рисования

  2. Место для его рисования на источникеSurface

Координаты(SCREEN_WIDTH/2, SCREEN_HEIGHT/2) говорят вашей программе разместитьsurf точно в центре экрана, но это не совсем так:

Blitting a surface onto the screen

Причина, по которой изображение выглядит не по центру, заключается в том, что.blit() помещаетtop-left corner изsurf в указанное место. Если вы хотите, чтобыsurf был центрирован, вам нужно будет выполнить некоторые вычисления, чтобы сместить его вверх и влево. Вы можете сделать это, вычтя ширину и высотуsurf из ширины и высоты экрана, разделив каждую на 2, чтобы определить местонахождение центра, а затем передав эти числа в качестве аргументов вscreen.blit():

54 # Put the center of surf at the center of the display
55 surf_center = (
56     (SCREEN_WIDTH-surf.get_width())/2,
57     (SCREEN_HEIGHT-surf.get_height())/2
58 )
59
60 # Draw surf at the new coordinates
61 screen.blit(surf, surf_center)
62 pygame.display.flip()

Обратите внимание на вызовpygame.display.flip() после вызоваblit(). Это обновляет весь экран со всем, что было нарисовано с момента последнего переворота. Без вызова.flip() ничего не отображается.

Спрайты

В вашем игровом дизайне игрок начинает слева, а справа появляются препятствия. Вы можете представить все препятствия с помощью объектовSurface, чтобы все было проще рисовать, но как узнать, где их нарисовать? Как узнать, столкнулось ли препятствие с игроком? Что происходит, когда препятствие летит за пределы экрана? Что делать, если вы хотите нарисовать фоновые изображения, которые также перемещаются? Что если вы хотите, чтобы ваши изображения были анимированными? Вы можете справиться со всеми этими и другими ситуациями с помощьюsprites.

С точки зрения программированияsprite — это 2D-представление чего-либо на экране. По сути, это картина. pygame предоставляетSprite class, который предназначен для хранения одного или нескольких графических представлений любого игрового объекта, который вы хотите отобразить на экране. Чтобы использовать его, вы создаете новый класс, расширяющийSprite. Это позволяет использовать его встроенные методы.

игроки

Вот как вы используете объектыSprite в текущей игре для определения игрока. Вставьте этот код после строки 18:

20 # Define a Player object by extending pygame.sprite.Sprite
21 # The surface drawn on the screen is now an attribute of 'player'
22 class Player(pygame.sprite.Sprite):
23     def __init__(self):
24         super(Player, self).__init__()
25         self.surf = pygame.Surface((75, 25))
26         self.surf.fill((255, 255, 255))
27         self.rect = self.surf.get_rect()

Сначала вы определяетеPlayer, расширяяpygame.sprite.Sprite в строке 22. Затем.__init__() использует.super() для вызова метода.__init__() дляSprite. Для получения дополнительной информации о том, почему это необходимо, вы можете прочитатьSupercharge Your Classes With Python super().

Затем вы определяете и инициализируете.surf, чтобы удерживать изображение для отображения, которое в настоящее время представляет собой белое поле. Вы также определяете и инициализируете.rect, который вы будете использовать для рисования игрока позже. Чтобы использовать этот новый класс, вам нужно создать новый объект и изменить код для рисования. Разверните блок кода ниже, чтобы увидеть все это вместе:

Запустите этот код. Вы увидите белый прямоугольник примерно в середине экрана:

Basic player sprite being drawn

Как вы думаете, что произойдет, если вы измените строку 59 наscreen.blit(player.surf, player.rect)? Попробуйте и посмотрите:

55 # Fill the screen with black
56 screen.fill((0, 0, 0))
57
58 # Draw the player on the screen
59 screen.blit(player.surf, player.rect)
60
61 # Update the display
62 pygame.display.flip()

Когда вы передаетеRect в.blit(), он использует координаты верхнего левого угла для рисования поверхности. Вы будете использовать это позже, чтобы заставить вашего игрока двигаться!

Пользовательский ввод

До сих пор вы научились настраиватьpygame и рисовать объекты на экране. Теперь начинается самое интересное! Вы сделаете плеер управляемым с помощью клавиатуры.

Ранее вы видели, чтоpygame.event.get() возвращает список событий в очереди событий, которую вы просматриваете на предмет типов событийKEYDOWN. Ну, это не единственный способ читать нажатия клавиш. pygame также предоставляетpygame.event.get_pressed(), который возвращаетdictionary, содержащий все текущие событияKEYDOWN в очереди.

Поместите это в свой игровой цикл сразу после цикла обработки событий. Это возвращает словарь, содержащий клавиши, нажимаемые в начале каждого кадра:

54 # Get the set of keys pressed and check for user input
55 pressed_keys = pygame.key.get_pressed()

Затем вы пишете метод вPlayer для приема этого словаря. Это будет определять поведение спрайта на основе нажатых клавиш. Вот как это может выглядеть:

29 # Move the sprite based on user keypresses
30 def update(self, pressed_keys):
31     if pressed_keys[K_UP]:
32         self.rect.move_ip(0, -5)
33     if pressed_keys[K_DOWN]:
34         self.rect.move_ip(0, 5)
35     if pressed_keys[K_LEFT]:
36         self.rect.move_ip(-5, 0)
37     if pressed_keys[K_RIGHT]:
38         self.rect.move_ip(5, 0)

K_UP,K_DOWN,K_LEFT иK_RIGHT соответствуют клавишам со стрелками на клавиатуре. Если словарная запись для этого ключа —True, значит, этот ключ нажат, и вы перемещаете игрока.rect в правильном направлении. Здесь вы используете.move_ip(), что означаетmove in place, для перемещения текущегоRect.

Затем вы можете вызывать.update() каждый кадр, чтобы перемещать спрайт игрока в ответ на нажатия клавиш. Добавьте этот вызов сразу после вызова.get_pressed():

52 # Main loop
53 while running:
54     # for loop through the event queue
55     for event in pygame.event.get():
56         # Check for KEYDOWN event
57         if event.type == KEYDOWN:
58             # If the Esc key is pressed, then exit the main loop
59             if event.key == K_ESCAPE:
60                 running = False
61         # Check for QUIT event. If QUIT, then set running to false.
62         elif event.type == QUIT:
63             running = False
64
65     # Get all the keys currently pressed
66     pressed_keys = pygame.key.get_pressed()
67
68     # Update the player sprite based on user keypresses
69     player.update(pressed_keys)
70
71     # Fill the screen with black
72     screen.fill((0, 0, 0))

Теперь вы можете перемещать прямоугольник игрока по экрану с помощью клавиш со стрелками:

Keypresses moving a sprite in pygame

Вы можете заметить две небольшие проблемы:

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

  2. Прямоугольник игрока может сдвинуться с экрана. Давайте решим это сейчас.

Чтобы игрок оставался на экране, вам нужно добавить некоторую логику, чтобы определять, собирается лиrect уйти с экрана. Для этого вы проверяете, не переместились ли координатыrect за пределы экрана. Если это так, то вы даете команду программе переместить ее обратно к краю:

25 # Move the sprite based on user keypresses
26 def update(self, pressed_keys):
27     if pressed_keys[K_UP]:
28         self.rect.move_ip(0, -5)
29     if pressed_keys[K_DOWN]:
30         self.rect.move_ip(0, 5)
31     if pressed_keys[K_LEFT]:
32         self.rect.move_ip(-5, 0)
33     if pressed_keys[K_RIGHT]:
34         self.rect.move_ip(5, 0)
35
36     # Keep player on the screen
37     if self.rect.left < 0:
38         self.rect.left = 0
39     if self.rect.right > SCREEN_WIDTH:
40         self.rect.right = SCREEN_WIDTH
41     if self.rect.top <= 0:
42         self.rect.top = 0
43     if self.rect.bottom >= SCREEN_HEIGHT:
44         self.rect.bottom = SCREEN_HEIGHT

Здесь вместо использования.move() вы просто меняете соответствующие координаты.top,.bottom,.left или.right напрямую. Проверьте это, и вы увидите, что прямоугольник игрока больше не может сдвинуться с экрана.

Теперь давайте добавим несколько врагов!

враги

Что за игра без врагов? Вы будете использовать методы, которые вы уже изучили, чтобы создать базовый класс врагов, а затем создадите множество из них, чтобы ваш игрок избегал их. Сначала импортируйте библиотекуrandom:

 4 # Import random for random numbers
 5 import random

Затем создайте новый класс спрайтов с именемEnemy, следуя тому же шаблону, который вы использовали дляPlayer:

55 # Define the enemy object by extending pygame.sprite.Sprite
56 # The surface you draw on the screen is now an attribute of 'enemy'
57 class Enemy(pygame.sprite.Sprite):
58     def __init__(self):
59         super(Enemy, self).__init__()
60         self.surf = pygame.Surface((20, 10))
61         self.surf.fill((255, 255, 255))
62         self.rect = self.surf.get_rect(
63             center=(
64                 random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
65                 random.randint(0, SCREEN_HEIGHT),
66             )
67         )
68         self.speed = random.randint(5, 20)
69
70     # Move the sprite based on speed
71     # Remove the sprite when it passes the left edge of the screen
72     def update(self):
73         self.rect.move_ip(-self.speed, 0)
74         if self.rect.right < 0:
75             self.kill()

МеждуEnemy иPlayer есть четыре заметных различия:

  1. On lines 62 to 67, вы обновляетеrect, чтобы он был случайным местом вдоль правого края экрана. Центр прямоугольника находится за пределами экрана. Он расположен в некотором месте между 20 и 100 пикселями от правого края и где-то между верхним и нижним краями.

  2. On line 68, вы определяете.speed как случайное число от 5 до 20. Это указывает, как быстро этот враг движется к игроку.

  3. On lines 73 to 76, вы определяете.update(). Это не требует никаких аргументов, поскольку враги двигаются автоматически. Вместо этого.update() перемещает врага в левую часть экрана на.speed, определенное при его создании.

  4. On line 74, вы проверяете, ушел ли противник за пределы экрана. Чтобы убедиться, чтоEnemy полностью за пределами экрана и не исчезнет просто так, пока он все еще виден, вы убедитесь, что правая сторона.rect прошла за левую часть экрана. Когда противник находится за кадром, вы вызываете.kill(), чтобы предотвратить его дальнейшую обработку.

Итак, что делает.kill()? Чтобы понять это, вы должны знать оSprite Groups.

Спрайт группы

Еще один очень полезный класс, который предоставляетpygame, — этоSprite Group. Это объект, содержащий группу объектовSprite. Так зачем его использовать? Разве вы не можете вместо этого просто отслеживать свои объектыSprite в списке? Что ж, вы можете, но преимущество использованияGroup заключается в методах, которые он предоставляет. Эти методы помогают определить, столкнулся ли какой-либоEnemy сPlayer, что значительно упрощает обновление.

Давайте посмотрим, как создавать группы спрайтов. Вы создадите два разных объектаGroup:

  1. ПервыеGroup будут удерживать каждыеSprite в игре.

  2. ВторойGroup будет содержать только объектыEnemy.

Вот как это выглядит в коде:

82 # Create the 'player'
83 player = Player()
84
85 # Create groups to hold enemy sprites and all sprites
86 # - enemies is used for collision detection and position updates
87 # - all_sprites is used for rendering
88 enemies = pygame.sprite.Group()
89 all_sprites = pygame.sprite.Group()
90 all_sprites.add(player)
91
92 # Variable to keep the main loop running
93 running = True

Когда вы вызываете.kill(),Sprite удаляется из каждогоGroup, которому он принадлежит. Это также удаляет ссылки наSprite, что позволяет сборщику мусора Python при необходимости освобождать память.

Теперь, когда у вас есть группаall_sprites, вы можете изменить способ рисования объектов. Вместо вызова.blit() только дляPlayer, вы можете перебирать все вall_sprites:

117 # Fill the screen with black
118 screen.fill((0, 0, 0))
119
120 # Draw all sprites
121 for entity in all_sprites:
122     screen.blit(entity.surf, entity.rect)
123
124 # Flip everything to the display
125 pygame.display.flip()

Теперь все, что помещено вall_sprites, будет отрисовываться с каждым кадром, будь то враг или игрок.

Есть только одна проблема … У тебя нет врагов! Вы можете создать кучу врагов в начале игры, но игра быстро станет скучной, когда все они покинут экран через несколько секунд. Вместо этого давайте рассмотрим, как поддерживать постоянный приток врагов в ходе игры.

Пользовательские события

Дизайн призывает врагов появляться через равные промежутки времени. Это означает, что через заданные интервалы вам нужно сделать две вещи:

  1. Создайте новыйEnemy.

  2. Добавьте его вall_sprites иenemies.

У вас уже есть код, который обрабатывает случайные события. Цикл обработки событий предназначен для поиска случайных событий, происходящих в каждом кадре, и надлежащего обращения с ними. К счастью,pygame не ограничивает вас использованием только тех типов событий, которые он определил. Вы можете определить свои собственные события для обработки по своему усмотрению.

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

78 # Create the screen object
79 # The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
80 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
81
82 # Create a custom event for adding a new enemy
83 ADDENEMY = pygame.USEREVENT + 1
84 pygame.time.set_timer(ADDENEMY, 250)
85
86 # Instantiate player. Right now, this is just a rectangle.
87 player = Player()

pygame определяет события внутри как целые числа, поэтому вам нужно определить новое событие с уникальным целым числом. Последнее событиеpygame резервов называетсяUSEREVENT, поэтому определениеADDENEMY = pygame.USEREVENT + 1 в строке 83 гарантирует его уникальность.

Затем вам нужно регулярно вставлять это новое событие в очередь на протяжении всей игры. Вот тут и пригодится модульtime. Строка 84 запускает новое событиеADDENEMY каждые 250 миллисекунд или четыре раза в секунду. Вы вызываете.set_timer() вне игрового цикла, поскольку вам нужен только один таймер, но он будет срабатывать на протяжении всей игры.

Добавьте код для обработки вашего нового события:

100 # Main loop
101 while running:
102     # Look at every event in the queue
103     for event in pygame.event.get():
104         # Did the user hit a key?
105         if event.type == KEYDOWN:
106             # Was it the Escape key? If so, stop the loop.
107             if event.key == K_ESCAPE:
108                 running = False
109
110         # Did the user click the window close button? If so, stop the loop.
111         elif event.type == QUIT:
112             running = False
113
114         # Add a new enemy?
115         elif event.type == ADDENEMY:
116             # Create the new enemy and add it to sprite groups
117             new_enemy = Enemy()
118             enemies.add(new_enemy)
119             all_sprites.add(new_enemy)
120
121     # Get the set of keys pressed and check for user input
122     pressed_keys = pygame.key.get_pressed()
123     player.update(pressed_keys)
124
125     # Update enemy position
126     enemies.update()

Каждый раз, когда обработчик событий видит новое событиеADDENEMY в строке 115, он создаетEnemy и добавляет его кenemies иall_sprites. ПосколькуEnemy находится вall_sprites, он будет отрисовываться каждый кадр. Вам также необходимо вызватьenemies.update() в строке 126, который обновляет все вenemies, чтобы убедиться, что они перемещаются правильно:

Enemies flying by in pygame

Однако это не единственная причина, по которой существует группа только дляenemies.

Обнаружение столкновений

Ваш игровой дизайн требует, чтобы игра заканчивалась всякий раз, когда враг сталкивается с игроком. Проверка на наличие столкновений является основной техникой программирования игры и обычно требует некоторой нетривиальной математики, чтобы определить, будут ли два спрайта перекрывать друг друга.

Здесь пригодится фреймворк вродеpygame! Написание кода обнаружения столкновений утомительно, ноpygame имеет МНОГОcollision detection methods, доступное для использования.

В этом руководстве вы будете использовать метод под названием.spritecollideany(), который читается как «спрайт сталкивается с любым». Этот метод принимает в качестве параметровSprite иGroup. Он просматривает каждый объект вGroup и проверяет, пересекается ли его.rect с.rectSprite. Если да, то возвращаетсяTrue. В противном случае возвращаетсяFalse. Это идеально подходит для этой игры, поскольку вам нужно проверить, не сталкивается ли отдельныйplayer с одним изGroup изenemies.

Вот как это выглядит в коде:

130 # Draw all sprites
131 for entity in all_sprites:
132     screen.blit(entity.surf, entity.rect)
133
134 # Check if any enemies have collided with the player
135 if pygame.sprite.spritecollideany(player, enemies):
136     # If so, then remove the player and stop the loop
137     player.kill()
138     running = False

Строка 135 проверяет, столкнулся лиplayer с каким-либо из объектов вenemies. Если это так, то вызываетсяplayer.kill(), чтобы удалить его из каждой группы, к которой он принадлежит. Поскольку визуализируются только объекты вall_sprites,player больше не будет отображаться. После того, как игрок был убит, вам также необходимо выйти из игры, поэтому вы устанавливаетеrunning = False для выхода из игрового цикла в строке 138.

Теперь у вас есть основные элементы игры:

Pygame window

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

Спрайт Изображения

Хорошо, у вас есть игра, но давайте будем честными … Это некрасиво. Игрок и враги — это просто белые блоки на черном фоне. КогдаPong был новым, это было по последнему слову техники, но он больше не работает. Давайте заменим все эти скучные белые прямоугольники более прохладными изображениями, которые сделают игру настоящей игрой.

Ранее вы узнали, что изображения на диске могут быть загружены вSurface с некоторой помощью модуляimage. Для этого урока мы сделали небольшую струю для игрока и несколько ракет для врагов. Вы можете использовать этот рисунок, нарисовать свой собственный или загрузить несколькоfree game art assets для использования. Вы можете нажать на ссылку ниже, чтобы загрузить искусство, используемое в этом руководстве:

Изменение конструкторов объектов

Прежде чем использовать изображения для представления игрока и вражеских спрайтов, необходимо внести некоторые изменения в их конструкторы. Код ниже заменяет код, использованный ранее:

 7 # Import pygame.locals for easier access to key coordinates
 8 # Updated to conform to flake8 and black standards
 9 # from pygame.locals import *
10 from pygame.locals import (
11     RLEACCEL,
12     K_UP,
13     K_DOWN,
14     K_LEFT,
15     K_RIGHT,
16     K_ESCAPE,
17     KEYDOWN,
18     QUIT,
19 )
20
21 # Define constants for the screen width and height
22 SCREEN_WIDTH = 800
23 SCREEN_HEIGHT = 600
24
25
26 # Define the Player object by extending pygame.sprite.Sprite
27 # Instead of a surface, use an image for a better-looking sprite
28 class Player(pygame.sprite.Sprite):
29     def __init__(self):
30         super(Player, self).__init__()
31         self.image = pygame.image.load("jet.png").convert()
32         self.image.set_colorkey((255, 255, 255), RLEACCEL)
33         self.rect = self.image.get_rect()

Давайте немного распакуем строку 31. pygame.image.load() загружает образ с диска. Вы передаете ему путь к файлу. Он возвращаетSurface, а вызов.convert() оптимизируетSurface, ускоряя будущие вызовы.blit().

В строке 32 используется.set_colorkey(), чтобы указать, что цветpygame будет отображаться как прозрачный. В этом случае вы выбираете белый, потому что это цвет фона изображения струи. КонстантаRLEACCEL — это необязательный параметр, который помогает быстрее отображатьpygame на дисплеях без ускорения. Это добавляется к оператору импортаpygame.locals в строке 11.

Больше ничего не нужно менять. Изображение по-прежнемуSurface, только теперь на нем нарисовано изображение. Вы все еще используете его таким же образом.

Вот как выглядят похожие изменения вEnemy:

59 # Define the enemy object by extending pygame.sprite.Sprite
60 # Instead of a surface, use an image for a better-looking sprite
61 class Enemy(pygame.sprite.Sprite):
62     def __init__(self):
63         super(Enemy, self).__init__()
64         self.surf = pygame.image.load("missile.png").convert()
65         self.surf.set_colorkey((255, 255, 255), RLEACCEL)
66         # The starting position is randomly generated, as is the speed
67         self.rect = self.surf.get_rect(
68             center=(
69                 random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
70                 random.randint(0, SCREEN_HEIGHT),
71             )
72         )
73         self.speed = random.randint(5, 20)

Запуск программы сейчас должен показать, что это та же игра, что и у вас раньше, только теперь вы добавили красивую графикуskins с изображениями. Но зачем останавливаться на том, чтобы заставить игрока и вражеских спрайтов выглядеть красиво? Давайте добавим несколько облаков, проходящих мимо, чтобы создать впечатление струи, летящей по небу.

Добавление фоновых изображений

Для фоновых облаков вы используете те же принципы, что и дляPlayer иEnemy:

  1. Создайте классCloud.

  2. Добавьте к нему изображение облака.

  3. Создайте метод.update(), который перемещаетcloud в левую часть экрана.

  4. Создайте настраиваемое событие и обработчик для создания новых объектовcloud через заданный интервал времени.

  5. Добавьте вновь созданные объектыcloud в новыйGroup с именемclouds.

  6. Обновите и нарисуйтеclouds в своем игровом цикле.

Вот как выглядитCloud:

 83 # Define the cloud object by extending pygame.sprite.Sprite
 84 # Use an image for a better-looking sprite
 85 class Cloud(pygame.sprite.Sprite):
 86     def __init__(self):
 87         super(Cloud, self).__init__()
 88         self.surf = pygame.image.load("cloud.png").convert()
 89         self.surf.set_colorkey((0, 0, 0), RLEACCEL)
 90         # The starting position is randomly generated
 91         self.rect = self.surf.get_rect(
 92             center=(
 93                 random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
 94                 random.randint(0, SCREEN_HEIGHT),
 95             )
 96
 97     # Move the cloud based on a constant speed
 98     # Remove the cloud when it passes the left edge of the screen
 99     def update(self):
100         self.rect.move_ip(-5, 0)
101         if self.rect.right < 0:
102             self.kill()

Это все должно выглядеть очень знакомым. Это почти то же самое, что иEnemy.

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

116 # Create custom events for adding a new enemy and a cloud
117 ADDENEMY = pygame.USEREVENT + 1
118 pygame.time.set_timer(ADDENEMY, 250)
119 ADDCLOUD = pygame.USEREVENT + 2
120 pygame.time.set_timer(ADDCLOUD, 1000)

Это говорит о том, что нужно подождать 1000 миллисекунд или одну секунду перед созданием следующегоcloud.

Затем создайте новыйGroup для хранения каждого вновь созданногоcloud:

125 # Create groups to hold enemy sprites, cloud sprites, and all sprites
126 # - enemies is used for collision detection and position updates
127 # - clouds is used for position updates
128 # - all_sprites is used for rendering
129 enemies = pygame.sprite.Group()
130 clouds = pygame.sprite.Group()
131 all_sprites = pygame.sprite.Group()
132 all_sprites.add(player)

Затем добавьте обработчик для нового событияADDCLOUD в обработчике событий:

137 # Main loop
138 while running:
139     # Look at every event in the queue
140     for event in pygame.event.get():
141         # Did the user hit a key?
142         if event.type == KEYDOWN:
143             # Was it the Escape key? If so, then stop the loop.
144             if event.key == K_ESCAPE:
145                 running = False
146
147         # Did the user click the window close button? If so, stop the loop.
148         elif event.type == QUIT:
149             running = False
150
151         # Add a new enemy?
152         elif event.type == ADDENEMY:
153             # Create the new enemy and add it to sprite groups
154             new_enemy = Enemy()
155             enemies.add(new_enemy)
156             all_sprites.add(new_enemy)
157
158         # Add a new cloud?
159         elif event.type == ADDCLOUD:
160             # Create the new cloud and add it to sprite groups
161             new_cloud = Cloud()
162             clouds.add(new_cloud)
163             all_sprites.add(new_cloud)

Наконец, убедитесь, чтоclouds обновляются каждый кадр:

167 # Update the position of enemies and clouds
168 enemies.update()
169 clouds.update()
170
171 # Fill the screen with sky blue
172 screen.fill((135, 206, 250))

Строка 172 обновляет исходныйscreen.fill(), чтобы заполнить экран приятным небесно-голубым цветом. Вы можете изменить этот цвет на что-то другое. Может быть, вы хотите инопланетный мир с пурпурным небом, ядовитую пустошь в неоново-зеленом или поверхность Марса в красном!

Обратите внимание, что каждый новыйCloud иEnemy добавляется кall_sprites, а также кclouds иenemies. Это сделано потому, что каждая группа используется для отдельной цели:

  • Rendering выполняется с использованиемall_sprites.

  • Position updates выполняется с использованиемclouds иenemies.

  • Collision detection выполняется с использованиемenemies.

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

Скорость игры

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

Причина этого в том, что игровой цикл обрабатывает кадры настолько быстро, насколько позволяет процессор и среда. Поскольку все спрайты перемещаются один раз за кадр, они могут перемещаться сотни раз в секунду. Количество кадров, обрабатываемых каждую секунду, называетсяframe rate, и правильное определение этого значения — разница между играбельной игрой и той, которую можно забыть.

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

ИспользованиеClock для установки воспроизводимой частоты кадров требует всего двух строк кода. Первый создает новыйClock перед началом игрового цикла:

106 # Setup the clock for a decent framerate
107 clock = pygame.time.Clock()

Второй вызывает.tick(), чтобы сообщитьpygame, что программа достигла конца кадра:

188 # Flip everything to the display
189 pygame.display.flip()
190
191 # Ensure program maintains a rate of 30 frames per second
192 clock.tick(30)

Аргумент, переданный.tick(), устанавливает желаемую частоту кадров. Для этого.tick() вычисляет количество миллисекунд, которое должен занять каждый кадр, на основе желаемой частоты кадров. Затем он сравнивает это число с количеством миллисекунд, прошедших с момента последнего вызова.tick(). Если прошло недостаточно времени,.tick() задерживает обработку, чтобы гарантировать, что она никогда не превысит указанную частоту кадров.

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

Setting the frame rate in pygame

Поиграйте с этим номером, чтобы увидеть, что вам больше нравится!

Звуковые эффекты

Пока что вы сосредоточены на игровом процессе и визуальных аспектах вашей игры. Теперь давайте рассмотрим, как придать вашей игре слуховой вкус. pygame предоставляетmixer для обработки всех действий, связанных со звуком. Вы будете использовать классы и методы этого модуля для обеспечения фоновой музыки и звуковых эффектов для различных действий.

Названиеmixer относится к тому факту, что модуль смешивает различные звуки в единое целое. Используя подмодульmusic, вы можете передавать отдельные звуковые файлы в различных форматах, таких какMP3,Ogg иMod. Вы также можете использоватьSound для удержания одного звукового эффекта для воспроизведения в форматах Ogg илиuncompressed WAV. Все воспроизведение происходит в фоновом режиме, поэтому, когда вы проигрываетеSound, метод немедленно возвращается по мере воспроизведения звука.

Note:pygame documentation указывает, что поддержка MP3 ограничена, а неподдерживаемые форматы могут вызвать сбои системы. Звуки, упомянутые в этой статье, были протестированы, и мы рекомендуем тщательно протестировать любые звуки перед выпуском вашей игры.

Как и в большинстве случаевpygame, использованиеmixer начинается с этапа инициализации. К счастью, этим уже занимаетсяpygame.init(). Вам нужно только вызватьpygame.mixer.init(), если вы хотите изменить значения по умолчанию:

106 # Setup for sounds. Defaults are good.
107 pygame.mixer.init()
108
109 # Initialize pygame
110 pygame.init()
111
112 # Set up the clock for a decent framerate
113 clock = pygame.time.Clock()

pygame.mixer.init() принимаетa number of arguments, но в большинстве случаев значения по умолчанию работают нормально. Обратите внимание: если вы хотите изменить значения по умолчанию, вам нужно вызватьpygame.mixer.init() перед вызовомpygame.init(). В противном случае значения по умолчанию будут действовать независимо от ваших изменений.

После инициализации системы вы можете настроить свои звуки и фоновую музыку:

135 # Load and play background music
136 # Sound source: http://ccmixter.org/files/Apoxode/59262
137 # License: https://creativecommons.org/licenses/by/3.0/
138 pygame.mixer.music.load("Apoxode_-_Electric_1.mp3")
139 pygame.mixer.music.play(loops=-1)
140
141 # Load all sound files
142 # Sound sources: Jon Fincher
143 move_up_sound = pygame.mixer.Sound("Rising_putter.ogg")
144 move_down_sound = pygame.mixer.Sound("Falling_putter.ogg")
145 collision_sound = pygame.mixer.Sound("Collision.ogg")

Lines 138 and 139 загружает фоновый звуковой клип и начинает его воспроизведение. Вы можете указать звуковому клипу зацикливаться и никогда не заканчиваться, задав именованный параметрloops=-1.

Lines 143 to 145 загружает три звука, которые вы будете использовать для различных звуковых эффектов. Первые два — это повышающиеся и понижающиеся звуки, которые воспроизводятся, когда игрок движется вверх или вниз. Последний звук используется всякий раз, когда происходит столкновение. Вы также можете добавить другие звуки, такие как звук, когда создаетсяEnemy, или последний звук, когда игра заканчивается.

Итак, как вы используете звуковые эффекты? Вы хотите воспроизвести каждый звук, когда происходит определенное событие. Например, когда корабль движется вверх, вы хотите сыгратьmove_up_sound. Следовательно, вы добавляете вызов.play() всякий раз, когда обрабатываете это событие. В дизайне это означает добавление следующих вызовов к.update() дляPlayer:

26 # Define the Player object by extending pygame.sprite.Sprite
27 # Instead of a surface, use an image for a better-looking sprite
28 class Player(pygame.sprite.Sprite):
29     def __init__(self):
30         super(Player, self).__init__()
31         self.surf = pygame.image.load("jet.png").convert()
32         self.surf.set_colorkey((255, 255, 255), RLEACCEL)
33         self.rect = self.surf.get_rect()
34
35     # Move the sprite based on keypresses
36     def update(self, pressed_keys):
37         if pressed_keys[K_UP]:
38             self.rect.move_ip(0, -5)
39             move_up_sound.play()
40         if pressed_keys[K_DOWN]:
41             self.rect.move_ip(0, 5)
42             move_down_sound.play()

Для столкновения между игроком и врагом вы воспроизводите звук, когда обнаруживаются столкновения:

201 # Check if any enemies have collided with the player
202 if pygame.sprite.spritecollideany(player, enemies):
203     # If so, then remove the player
204     player.kill()
205
206     # Stop any moving sounds and play the collision sound
207     move_up_sound.stop()
208     move_down_sound.stop()
209     collision_sound.play()
210
211     # Stop the loop
212     running = False

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

Наконец, когда игра окончена, все звуки должны прекратиться. Это верно независимо от того, заканчивается ли игра из-за столкновения или пользователь выходит вручную. Для этого добавьте следующие строки в конце программы после цикла:

220 # All done! Stop and quit the mixer.
221 pygame.mixer.music.stop()
222 pygame.mixer.quit()

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

Это оно! Проверьте это снова, и вы должны увидеть что-то вроде этого:

Pygame window

Примечание к источникам

Возможно, вы заметили комментарий в строках 136-137 при загрузке фоновой музыки, в котором указан источник музыки и ссылка на лицензию Creative Commons. Это было сделано, потому что создатель этого звука требовал этого. В лицензионных требованиях указывалось, что для использования звука необходимо предоставить как надлежащую атрибуцию, так и ссылку на лицензию.

Вот некоторые источники музыки, звука и искусства, которые вы можете найти для полезного контента:

  • OpenGameArt.org: звуки, звуковые эффекты, спрайты и другие изображения

  • Kenney.nl: звуки, звуковые эффекты, спрайты и другие изображения

  • Gamer Art 2D: спрайты и другие изображения

  • CC Mixter: звуки и звуковые эффекты

  • Freesound: звуки и звуковые эффекты

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

Заключение

Из этого руководства вы узнали, чем программирование игр с помощьюpygame отличается от стандартного процедурного программирования. Вы также узнали, как:

  • Реализация циклов событий

  • Рисовать предметы на экране

  • Воспроизведение звуковых эффектов и музыки

  • Обрабатывать пользовательский ввод

Для этого вы использовали подмножество модулейpygame, включаяdisplay,mixer иmusic,time,image,event и модулиkey. Вы также использовали несколько классовpygame, включаяRect,Surface,Sound иSprite. Но это лишь малая часть того, на что способенpygame! Посмотритеofficial pygame documentation для получения полного списка доступных модулей и классов.

Вы можете найти весь код, графику и звуковые файлы для этой статьи, нажав на ссылку ниже:

Не стесняйтесь оставлять комментарии ниже. Счастливого Pythoning!

Понравилась статья? Поделить с друзьями:
  • Как написать свою игру на java
  • Как написать свою игру для телефона
  • Как написать свою игру для андроид
  • Как написать свою детскую книгу
  • Как написать свою дату рождения цифрами