Разрабатываем игру на PyGame

Введение

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

Для этого мы научимся:

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

Для начала работы нам понадобится установить питон (с сайта python.org) и библиотеку pygame. Для этого после установки питона нужно открыть терминал (командную строку) и выполнить команду pip install pygame.

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

Создаем игровое окно

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

Игровой цикл - это сердце любой игры, в нем все игровые объекты меняют свои координаты и перерисовываются. Одна итерация такого цикла - это и есть кадр (известный вам по термину FPS - количество кадров секунду).

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

import pygame

pygame.init()

# создаем окно размера 800 на 600
screen = pygame.display.set_mode((800, 600))

# указываем название
pygame.display.set_caption("Asteroids")

# игровой цикл
while True:
    # обрабатываем события
    for e in pygame.event.get():
        # если нажали на крестик
        if e.type == pygame.QUIT:
            # закрыть окно
            raise SystemExit("QUIT")

    # перерисовать окно
    pygame.display.update()

Добавляем фоновое изображение

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

Изображение в примере должно лежать в папке с нашим скриптом и называться sky.jpg.

import pygame
# импортируем функцию для масштабирования картинок
from pygame.transform import scale

pygame.init()

screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Asteroids")

# загружаем картинку из папки
big_sky = pygame.image.load("sky.jpg")
# масштабируем картинку под размер экрана
sky = scale(big_sky, (800, 600))

while True:

    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            raise SystemExit("QUIT")

    # рисуем картинку с небом на экране
    screen.blit(sky, (0, 0))

    pygame.display.update()

Добавляем игрока

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

Класс - это описание некоторой сущности, по сути - чертеж объекта. В нем описывается, какие свойства будут у объекта и какие действия он будет уметь совершать.

Давайте попытаемся описать класс для космического корабля и нарисовать его. В классе нам потребуется описать конструктор (для инициализации стартовых параметров), функции для рисования и перемещения корабля. Кстати, нам снова понадобится прозрачная картинка для корабля, можно поискать по запросу starship 2d asset или взять нашу.

import pygame
from pygame.transform import scale

# описываем класс "космический корабль", расширяющий встроенный класс Sprite
class Spaceship(pygame.sprite.Sprite):
    # конструктор - функция, в которую мы передаем начальные координаты
    def __init__(self, x, y):
        # инициализируем спрайт
        pygame.sprite.Sprite.__init__(self)

        # выбираем прямоугольную область размера 50 на 100
        self.rect = pygame.Rect(x, y, 50, 100)

        # загружаем картинку с кораблем
        self.image = scale(pygame.image.load("ship.png"), (50, 100))

        # задаем начальную скорость по оси x
        self.xvel = 0

    # функция рисования корабля
    def draw(self, screen):
        # рисуем корабль на экране на месте занимаемой им прямоугольной области
        screen.blit(self.image, (self.rect.x, self.rect.y))

    # функция перемещения, параметры - нажата ли стрелочки влево и вправо
    def update(self, left, right):
        # если нажата клавиша влево, уменьшаем скорость
        if left:
            self.xvel -= 3
        # если нажата клавиша вправо, увеличиваем скорость
        if right:
            self.xvel += 3
        # если ничего не нажато - тормозим
        if not (left or right):
            self.xvel = 0
        # изменяем координаты на скорость
        self.rect.x += self.xvel

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Asteroids")

sky = scale(pygame.image.load("sky.jpg"), (800, 600))
# создаем корабль в точке 400 400
ship = Spaceship(400, 400)

while True:
    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            raise SystemExit("QUIT")
    # рисуем небо
    screen.blit(sky, (0, 0))

    # просим корабль нарисоваться
    ship.draw(screen)

    pygame.display.update()

Настраиваем перемещение игрока

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

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

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

import pygame
from pygame.transform import scale

class Spaceship(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)

        self.rect = pygame.Rect(x, y, 50, 100)
        self.image = scale(pygame.image.load("ship.png"), (50, 100))
        self.xvel = 0

    def draw(self, screen):
        screen.blit(self.image, (self.rect.x, self.rect.y))

    def update(self, left, right):
        if left:
            self.xvel -= 3

        if right:
            self.xvel += 3
        if not (left or right):
            self.xvel = 0
        self.rect.x += self.xvel

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Asteroids")

sky = scale(pygame.image.load("sky.jpg"), (800, 600))
# создаем корабль в точке 400 400
ship = Spaceship(400, 400)

# заведем переменные, чтобы помнить, какие клавиши нажаты
left = False
right = False

while True:
    for e in pygame.event.get():
        # если нажата клавиша - меняем переменную
        if e.type == pygame.KEYDOWN and e.key == pygame.K_LEFT:
            left = True
        if e.type == pygame.KEYDOWN and e.key == pygame.K_RIGHT:
            right = True

        # если отпущена клавиша - меняем переменную
        if e.type == pygame.KEYUP and e.key == pygame.K_LEFT:
            left = False
        if e.type == pygame.KEYUP and e.key == pygame.K_RIGHT:
            right = False

        if e.type == pygame.QUIT:
            raise SystemExit("QUIT")
    # рисуем небо
    screen.blit(sky, (0, 0))

    # перемещаем корабль
    ship.update(left, right)
    # просим корабль нарисоваться
    ship.draw(screen)

    pygame.display.update()

Астероиды

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

Кстати, нам снова понадобится картинка, можно взять нашу.

import pygame
import random
from pygame.transform import scale

# создаем Астероид, расширяющий класс Спрайт
class Asteroid(pygame.sprite.Sprite):
    # конструктор, в который передаются стартовые координаты
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)

        # загружаем картинку с астероидом и масштабируем под размер 50 на 50
        self.image = scale(pygame.image.load("asteroid.png"), (50, 50))
        # задаем прямоугольную область 50 на 50
        self.rect = pygame.Rect(x, y, 50, 50)
        # задаем скорость
        self.yvel = 5

    # функция рисования астероида
    def draw(self, screen):
        screen.blit(self.image, (self.rect.x, self.rect.y))

    # функция перемещения астероида
    def update(self):
        self.rect.y += self.yvel

        # если астероид за границей карты - он умирает
        if self.rect.y > 900:
            self.kill()

class Spaceship(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)

        self.rect = pygame.Rect(x, y, 50, 100)
        self.image = scale(pygame.image.load("ship.png"), (50, 100))
        self.xvel = 0

    def draw(self, screen):
        screen.blit(self.image, (self.rect.x, self.rect.y))

    def update(self, left, right):
        if left:
            self.xvel -= 3

        if right:
            self.xvel += 3
        if not (left or right):
            self.xvel = 0
        self.rect.x += self.xvel

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Asteroids")

sky = scale(pygame.image.load("sky.jpg"), (800, 600))

ship = Spaceship(400, 400)

left = False
right = False

# создадим группу спрайтов, в которой будут храниться все астероиды
asteroids = pygame.sprite.Group()

while True:
    # с некоторой вероятность будем добавлять астероиды сверху экрана
    if random.randint(1, 1000) > 900:
        # выбираем координату по оси x
        asteroid_x = random.randint(-100, 700)
        # выбираем точку за верхней граничей экрана
        asteroid_y = -100
        # создаем астероид
        asteroid = Asteroid(asteroid_x, asteroid_y)
        # и добавляем его в группу
        asteroids.add(asteroid)

    for e in pygame.event.get():

        if e.type == pygame.KEYDOWN and e.key == pygame.K_LEFT:
            left = True
        if e.type == pygame.KEYDOWN and e.key == pygame.K_RIGHT:
            right = True

        if e.type == pygame.KEYUP and e.key == pygame.K_LEFT:
            left = False
        if e.type == pygame.KEYUP and e.key == pygame.K_RIGHT:
            right = False

        if e.type == pygame.QUIT:
            raise SystemExit("QUIT")

    # рисуем небо
    screen.blit(sky, (0, 0))

    # перемещаем корабль
    ship.update(left, right)
    # просим корабль нарисоваться
    ship.draw(screen)

    # перемещаем и рисуем каждый астероид в группе
    for asteroid in asteroids:
        asteroid.update()
        asteroid.draw(screen)

    pygame.display.update()

Столкновения

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

Попробуйте добавить закрытие игры или вывод надписи Game Over, когда жизнь опускается до нуля.

import pygame
import random
from pygame.transform import scale

class Asteroid(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)

        self.image = scale(pygame.image.load("asteroid.png"), (50, 50))
        self.rect = pygame.Rect(x, y, 50, 50)
        self.yvel = 5

    def draw(self, screen):
        screen.blit(self.image, (self.rect.x, self.rect.y))

    def update(self):
        self.rect.y += self.yvel

        if self.rect.y > 900:
            self.kill()

class Spaceship(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)

        self.rect = pygame.Rect(x, y, 50, 100)
        self.image = scale(pygame.image.load("ship.png"), (50, 100))
        self.xvel = 0
        # добавим кораблю здоровье
        self.life = 100

    def draw(self, screen):
        screen.blit(self.image, (self.rect.x, self.rect.y))

    # добавим группу с астероидами в обновление координат корабля
    def update(self, left, right, asteroids):
        if left:
            self.xvel -= 3

        if right:
            self.xvel += 3

        if not (left or right):
            self.xvel = 0

        self.rect.x += self.xvel

        # для каждого астероида
        for asteroid in asteroids:
            # если область, занимаемая астероидом пересекает область корабля
            if self.rect.colliderect(asteroid.rect):
                # уменьшаем жизнь
                self.life -= 1

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Asteroids")

sky = scale(pygame.image.load("sky.jpg"), (800, 600))

ship = Spaceship(400, 400)

left = False
right = False

asteroids = pygame.sprite.Group()

# загрузим системный шрифт
pygame.font.init()
font = pygame.font.SysFont('Comic Sans MS', 30)

while True:

    if random.randint(1, 1000) > 900:
        asteroid_x = random.randint(-100, 700)
        asteroid_y = -100
        asteroid = Asteroid(asteroid_x, asteroid_y)
        asteroids.add(asteroid)

    for e in pygame.event.get():

        if e.type == pygame.KEYDOWN and e.key == pygame.K_LEFT:
            left = True
        if e.type == pygame.KEYDOWN and e.key == pygame.K_RIGHT:
            right = True

        if e.type == pygame.KEYUP and e.key == pygame.K_LEFT:
            left = False
        if e.type == pygame.KEYUP and e.key == pygame.K_RIGHT:
            right = False

        if e.type == pygame.QUIT:
            raise SystemExit("QUIT")

    screen.blit(sky, (0, 0))

    # добавим группу астероидов в параметры
    ship.update(left, right, asteroids)
    ship.draw(screen)

    for asteroid in asteroids:
        asteroid.update()
        asteroid.draw(screen)

    # выведем жизнь на экран белым цветом
    life = font.render(f'HP: {ship.life}', False, (255, 255, 255))
    screen.blit(life, (20, 20))

    pygame.display.update()

Анимация столкновений

При столкновении астероида и корабля можно попробовать добавить анимацию взрывов на корабле.

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

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

import pygame
import random
from pygame.transform import scale

class Explosion(pygame.sprite.Sprite):
    def __init__(self, x, y):
        self.rect = pygame.Rect(x, y, 40, 40)
        self.images = []
        self.index = 0

        for i in range(8):
            image = scale(pygame.image.load(f"explosion/tile00{i}.png"), (40, 40))
            self.images.append(image)

    def draw(self, screen):
        if self.index < 8:
            screen.blit(self.images[self.index], (self.rect.x, self.rect.y))
            self.index += 1

class Spaceship(pygame.sprite.Sprite):
    def __init__(self, x, y):
        self.rect = pygame.Rect(x, y, 50, 100)
        self.image = scale(pygame.image.load("ship.png"), (50, 100))
        self.xvel = 0
        self.life = 100
        self.explosions = []

    def draw(self, screen):
        screen.blit(self.image, (self.rect.x, self.rect.y))

        for explosion in self.explosions:
            explosion.draw(screen)

    def update(self, left, right, asteroids):
        if left:
            self.xvel += -3

        if right:
            self.xvel += 3

        if not (left or right):
            self.xvel = 0

        for asteroid in asteroids:
            if self.rect.colliderect(asteroid.rect):
                self.life -= 1
                rx = random.randint(-5, 40)
                ry = random.randint(-5, 40)
                explosion = Explosion(self.rect.x + rx, self.rect.y + ry)
                self.explosions.append(explosion)

        self.rect.x += self.xvel

class Asteroid(pygame.sprite.Sprite):
    def __init__(self, x, y):
        self.image = scale(pygame.image.load("asteroid.png"), (50, 50))
        self.rect = pygame.Rect(x, y, 50, 50)
        self.yvel = 5

    def draw(self, screen):
        screen.blit(self.image, (self.rect.x, self.rect.y))

    def update(self):
        self.rect.y += self.yvel

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Asteroids")
timer = pygame.time.Clock()

sky = scale(pygame.image.load("sky.jpg"), (800, 600))
ship = Spaceship(400, 400)

left = False
right = False

asteroids = []

pygame.font.init()
font = pygame.font.SysFont('Comic Sans MS', 30)

while True:

    if random.randint(1, 1000) > 900:
        asteroid_x = random.randint(-100, 700)
        asteroid_y = -100
        asteroid = Asteroid(asteroid_x, asteroid_y)
        asteroids.append(asteroid)

    timer.tick(60)

    for e in pygame.event.get():
        if e.type == pygame.KEYDOWN and e.key == pygame.K_LEFT:
            left = True
        if e.type == pygame.KEYDOWN and e.key == pygame.K_RIGHT:
            right = True

        if e.type == pygame.KEYUP and e.key == pygame.K_LEFT:
            left = False
        if e.type == pygame.KEYUP and e.key == pygame.K_RIGHT:
            right = False

        if e.type == pygame.QUIT:
            raise SystemExit("QUIT")

    screen.blit(sky, (0, 0))

    ship.update(left, right, asteroids)
    ship.draw(screen)

    for asteroid in asteroids:
        asteroid.update()
        asteroid.draw(screen)

    textsurface = font.render(f'HP: {ship.life}', False, (255, 255, 255))
    screen.blit(textsurface, (20, 20))

    pygame.display.update()

2D RGP - перемещение персонажа в разные стороны и камера

По ссылке https://github.com/roctbb/rpg-template находится шаблон RPG игры на PyGame. В нем персонаж передвигается по лесу и собирает монеты.

В отличие от примера с астероидами в игру добавлены следующие изменения:

  • объекты в игре в начале отрисовываются из карты;
  • карта больше чем экран, при перемещении игрока перемещается камера;
  • на карте есть проходимые и непроходимые объекты;
  • анимация игрока - разная в разных направлениях.

Структура проекта:

  • images - папка с картинками;
  • game.py - основной файл игры;
  • game_platform.py - описание вспомогательных объектов (вода, деревья, камни, монеты);
  • player.py - описание класса "игрок".