![image](https://habrastorage.org/getpro/habr/post_images/e38/895/a15/e38895a159ff5bbd8da2f1040b61d219.gif)
Это четвёртая из пяти частей туториала, посвящённого созданию игр с помощью Python 3 и Pygame. В третьей части мы углубились в сердце Breakout и узнали, как обрабатывать события, познакомились с основным классом Breakout и увидели, как перемещать разные игровые объекты.
(Остальные части туториала: первая, вторая, третья, пятая.)
В этой части мы узнаем, как распознавать коллизии и что случается, когда мяч ударяется об разные объекты: ракетку, кирпичи, стены, потолок и пол. Наконец, мы рассмотрим важную тему пользовательского интерфейса и в частности то, как создать меню из собственных кнопок.
Распознавание коллизий
В играх объекты сталкиваются друг с другом, и Breakout не является исключением. В основном с объектами сталкивается мяч. В методе
handle_ball_collisions()
есть встроенная функция под названием intersect()
, которая используется для проверки того, ударился ли мяч об объект, и того, где он столкнулся с объектом. Она возвращает 'left', 'right', 'top', 'bottom' или None, если мяч не столкнулся с объектом.def handle_ball_collisions(self):
def intersect(obj, ball):
edges = dict(
left=Rect(obj.left, obj.top, 1, obj.height),
right=Rect(obj.right, obj.top, 1, obj.height),
top=Rect(obj.left, obj.top, obj.width, 1),
bottom=Rect(obj.left, obj.bottom, obj.width, 1))
collisions = set(edge for edge, rect in edges.items() if
ball.bounds.colliderect(rect))
if not collisions:
return None
if len(collisions) == 1:
return list(collisions)[0]
if 'top' in collisions:
if ball.centery >= obj.top:
return 'top'
if ball.centerx < obj.left:
return 'left'
else:
return 'right'
if 'bottom' in collisions:
if ball.centery >= obj.bottom:
return 'bottom'
if ball.centerx < obj.left:
return 'left'
else:
return 'right'
Столкновение мяча с ракеткой
Когда мяч стукается об ракетку, он отскакивает. Если он ударяется о верхнюю часть ракетки, то отражается обратно вверх, но сохраняет тот же компонент горизонтальной скорости.
Но если он ударяется о боковую часть ракетки, то отскакивает в противоположную сторону (влево или вправо) и продолжает движение вниз, пока не столкнётся с полом. В коде используется функция
intersect()
.# Удар об ракетку
s = self.ball.speed
edge = intersect(self.paddle, self.ball)
if edge is not None:
self.sound_effects['paddle_hit'].play()
if edge == 'top':
speed_x = s[0]
speed_y = -s[1]
if self.paddle.moving_left:
speed_x -= 1
elif self.paddle.moving_left:
speed_x += 1
self.ball.speed = speed_x, speed_y
elif edge in ('left', 'right'):
self.ball.speed = (-s[0], s[1])
Столкновение с полом
Когда ракетка пропускает мяч на пути вниз (или мяч ударяется об ракетку сбоку), то мяч продолжает падать и затем ударяется об пол. В этот момент игрок теряет жизнь и мяч создаётся заново, чтобы игра могла продолжаться. Игра завершается, когда у игрока заканчиваются жизни.
# Удар об пол
if self.ball.top > c.screen_height:
self.lives -= 1
if self.lives == 0:
self.game_over = True
else:
self.create_ball()
Столкновение с потолком и стенами
Когда мяч ударяется об стены или потолок, он просто отскакивает от них.
# Удар об потолок
if self.ball.top < 0:
self.ball.speed = (s[0], -s[1])
# Удар об стену
if self.ball.left < 0 or self.ball.right > c.screen_width:
self.ball.speed = (-s[0], s[1])
Столкновение с кирпичами
Когда мяч ударяется об кирпич, это является основным событием игры Breakout — кирпич исчезает, игрок получает очко, мяч отражается назад и происходят ещё несколько событий (звуковой эффект, а иногда и спецэффект), которые я рассмотрю позже.
Чтобы определить, что мяч ударился об кирпич, код проверят, пересекается ли какой-нибудь из кирпичей с мячом:
# Удар об кирпич
for brick in self.bricks:
edge = intersect(brick, self.ball)
if not edge:
continue
self.bricks.remove(brick)
self.objects.remove(brick)
self.score += self.points_per_brick
if edge in ('top', 'bottom'):
self.ball.speed = (s[0], -s[1])
else:
self.ball.speed = (-s[0], s[1])
Программирование игрового меню
В большинстве игр есть какой-нибудь UI. В Breakout есть простое меню с двумя кнопками, 'PLAY' и 'QUIT'. Меню отображается в начале игры и пропадает, когда игрок нажимает на 'PLAY'. Давайте посмотрим, как реализуются кнопки и меню, а также как они интегрируются в игру.
Создание кнопок
В Pygame нет встроенной библиотеки UI. Есть сторонние расширения, но для меню я решил создать свои кнопки. Кнопка — это игровой объект, имеющий три состояния: нормальное, выделенное и нажатое. Нормальное состояние — это когда мышь не находится над кнопкой, а выделенное состояние — когда мышь находится над кнопкой, но левая кнопка мыши ещё не нажата. Нажатое состояние — это когда мышь находится над кнопкой и игрок нажал на левую кнопку мыши.
Кнопка реализуется как прямоугольник с фоновым цветом и текст, отображаемый поверх него. Также кнопка получает функцию on_click (по умолчанию являющуюся пустой лямбда-функцией), которая вызывается при нажатии кнопки.
import pygame
from game_object import GameObject
from text_object import TextObject
import config as c
class Button(GameObject):
def __init__(self,
x,
y,
w,
h,
text,
on_click=lambda x: None,
padding=0):
super().__init__(x, y, w, h)
self.state = 'normal'
self.on_click = on_click
self.text = TextObject(x + padding,
y + padding, lambda: text,
c.button_text_color,
c.font_name,
c.font_size)
def draw(self, surface):
pygame.draw.rect(surface,
self.back_color,
self.bounds)
self.text.draw(surface)
Кнопка обрабатывает собственные события мыши и изменяет своё внутреннее состояние на основании этих событий. Когда кнопка находится в нажатом состоянии и получает событие
MOUSEBUTTONUP
, это означает, что игрок нажал на кнопку, и вызывается функция on_click()
.def handle_mouse_event(self, type, pos):
if type == pygame.MOUSEMOTION:
self.handle_mouse_move(pos)
elif type == pygame.MOUSEBUTTONDOWN:
self.handle_mouse_down(pos)
elif type == pygame.MOUSEBUTTONUP:
self.handle_mouse_up(pos)
def handle_mouse_move(self, pos):
if self.bounds.collidepoint(pos):
if self.state != 'pressed':
self.state = 'hover'
else:
self.state = 'normal'
def handle_mouse_down(self, pos):
if self.bounds.collidepoint(pos):
self.state = 'pressed'
def handle_mouse_up(self, pos):
if self.state == 'pressed':
self.on_click(self)
self.state = 'hover'
Свойство
back_color
, используемое для отрисовки фонового прямоугольника, всегда возвращает цвет, соответствующий текущему состоянию кнопки, чтобы игроку было ясно, что кнопка активна:@property
def back_color(self):
return dict(normal=c.button_normal_back_color,
hover=c.button_hover_back_color,
pressed=c.button_pressed_back_color)[self.state]
Создание меню
Функция
create_menu()
создаёт меню с двумя кнопками с текстом 'PLAY' и 'QUIT'. Она имеет две встроенные функции, on_play()
и on_quit()
, которые она передаёт соответствующей кнопке. Каждая кнопка добавляется в список objects
(для отрисовки), а также в поле menu_buttons
.def create_menu(self):
for i, (text, handler) in enumerate((('PLAY', on_play),
('QUIT', on_quit))):
b = Button(c.menu_offset_x,
c.menu_offset_y + (c.menu_button_h + 5) * i,
c.menu_button_w,
c.menu_button_h,
text,
handler,
padding=5)
self.objects.append(b)
self.menu_buttons.append(b)
self.mouse_handlers.append(b.handle_mouse_event)
При нажатии кнопки PLAY вызывается функция
on_play()
, удаляющая кнопки из списка objects
, чтобы они больше не отрисовывались. Кроме того, значения булевых полей, которые запускают начало игры — is_game_running
и start_level
— становятся равными True.При нажатии кнопки QUIT
is_game_running
принимает значение False
(фактически ставя игру на паузу), а game_over
присваивается значение True, что приводит к срабатыванию последовательности завершения игры.def on_play(button):
for b in self.menu_buttons:
self.objects.remove(b)
self.is_game_running = True
self.start_level = True
def on_quit(button):
self.game_over = True
self.is_game_running = False
Отображение и сокрытие игрового меню
Отображение и сокрытие меню выполняются неявным образом. Когда кнопки находятся в списке
objects
, меню видимо; когда они удаляются, оно скрывается. Всё очень просто.Можно создать встроенное меню с собственной поверхностью, которое рендерит свои подкомпоненты (кнопки и другие объекты), а затем просто добавлять/удалять эти компоненты меню, но для такого простого меню это не требуется.
Подводим итог
В этой части мы рассмотрели распознавание коллизий и то, что происходит, когда мяч сталкивается с разными объектами: ракеткой, кирпичами, стенами, полом и потолком. Также мы создали меню с собственными кнопками, которое можно скрывать и отображать по команде.
В последней части серии мы рассмотрим завершение игры, отслеживание очков и жизней, звуковые эффекты и музыку.
Затем мы разработаем сложную систему спецэффектов, добавляющих в игру немного специй. Наконец, мы обсудим дальнейшее развитие и возможные улучшения.