Пишем игру на Python, в которой нужно уворачиваться от предметов
hard

Пишем игру на Python, в которой нужно уворачиваться от предметов

Работаем с текстом, анимациями и коллизиями

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

Если вы только начинаете знакомиться с языком программирования Python, посмотрите наш мастрид — там много интересного.

https://thecode.media/wp-content/uploads/2024/08/code_game_back.jpg

import pygame
from sys import exit

pygame.init()

# объявляем ширину и высоту экрана
width = 800
height = 400

# создаём экран игры
screen = pygame.display.set_mode((width, height))

# устанавливаем количество кадров в секунду
fps = 60
# создаём объект таймера
clock = pygame.time.Clock()

# создаём новую поверхность
# 1 — задаём размеры:
width_ts = 200
height_ts = 200
# 2 — создаём поверхность по размерам
test_surface = pygame.Surface((width_ts, height_ts))
# 3 — добавляем цвет
test_surface.fill('White')

# загружаем в переменную картинку из папки с нашим файлом
back = pygame.image.load('code_game_back.jpg')

# даём название окну игры
pygame.display.set_caption("Detective CODE Game")

# объявляем переменную-флаг для цикла игры
game = True

# запускаем бесконечный цикл
while game:
   # получаем список возможных действий игрока
   for event in pygame.event.get():
       # если пользователь нажал на крестик закрытия окна…
       if event.type == pygame.QUIT:
           # …останавливаем цикл
           pygame.quit()
           # добавляем корректное завершение работы
           exit()

   # размещаем новую поверхность на нашем экране — белый квадрат
   screen.blit(test_surface, (300, 100))

   # размещаем новую поверхность на нашем экране — подготовленный jpeg
   screen.blit(back, (0, 0))

   # обновляем экран игры
   pygame.display.update()
   # добавляем к таймеру количество fps для частоты обновления основного цикла
   clock.tick(fps)

Вот что здесь происходит:

  • Мы создали главное окно игры размером 800 на 400 пикселей.
  • Дали название главному экрану.
  • Установили частоту обновления кадров — это пока ни на что не влияет, но сегодня пригодится.
  • Запустили бесконечный цикл игры, чтобы картинка постоянно обновлялась и главное окно не закрывалось на последней строке кода.

Пока что у нас готово только главное окно игры, стартуем с этой точки:

Пишем игру на Python, в которой нужно уворачиваться от предметов

Что сделаем сейчас

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

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

Для тренировки с анимациями и коллизиями реализуем сцену из игры Fahrenheit, но в 2D: с правого края экрана в главного героя летят предметы, а он от них уворачивается.

Как работают изображения и что ещё нужно знать

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

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

Нужны подготовленные картинки двух форматов:

  • .png — для движущихся объектов;
  • .jpg — для статичных.

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

teapot.png

candlestick.png

detective.png

wooden_box.png

code_game_back_floor.jpg

Должно получиться так:

Пишем игру на Python, в которой нужно уворачиваться от предметов

Сначала загружаем все картинки в переменные:

back = pygame.image.load('code_game_back_floor.jpg')
hero = pygame.image.load('detective.png')
pot = pygame.image.load('teapot.png')
candle = pygame.image.load('candlestick.png')
box = pygame.image.load('wooden_box.png')

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

# запускаем бесконечный цикл
while game:
   # получаем список возможных действий игрока
   for event in pygame.event.get():
       # если пользователь нажал на крестик закрытия окна
       if event.type == pygame.QUIT:
           # выключаем цикл
           pygame.quit()
           # добавляем корректное завершение работы
           exit()

   # обновляем экран игры
   pygame.display.update()
   # добавляем к таймеру количество fps для частоты обновления основного цикла
   clock.tick(fps)

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

# размещаем новые поверхности на нашем экране
screen.blit(back, (0, 0))
screen.blit(hero, (15, 130))
screen.blit(candle, (675, 30))
screen.blit(box, (700, 180))
screen.blit(pot, (675, 275))

Как это работает:

  • screen — переменная, в которой хранится главное окно игры;
  • blit() — метод, который размещает одно изображение на другом;
  • для отрисовки мы передаём в метод переменную с картинкой и координаты в пикселях относительно той поверхности-изображения, на которой разместим картинку.

Вот что получилось. Пока ничего не движется:

Пишем игру на Python, в которой нужно уворачиваться от предметов

Все наши изображения в форматах .png и .jpeg. Pygame умеет работать с этими файлами, но для оптимизации их нужно конвертировать. Для этого не нужно самим менять формат, просто добавим метод .convert() — и pygame сама переделает изображения так, чтобы работать с ними быстрее.

Для png-изображений нужно использовать метод .convert_alpha(), чтобы не закрасились прозрачные части. Так это выглядит в коде:

# загружаем в переменные картинки из папки с нашим файлом
back = pygame.image.load('code_game_back_floor.jpg').convert()
hero = pygame.image.load('detective.png').convert_alpha()
pot = pygame.image.load('teapot.png').convert_alpha()
candle = pygame.image.load('candlestick.png').convert_alpha()
box = pygame.image.load('wooden_box.png').convert_alpha()

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

Чтобы что-то написать в pygame, нужно сделать 3 вещи:

  • создать объект шрифта;
  • создать объект текста;
  • отрисовать текст на одной из видимых поверхностей.

Для добавления нужного шрифта в игру файл шрифта нужно положить в папку со скриптом. Мы для проекта взяли свободно распространяемый шрифт press-start.

В код нужно добавить такую строку — в скобках указываем название файла и размер текста:

# создаём объект шрифта: в скобках указываем шрифт и размер через запятую
text_font = pygame.font.Font('prstartk.ttf', 15)

Используем шрифт и создадим текстовый объект:

# создаём текст: в скобках указываем текст, сглаживание (нужно или нет) и цвет
text_surface = text_font.render('Detective CODE Game', False, 'White')

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

back.blit(text_surface, (250, 15))

Проверяем:

Пишем игру на Python, в которой нужно уворачиваться от предметов

Анимация в игре

Когда мы запускаем игру, все изображения отрисовываются по 60 раз в секунду. Но их координаты не меняются, поэтому изображение кажется статичным.

Вот что нужно сделать для анимации:

  • занести в переменные координаты, которые мы передаём при отрисовке объектов методом blit;
  • менять эти координаты в зависимости от времени или нажатий клавиш.

Занесём все координаты в отдельные переменные. Это нужно сделать до главного цикла:

# объявляем переменные с начальными координатами для всех анимаций
hero_x_pos = 15
hero_y_pos = 130
pot_x_pos = 800
pot_y_pos = 275
candle_x_pos = 800
candle_y_pos = 30
box_x_pos = 800
box_y_pos = 180

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

screen.blit(candle, (candle_x_pos, candle_y_pos))

Заставим предметы лететь на главного героя. Сделаем это в несколько этапов.

Для двух предметов сделаем переменные-флаги. Они будут давать сигнал, что можно начинать двигаться.

# создаём сигнальные переменные
pot_flag = False
box_flag = False

Один предмет запустим сразу со скоростью 4 пикселя в кадр. Как только он пересечёт середину экрана, включится первая сигнальная переменная и второй предмет начнёт движение:

candle_x_pos -= 4
if candle_x_pos == 400:
   pot_flag = True
if pot_flag:
   pot_x_pos -= 4

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

if pot_x_pos == 400:
   box_flag = True
if box_flag:
   box_x_pos -= 4

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

if candle_x_pos == -100:
   candle_x_pos = 800

if pot_x_pos == -100:
   pot_x_pos = 800

if box_x_pos == -100:
   box_x_pos = 1000

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

Добавляем в главный цикл игры такой код:

# получаем список всех нажатых клавиш
keys = pygame.key.get_pressed()

# если нажата клавиша вверх, поднимаем картинку
if keys[pygame.K_UP]:
   hero_y_pos -= 20
# если нажата клавиша вниз, опускаем картинку
if keys[pygame.K_DOWN]:
   hero_y_pos += 20

Теперь можно уворачиваться от бесконечно летящих предметов.

Пишем игру на Python, в которой нужно уворачиваться от предметов

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

Если герой столкнётся с одним из предметов, игра на это никак не отреагирует. Надо добавить обработку коллизий, а для этого понадобится ещё один инструмент — прямоугольники Rect.

Работаем с инструментом Rect

Прямоугольники в pygame полезны по двум причинам:

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

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

Работаем с инструментом Rect
На этой картинке не хватает ещё одного узла — center

Для размещения изображений с помощью Rect нужно запомнить:

  • Вся информация о картинке по-прежнему размещается на поверхности.
  • При этом её координаты теперь находятся в прямоугольнике.

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

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

Пишем игру на Python, в которой нужно уворачиваться от предметов

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

# помещаем изображение в рамку прямоугольника
# в скобках задаём точку привязки на рамке и координаты для неё
hero_rect = hero.get_rect(bottomleft=(15, 300))

С привязкой к bottomleft мы задаём координаты не для верхней левой точки, а для нижней. Объект hero_rect нужно передать как координаты при отрисовке — в этот момент мы передаём координаты именно левой нижней точки, которые установили ранее:

screen.blit(hero, hero_rect)

Наш детектив встал ровно на твёрдую землю:

Пишем игру на Python, в которой нужно уворачиваться от предметов

Перемещение прямоугольников

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

Помещаем все остальные объекты в прямоугольники:

# помещаем изображение в рамку прямоугольника
# в скобках задаём точку привязки и координаты для неё
hero_rect = hero.get_rect(center=(hero_x_pos, hero_y_pos))
pot_rect = pot.get_rect(center=(pot_x_pos, pot_y_pos))
candle_rect = candle.get_rect(center=(candle_x_pos, candle_y_pos))
box_rect = box.get_rect(center=(box_x_pos, box_y_pos))

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

screen.blit(hero, hero_rect)
screen.blit(candle, candle_rect)
screen.blit(box, box_rect)
screen.blit(pot, pot_rect)

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

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

# запускаем движение всех предметов
candle_rect.left -= 4
# когда подсвечник пересёк половину экрана,
# меняем сигнальную переменную для чайника и начинаем его движение
if candle_rect.left <= 400:
   pot_flag = True
if pot_flag:
   pot_rect.left -= 4

# когда чайник пересёк половину экрана,
# меняем сигнальную переменную для ящика и начинаем его движение
if pot_rect.left <= 400:
   box_flag = True
if box_flag:
   box_rect.left -= 4

# обнуляем начальные координаты, когда правая грань
# скрылась за границей экрана
if candle_rect.right <= 0:
   candle_rect.left = 800
if pot_rect.right <= 0:
   pot_rect.left = 800
if box_rect.right <= 0:
   box_rect.left = 1000

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

С прямоугольниками удобно отслеживать позиции всех изображений — воспользуемся этим и реализуем столкновения главного героя и предметов. Для отслеживания столкновений между двумя объектами rect нужно к первому объекту применить метод colliderect() и в скобках указать второй. Это выражение вернёт True или False:

hero_rect.colliderect(candle_rect)

Чтобы наша игра реагировала на столкновения, добавим вывод текста в моменты коллизии:

# создаём новый объект текста до основного цикла игры
text_font_collide = pygame.font.Font('prstartk.ttf', 50)
text_collide = text_font_collide.render('CoLLiDE!!', False, 'Red')

В цикле игры после кода для перемещения объектов пишем:

# выводим сообщения о столкновении
if hero_rect.colliderect(candle_rect) or hero_rect.colliderect(pot_rect) or hero_rect.colliderect(box_rect):
   screen.blit(text_collide, (200, 165))

Смотрим, как это всё работает:

Пишем игру на Python, в которой нужно уворачиваться от предметов

Что дальше

В нашу игру уже можно играть, но можно многое добавить:

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

Этим займёмся в следующий раз.

import pygame
from sys import exit

pygame.init()

# объявляем ширину и высоту экрана
width = 800
height = 400

# создаём экран игры
screen = pygame.display.set_mode((width, height))

# устанавливаем количество кадров в секунду
fps = 60
# создаём объект таймера
clock = pygame.time.Clock()

# создаём объекты шрифта: в скобках указываем шрифт и размер через запятую
text_font = pygame.font.Font('prstartk.ttf', 15)
text_font_collide = pygame.font.Font('prstartk.ttf', 50)

# создаём новую поверхность земли отдельным объектом surface
# в финальном коде не используется
# 1 - задаём размеры:
width_ts = 800
height_ts = 200
# 2 - создаём  поверхность по  размерам
test_surface = pygame.Surface((width_ts, height_ts))
# 3 - добавляем цвет
test_surface.fill('Brown')

# загружаем в переменные картинки из папки с нашим файлом
back = pygame.image.load('code_game_back_floor.jpg').convert()
hero = pygame.image.load('detective.png').convert_alpha()
pot = pygame.image.load('teapot.png').convert_alpha()
candle = pygame.image.load('candlestick.png').convert_alpha()
box = pygame.image.load('wooden_box.png').convert_alpha()

# объявляем переменные с начальными координатами для всех анимаций
hero_x_pos = 75
hero_y_pos = 180
candle_x_pos = 900
candle_y_pos = 70
box_x_pos = 900
box_y_pos = 200
pot_x_pos = 900
pot_y_pos = 345

# помещаем изображения в рамки прямоугольника
# в скобках задаём точку привязки и координаты для неё
hero_rect = hero.get_rect(center=(hero_x_pos, hero_y_pos))
pot_rect = pot.get_rect(center=(pot_x_pos, pot_y_pos))
candle_rect = candle.get_rect(center=(candle_x_pos, candle_y_pos))
box_rect = box.get_rect(center=(box_x_pos, box_y_pos))

# создаём сигнальные переменные
pot_flag = False
box_flag = False

# создаём объект текста: в скобках указываем текст, сглаживание и цвет
text_surface = text_font.render('Detective CODE Game', False, 'White')
text_collide = text_font_collide.render('CoLLiDE!!', False, 'Red')

# даём название окну игры
pygame.display.set_caption('Detective CODE Game')

# объявляем переменную-флаг для цикла игры
game = True

# запускаем бесконечный цикл
while game:
   # получаем список возможных действий игрока
   for event in pygame.event.get():
       # если пользователь нажал на крестик закрытия окна
       if event.type == pygame.QUIT:
           # выключаем цикл
           pygame.quit()
           # добавляем корректное завершение работы
           exit()

       # получаем список всех нажатых клавиш
       keys = pygame.key.get_pressed()

       # если нажата клавиша вверх, двигаем картинку вверх
       if keys[pygame.K_UP]:
           hero_rect.top -= 20
       # если нажата клавиша вниз, двигаем картинку вниз
       if keys[pygame.K_DOWN]:
           hero_rect.top += 20

   # размещаем все поверхности на нашем экране
   screen.blit(back, (0, 0))
   screen.blit(hero, hero_rect)
   screen.blit(candle, candle_rect)
   screen.blit(box, box_rect)
   screen.blit(pot, pot_rect)
   back.blit(text_surface, (250, 15))

   # запускаем движение всех предметов
   candle_rect.left -= 4
   # когда подсвечник пересёк половину экрана,
   # меняем сигнальную переменную для чайника и начинаем его движение
   if candle_rect.left <= 400:
       pot_flag = True
   if pot_flag:
       pot_rect.left -= 4

   # когда чайник пересёк половину экрана,
   # меняем сигнальную переменную для ящика и начинаем его движение
   if pot_rect.left <= 400:
       box_flag = True
   if box_flag:
       box_rect.left -= 4

   # обнуляем начальные координаты, когда правая грань
   # скрылась за границей экрана
   if candle_rect.right <= 0:
       candle_rect.left = 800
   if pot_rect.right <= 0:
       pot_rect.left = 800
   if box_rect.right <= 0:
       box_rect.left = 1000

# выводим сообщения о столкновении
if hero_rect.colliderect(candle_rect) or hero_rect.colliderect(pot_rect) or hero_rect.colliderect(box_rect):
   screen.blit(text_collide, (200, 165))

   # обновляем экран игры
   pygame.display.update()
   # добавляем к таймеру количество fps для частоты обновления основного цикла
   clock.tick(fps)

Редактор:

Инна Долога

Обложка:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Маша Климентьева

Соцсети:

Юлия Зубарева

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
hard