Отображение и сокрытие игрового меню

Создание игр на Python 3 и Pygame: Часть 4

https://habr.com/ru/post/347266/


Это четвёртая из пяти частей туториала, посвящённого созданию игр с помощью 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, меню видимо; когда они удаляются, оно скрывается. Всё очень просто.

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

Подводим итог

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

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

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




double arrow
Сейчас читают про: