image

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

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

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


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

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

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

Комментарии (2)


  1. 640509-040147
    28.01.2018 00:11

    Kivy не пробовали?


  1. HeaTTheatR
    28.01.2018 05:50

    PyGame явно проигрывает Kivy.