Статьи

Сборка игр с Python 3 и Pygame: часть 4

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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’

Когда мяч попадает на весло, он отскакивает. Если он достигнет верхней части весла, он отскочит назад, но останется с той же горизонтальной составляющей скорости.

Но если он коснется стороны весла, он отскочит к противоположной стороне (влево или вправо) и продолжит движение вниз, пока не достигнет пола. Код использует функцию пересечения ().

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
# Hit paddle
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])

Когда весло пропускает мяч на пути вниз (или если мяч ударяет по веслу сбоку), мяч будет продолжать падать и в конечном итоге попадет на пол. В этот момент игрок теряет жизнь, и мяч воссоздается, поэтому игра может продолжаться. Игра заканчивается, когда у игрока заканчиваются жизни.

1
2
3
4
5
6
7
# Hit floor
if self.ball.top > c.screen_height:
    self.lives -= 1
    if self.lives == 0:
        self.game_over = True
    else:
        self.create_ball()

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

1
2
3
4
5
6
7
# Hit ceiling
if self.ball.top < 0:
    self.ball.speed = (s[0], -s[1])
 
# Hit wall
if self.ball.left < 0 or self.ball.right > c.screen_width:
    self.ball.speed = (-s[0], s[1])

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
# Hit brick
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])

Большинство игр имеют некоторый интерфейс. Breakout имеет простое меню, в котором есть две кнопки «PLAY» и «QUIT». Меню появляется в начале игры и исчезает, когда игрок нажимает кнопку «ИГРАТЬ». Давайте посмотрим, как реализованы кнопки и меню и как они интегрируются в игру.

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

Кнопка выполнена в виде прямоугольника с цветом фона и текстом, отображаемым поверх него. Кнопка также получает функцию on_click (по умолчанию функция noop lambda), которая вызывается при нажатии кнопки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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() функция on_click() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 , используемое для рисования фонового прямоугольника, всегда возвращает цвет, соответствующий текущему состоянию кнопки, поэтому игроку ясно, что кнопка активна:

1
2
3
4
5
@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 .

01
02
03
04
05
06
07
08
09
10
11
12
13
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, что приводит к завершению последовательности игры.

01
02
03
04
05
06
07
08
09
10
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 , меню отображается; когда они удалены, это скрыто. Так просто, как, что.

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

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

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

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