PyGame: учебник по программированию игр на Python

PyGame: учебник по программированию игр на Python

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

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

  • Рисовать предметы на экране
  • Воспроизводить звуковые эффекты и музыку
  • Обрабатывать пользовательский ввод
  • Реализовать циклы событий
  • Описать, чем программирование игр отличается от стандартного процедурного программирования на Python.

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

Предыстория и установка

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

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

$ pip install pygame

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

$ python3 -m pygame.examples.aliens

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

Базовая программа PyGame

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

# Simple pygame program # Import and initialize the pygame library import pygame pygame.init() # Set up the drawing window screen = pygame.display.set_mode([500, 500]) # Run until the user asks to quit running = True while running: # Did the user click the window close button? for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # Fill the background with white screen.fill((255, 255, 255)) # Draw a solid blue circle in the center pygame.draw.circle(screen, (0, 0, 255), (250, 250), 75) # Flip the display pygame.display.flip() # Done! Time to quit. pygame.quit()

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

PyGame: учебник по программированию игр на Python

Разберем этот код по частям:

  • Строки 4 и 5 импортируют и инициализируют библиотеку pygame. Без этих строк не будет pygame.
  • Строка 8 настраивает окно отображения вашей программы. Вы предоставляете либо список, либо кортеж, определяющий ширину и высоту создаваемого окна. Эта программа использует список для создания квадратного окна с 500 пикселями с каждой стороны.
  • Строки 11 и 12 настраивают игровой цикл для управления завершением программы. Позже в этом руководстве вы познакомитесь с игровыми циклами.
  • Строки с 15 по 17 сканируют и обрабатывают события внутри игрового цикла. Вы также доберетесь до событий немного позже. В этом случае обрабатывается только одно событие pygame.QUIT, которое происходит, когда пользователь нажимает кнопку закрытия окна.
  • Строка 20 заполняет окно сплошным цветом. screen.fill() принимает либо список, либо кортеж, определяющий значения RGB для цвета.
  • Строка 23 рисует круг в окне, используя следующие параметры: screen: окно, в котором будет происходить процесс(0, 0, 255): кортеж, содержащий значения цвета RGB.(250, 250): кортеж, определяющий координаты центра круга75: радиус круга для рисования в пикселях
  • Строка 26 обновляет содержимое дисплея на экране. Без этого вызова в окне ничего не появляется!
  • Строка 29 выходит из pygame. Это происходит только после завершения цикла.

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

Концепции PyGame

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

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

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

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

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

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

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

Изображения и прямоугольники

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

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

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

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

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

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

Один мой бывший коллега , описывая программные проекты, говорил: «Вы не знаете, что делаете, пока не узнаете, чего не делаете». Имея это в виду, вот некоторые вещи, которые не будут рассмотрены в этом руководстве:

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

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

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

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

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

# Import the pygame module import pygame # Import pygame.locals for easier access to key coordinates # Updated to conform to flake8 and black standards from pygame.locals import ( K_UP, K_DOWN, K_LEFT, K_RIGHT, K_ESCAPE, KEYDOWN, QUIT, ) # Initialize pygame pygame.init()

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

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

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

# Import the pygame module import pygame # Import pygame.locals for easier access to key coordinates # Updated to conform to flake8 and black standards from pygame.locals import ( K_UP, K_DOWN, K_LEFT, K_RIGHT, K_ESCAPE, KEYDOWN, QUIT, ) # Initialize pygame pygame.init() # Define constants for the screen width and height SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 # Create the screen object # The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

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

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

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

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

  • Обрабатывает пользовательский ввод
  • Обновляет состояние всех игровых объектов
  • Обновляет дисплей и аудиовыход
  • Поддерживает скорость игры

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

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

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

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

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

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

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

# Variable to keep the main loop running running = True # Main loop while running: # Look at every event in the queue for event in pygame.event.get(): # Did the user hit a key? if event.type == KEYDOWN: # Was it the Escape key? If so, stop the loop. if event.key == K_ESCAPE: running = False # Did the user click the window close button? If so, stop the loop. elif event.type == QUIT: running = False

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

  • В строке 28 задается управляющая переменная для игрового цикла. Для выхода из цикла и игры вы устанавливаете running = False. Игровой цикл начинается в строке 29.
  • Строка 31 запускает обработчик событий, просматривая каждое событие, находящееся в данный момент в очереди. Если событий нет, то список пуст, и обработчик ничего не делает.
  • Строки с 35 по 38 проверяют, является ли текущее KEYDOWN событием. Если да, то программа проверяет, какая клавиша была нажата, по атрибуту event.key. Если ключом является Esc, обозначенный K_ESCAPE, то он выходит из игрового цикла, устанавливая running = False.
  • Строки 41 и 42 выполняют аналогичную проверку для типа события с именем QUIT. Это событие происходит только тогда, когда пользователь нажимает кнопку закрытия окна. Пользователь также может использовать любое другое действие операционной системы, чтобы закрыть окно.

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

PyGame: учебник по программированию игр на Python

Окно не исчезнет, пока вы не нажмете клавишу Esc или иным образом не вызовете событие QUIT, закрыв окно.

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

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

  • screen.fill() - заполнить фон
  • pygame.draw.circle() - нарисовать круг

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

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

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

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

Использование .blit() и .flip()

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

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

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

  • Рисование Surface_
  • Место, в котором нужно нарисовать его

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

PyGame: учебник по программированию игр на Python

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

# Put the center of surf at the center of the display surf_center = ( (SCREEN_WIDTH-surf.get_width())/2, (SCREEN_HEIGHT-surf.get_height())/2 ) # Draw surf at the new coordinates screen.blit(surf, surf_center) pygame.display.flip()

Обратите внимание на вызов pygame.display.flip() после вызова blit(). Это обновляет весь экран с момента последнего перелистывания. Без вызова .flip() ничего не будет отображаться.

Спрайты

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

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

Игроки

Вот как вы используете объекты Sprite в текущей игре для определения игрока. Вставьте этот код после строки 18:

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

Сначала вы определяете Player, расширяя строку 22 pygame.sprite.Sprite. Затем используете .__init__() .super()для вызова метода.

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

# Import the pygame module import pygame # Import pygame.locals for easier access to key coordinates # Updated to conform to flake8 and black standards from pygame.locals import ( K_UP, K_DOWN, K_LEFT, K_RIGHT, K_ESCAPE, KEYDOWN, QUIT, ) # Define constants for the screen width and height SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 # Define a player object by extending pygame.sprite.Sprite # The surface drawn on the screen is now an attribute of 'player' class Player(pygame.sprite.Sprite): def __init__(self): super(Player, self).__init__() self.surf = pygame.Surface((75, 25)) self.surf.fill((255, 255, 255)) self.rect = self.surf.get_rect() # Initialize pygame pygame.init() # Create the screen object # The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) # Instantiate player. Right now, this is just a rectangle. player = Player() # Variable to keep the main loop running running = True # Main loop while running: # for loop through the event queue for event in pygame.event.get(): # Check for KEYDOWN event if event.type == KEYDOWN: # If the Esc key is pressed, then exit the main loop if event.key == K_ESCAPE: running = False # Check for QUIT event. If QUIT, then set running to false. elif event.type == QUIT: running = False # Fill the screen with black screen.fill((0, 0, 0)) # Draw the player on the screen screen.blit(player.surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2)) # Update the display pygame.display.flip()

Запустите этот код. Вы увидите белый прямоугольник примерно посередине экрана:

PyGame: учебник по программированию игр на Python

Как вы думаете, что произойдет, если вы замените строку 59 на screen.blit(player.surf, player.rect)? Попробуйте и посмотрите:

# Fill the screen with black screen.fill((0, 0, 0)) # Draw the player on the screen screen.blit(player.surf, player.rect) # Update the display pygame.display.flip()

Когда вы передаете Rectto .blit(), он использует координаты верхнего левого угла для рисования поверхности. Вы будете использовать это позже, чтобы заставить вашего игрока двигаться!

Пользовательский ввод

Итак, вы узнали, как создавать pygame и рисовать объекты на экране. Теперь начинается настоящее веселье! Вы сделаете плеер управляемым с помощью клавиатуры.

Ранее вы видели, что pygame.event.get() возвращает список событий в очереди, которую вы сканируете на наличие типов событий KEYDOWN. Ну, это не единственный способ читать нажатия клавиш. pygame также предоставляет pygame.event.get_pressed(), который возвращает словарь , содержащий все текущие события в очереди KEYDOWN.

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

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

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

# Move the sprite based on user keypresses def update(self, pressed_keys): if pressed_keys[K_UP]: self.rect.move_ip(0, -5) if pressed_keys[K_DOWN]: self.rect.move_ip(0, 5) if pressed_keys[K_LEFT]: self.rect.move_ip(-5, 0) if pressed_keys[K_RIGHT]: self.rect.move_ip(5, 0)

K_UP, K_DOWN, K_LEFT и K_RIGHT соответствуют клавишам со стрелками на клавиатуре. Если словарная статья для этой клавиши — True, то эта клавиша нажата, и вы перемещаете игрока в правильном направлении. Здесь вы используете .move_ip(), что означает перемещение на месте , чтобы переместить текущий Rect.

Затем вы можете вызвать каждый кадр .update() для перемещения спрайта игрока в ответ на нажатия клавиш. Добавьте этот вызов сразу после вызова .get_pressed():

# Main loop while running: # for loop through the event queue for event in pygame.event.get(): # Check for KEYDOWN event if event.type == KEYDOWN: # If the Esc key is pressed, then exit the main loop if event.key == K_ESCAPE: running = False # Check for QUIT event. If QUIT, then set running to false. elif event.type == QUIT: running = False # Get all the keys currently pressed pressed_keys = pygame.key.get_pressed() # Update the player sprite based on user keypresses player.update(pressed_keys) # Fill the screen with black screen.fill((0, 0, 0))

Теперь вы можете перемещать прямоугольник вашего плеера по экрану с помощью клавиш со стрелками:

Далее необходимо решить 2 проблемы:

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

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

# Move the sprite based on user keypresses def update(self, pressed_keys): if pressed_keys[K_UP]: self.rect.move_ip(0, -5) if pressed_keys[K_DOWN]: self.rect.move_ip(0, 5) if pressed_keys[K_LEFT]: self.rect.move_ip(-5, 0) if pressed_keys[K_RIGHT]: self.rect.move_ip(5, 0) # Keep player on the screen if self.rect.left < 0: self.rect.left = 0 if self.rect.right > SCREEN_WIDTH: self.rect.right = SCREEN_WIDTH if self.rect.top <= 0: self.rect.top = 0 if self.rect.bottom >= SCREEN_HEIGHT: self.rect.bottom = SCREEN_HEIGHT

Здесь вместо использования .move() вы просто меняете соответствующие координаты .top, .bottom, .leftили .right напрямую. Проверьте это, и вы обнаружите, что прямоугольник игрока больше не может перемещаться за пределы экрана.

Теперь давайте добавим врагов!

Враги

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

# Import random for random numbers import random

Затем создайте новый класс спрайтов с именем Enemy, следуя тому же шаблону, который вы использовали для Player:

# Define the enemy object by extending pygame.sprite.Sprite # The surface you draw on the screen is now an attribute of 'enemy' class Enemy(pygame.sprite.Sprite): def __init__(self): super(Enemy, self).__init__() self.surf = pygame.Surface((20, 10)) self.surf.fill((255, 255, 255)) self.rect = self.surf.get_rect( center=( random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100), random.randint(0, SCREEN_HEIGHT), ) ) self.speed = random.randint(5, 20) # Move the sprite based on speed # Remove the sprite when it passes the left edge of the screen def update(self): self.rect.move_ip(-self.speed, 0) if self.rect.right < 0: self.kill()

Есть четыре заметных различия между Enemy и Player:

  • В строках с 62 по 67 вы обновляете случайное место вдоль правого края экрана. Центр прямоугольника находится за пределами экрана. Он расположен в некотором положении между 20 и 100 пикселями от правого края и где-то между верхним и нижним краями.
  • В строке 68 вы определяете .speed как случайное число от 5 до 20. Это указывает, как быстро враг движется к игроку.
  • В строках с 73 по 76 вы определяете .update(). Этот метод не требует аргументов, так как враги двигаются автоматически. Вместо этого .update() перемещает врага к левой стороне экрана.
  • В строке 74 вы проверяете, ушел ли враг за пределы экрана. Чтобы убедиться, что значок Enemy полностью исчез с экрана и не исчезнет, пока он все еще виден, убедитесь, что правая часть экрана .rect вышла за левую часть экрана. Как только враг исчезает с экрана, вы вызываете .kill(), чтобы предотвратить его дальнейшую обработку.

Итак, что делает .kill()? Чтобы понять это, вы должны знать о группах спрайтов .

Группы спрайтов

Еще один очень полезный класс, предоставляющий возможности - Sprite Group. Это объект, который содержит группу объектов Sprite. Так зачем его использовать? Разве вы не можете вместо этого просто отслеживать свои объекты в списке? Ну, можете, но преимущество использования заключается в методах Group, которые он раскрывает. Эти методы помогают определить, не столкнулся ли кто-либо с файлом Player, что значительно упрощает обновление.

Давайте посмотрим, как создавать группы спрайтов. Вы создадите два разных объекта Group:

  • Первый Group проведет каждый Sprite в игре.
  • Второй Group будет содержать только объекты Enemy.

Вот как это выглядит в коде:

# Create the 'player' player = Player() # Create groups to hold enemy sprites and all sprites # - enemies is used for collision detection and position updates # - all_sprites is used for rendering enemies = pygame.sprite.Group() all_sprites = pygame.sprite.Group() all_sprites.add(player) # Variable to keep the main loop running running = True

Когда вы вызываете .kill(), Sprite удаляется из всех Group, которым он принадлежит. Это также удаляет ссылки на, что позволяет сборщику мусора Python освобождать память по мере необходимости.

Теперь, когда у вас есть группа all_sprites, вы можете изменить способ рисования объектов. Вместо того, чтобы вызывать .blit(), вы можете перебрать всё в all_sprites:

# Fill the screen with black screen.fill((0, 0, 0)) # Draw all sprites for entity in all_sprites: screen.blit(entity.surf, entity.rect) # Flip everything to the display pygame.display.flip()

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

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

Пользовательские события

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

  • Создавать новый Enemy.
  • Добавить его в all_sprites и enemies.

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

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

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

pygame внутренне определяет события как целые числа, поэтому вам необходимо определить новое событие с уникальным целым числом. Последнее резервное событие pygame называется USEREVENT, поэтому определение ADDENEMY = pygame.USEREVENT + 1 в строке 83 гарантирует его уникальность.

Затем вам нужно вставить это новое событие в очередь через равные промежутки времени на протяжении всей игры. Вот тут-то и появляется модуль time. Строка 84 запускает новое событие ADDENEMY каждые 250 миллисекунд, или четыре раза в секунду. Вы вызываете .set_timer() вне игрового цикла, так как вам нужен только один таймер, но он будет срабатывать на протяжении всей игры.

Добавьте код для обработки вашего нового события:

# Main loop while running: # Look at every event in the queue for event in pygame.event.get(): # Did the user hit a key? if event.type == KEYDOWN: # Was it the Escape key? If so, stop the loop. if event.key == K_ESCAPE: running = False # Did the user click the window close button? If so, stop the loop. elif event.type == QUIT: running = False # Add a new enemy? elif event.type == ADDENEMY: # Create the new enemy and add it to sprite groups new_enemy = Enemy() enemies.add(new_enemy) all_sprites.add(new_enemy) # Get the set of keys pressed and check for user input pressed_keys = pygame.key.get_pressed() player.update(pressed_keys) # Update enemy position enemies.update()

Всякий раз, когда обработчик событий видит новое событие ADDENEMY в строке 115, он создает Enemy и добавляет его в enemies и all_sprites. Поскольку Enemy находится в all_sprites, он будет отображаться в каждом кадре. Вам также нужно вызвать строку 126 enemies.update(), которая обновляет все в enemies, чтобы убедиться, что они перемещаются правильно:

Однако это не единственная причина, по которой существует группа только для enemies.

Обнаружение столкновений

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

Вот где фреймворк вроде pygame пригодится! Написание кода обнаружения столкновений утомительно, но для вас доступно МНОГО методов обнаружения столкновений pygame .

В этом уроке вы будете использовать метод под названием .spritecollideany(), который читается как «столкновение спрайтов». Этот метод принимает Sprite и Group в качестве параметров. Он просматривает каждый объект в Group и проверяет, пересекается ли .rect с объектом .rect в Sprite. Если да, то возвращается True. В противном случае возвращается False. Это идеально подходит для этой игры, так как вам нужно проверить, не сталкивается ли сингл player с одним из Group enemies.

Вот как это выглядит в коде:

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

Строка 135 проверяет, не столкнулся ли player с каким-либо из объектов в enemies. Если это так, то player.kill() вызывается удалить его из каждой группы, к которой он принадлежит. Поскольку рендерятся только объекты в all_sprites, они больше не будут этого делать. Как только игрок был убит, вам также нужно выйти из игры, поэтому вы устанавливаете running = False в строке 138.

На данный момент у вас есть основные элементы игры:

PyGame: учебник по программированию игр на Python

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

Изображения спрайтов

Хорошо, у вас есть игра, но давайте будем честными… Она немного уродлива. Игрок и враги — это просто белые блоки на черном фоне. Это было ультрасовременно, когда Pong был новым, но сейчас он просто не подходит. Давайте заменим все эти скучные белые прямоугольники более крутыми изображениями, которые сделают игру похожей на настоящую.

Ранее вы узнали, что изображения на диске могут быть загружены в файл Surface с помощью модуля image. Для этого урока мы сделали небольшой реактивный самолет для игрока и несколько ракет для врагов. Вы можете использовать этот вариант, рисовать свои собственные или загружать некоторые бесплатные игровые изображения. Вы можете щелкнуть ссылку ниже, чтобы загрузить рисунок, использованный в этом уроке:

Изменение конструкторов объектов

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

# Import pygame.locals for easier access to key coordinates # Updated to conform to flake8 and black standards # from pygame.locals import * from pygame.locals import ( RLEACCEL, K_UP, K_DOWN, K_LEFT, K_RIGHT, K_ESCAPE, KEYDOWN, QUIT, ) # Define constants for the screen width and height SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 # Define the Player object by extending pygame.sprite.Sprite # Instead of a surface, use an image for a better-looking sprite class Player(pygame.sprite.Sprite): def __init__(self): super(Player, self).__init__() self.surf = pygame.image.load("jet.png").convert() self.surf.set_colorkey((255, 255, 255), RLEACCEL) self.rect = self.surf.get_rect()

Давайте немного распакуем строку 31 pygame.image.load(), которая загружает образ с диска. Вы передаете её путь к файлу. Она возвращает Surface, а вызов .convert() оптимизирует Surface, ускоряя будущие вызовы .blit().

Строка 32 .set_colorkey() используется для указания того, что цвет pygame будет отображаться как прозрачный. В этом случае вы выбираете белый, потому что это цвет фона изображения. Константа RLEACCEL — это необязательный параметр, который помогает pygame ускорить отрисовку на неускоренных дисплеях. Это добавляется в pygame.locals оператор импорта в строке 11.

Больше ничего не нужно менять. Изображение по-прежнему Surface, за исключением того, что теперь на нем что-то нарисовано. Вы все еще используете его таким же образом.

Вот как выглядят похожие изменения Enemy:

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

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

Добавление фоновых изображений

Для фоновых облаков используются те же принципы, что и для Player и Enemy:

  • Создайте класс Cloud.
  • Добавьте к нему изображение облака.
  • Создайте метод .update(), который перемещает cloud к левой стороне экрана.
  • Создайте пользовательское событие и обработчик для создания новых объектов cloud с заданным интервалом времени.
  • Добавьте вновь созданные объекты cloud в новый файл Group с именем clouds.
  • Обновите и нарисуйте clouds в своем игровом цикле.

Вот как это выглядит:

# Define the cloud object by extending pygame.sprite.Sprite # Use an image for a better-looking sprite class Cloud(pygame.sprite.Sprite): def __init__(self): super(Cloud, self).__init__() self.surf = pygame.image.load("cloud.png").convert() self.surf.set_colorkey((0, 0, 0), RLEACCEL) # The starting position is randomly generated self.rect = self.surf.get_rect( center=( random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100), random.randint(0, SCREEN_HEIGHT), ) ) # Move the cloud based on a constant speed # Remove the cloud when it passes the left edge of the screen def update(self): self.rect.move_ip(-5, 0) if self.rect.right < 0: self.kill()

Это должно выглядеть очень знакомо. Это почти то же самое, что и Enemy.

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

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

Это говорит о том, что нужно подождать 1000 миллисекунд или одну секунду, прежде чем создавать следующий файл cloud.

Затем создайте новый Group для хранения каждого вновь созданного cloud:

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

Затем добавьте обработчик нового события ADDCLOUD в обработчик событий:

# Main loop while running: # Look at every event in the queue for event in pygame.event.get(): # Did the user hit a key? if event.type == KEYDOWN: # Was it the Escape key? If so, then stop the loop. if event.key == K_ESCAPE: running = False # Did the user click the window close button? If so, stop the loop. elif event.type == QUIT: running = False # Add a new enemy? elif event.type == ADDENEMY: # Create the new enemy and add it to sprite groups new_enemy = Enemy() enemies.add(new_enemy) all_sprites.add(new_enemy) # Add a new cloud? elif event.type == ADDCLOUD: # Create the new cloud and add it to sprite groups new_cloud = Cloud() clouds.add(new_cloud) all_sprites.add(new_cloud)

Наконец, убедитесь, что clouds обновляются каждый кадр:

# Update the position of enemies and clouds enemies.update() clouds.update() # Fill the screen with sky blue screen.fill((135, 206, 250))

Строка 172 обновляет оригинал screen.fill(), чтобы заполнить экран приятным небесно-голубым цветом. Вы можете изменить этот цвет на что-то другое. Может быть, вы хотите инопланетный мир с фиолетовым небом, ядовитую пустошь в неоново-зеленом цвете или поверхность Марса в красном цвете!

Обратите внимание, что каждый новый Cloud и Enemy добавляется all_sprites так же, как clouds и enemies. Это сделано потому, что каждая группа используется для отдельной цели:

  • Рендеринг выполняется с помощью all_sprites.
  • Обновление позиции выполняется с помощью clouds и enemies.
  • Обнаружение столкновений выполняется с помощью enemies.

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

Скорость игры

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

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

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

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

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

Второй вызывает .tick(), чтобы сообщить pygame, что программа достигла конца кадра:

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

Переданный аргумент .tick() устанавливает желаемую частоту кадров. Для этого .tick() вычисляется количество миллисекунд, которое должен занимать каждый кадр, исходя из желаемой частоты кадров. Затем он сравнивает это число с количеством миллисекунд, прошедших с момента последнего вызова .tick(). Если прошло недостаточно времени, .tick()задерживает обработку, чтобы гарантировать, что она никогда не превысит указанную частоту кадров.

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

Поэкспериментируйте с этим числом, чтобы понять, что вам больше подходит!

Звуковые эффекты

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

Название mixer относится к тому факту, что модуль смешивает различные звуки в единое целое. С помощью подмодуля music вы можете транслировать отдельные звуковые файлы в различных форматах, таких как MP3 , Ogg и Mod . Вы также можете использовать Sound для хранения одного звукового эффекта, который будет воспроизводиться в формате Ogg или несжатом формате WAV . Все воспроизведение происходит в фоновом режиме, поэтому при воспроизведении Sound метод возвращает значение сразу после воспроизведения звука.

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

Как и в большинстве случаев pygame, использование mixer начинается с шага инициализации. К счастью, этим уже занимается pygame.init(). Вам нужно только вызвать pygame.mixer.init(), если вы хотите изменить значения по умолчанию:

# Setup for sounds. Defaults are good. pygame.mixer.init() # Initialize pygame pygame.init() # Set up the clock for a decent framerate clock = pygame.time.Clock()

pygame.mixer.init() принимает несколько аргументов , но в большинстве случаев значения по умолчанию работают нормально. Обратите внимание, что если вы хотите изменить значения по умолчанию, вам нужно вызвать pygame.mixer.init() перед вызовом pygame.init(). В противном случае значения по умолчанию будут действовать независимо от ваших изменений.

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

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

Строки 138 и 139 загружают фоновый звуковой клип и начинают его воспроизведение. Вы можете указать звуковому клипу зацикливаться и никогда не заканчиваться, установив именованный параметр loops=-1.

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

Итак, как вы используете звуковые эффекты? Вы хотите воспроизводить каждый звук, когда происходит определенное событие. Например, когда корабль движется вверх, вы хотите сыграть move_up_sound. Поэтому вы добавляете вызов .play() всякий раз, когда обрабатываете это событие. В дизайне это означает добавление следующих вызовов .update() для Player:

# Define the Player object by extending pygame.sprite.Sprite # Instead of a surface, use an image for a better-looking sprite class Player(pygame.sprite.Sprite): def __init__(self): super(Player, self).__init__() self.surf = pygame.image.load("jet.png").convert() self.surf.set_colorkey((255, 255, 255), RLEACCEL) self.rect = self.surf.get_rect() # Move the sprite based on keypresses def update(self, pressed_keys): if pressed_keys[K_UP]: self.rect.move_ip(0, -5) move_up_sound.play() if pressed_keys[K_DOWN]: self.rect.move_ip(0, 5) move_down_sound.play()

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

# Check if any enemies have collided with the player if pygame.sprite.spritecollideany(player, enemies): # If so, then remove the player player.kill() # Stop any moving sounds and play the collision sound move_up_sound.stop() move_down_sound.stop() collision_sound.play() # Stop the loop running = False

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

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

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

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

Вот и все! Проверьте это снова, и вы должны увидеть что-то вроде этого:

PyGame: учебник по программированию игр на Python

Примечание об источниках

Вы могли заметить комментарий к строкам 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 можно сделать! Ознакомьтесь с официальной документацией pygame для получения полного списка доступных модулей и классов.

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

Образец кода: щелкните здесь, чтобы загрузить исходный код примера проекта PyGame, используемого в этом руководстве.

Спасибо за прочтение данной статьи!

22
3 комментария

nice game

Ответить

не могу найти весь код

Ответить

По ссылке: щелкните здесь, чтобы загрузить исходный код примера проекта PyGame
не могу найти

Ответить