Метеоры падают, корабль летает — а друг сквозь друга они проходят, как привидения. Игре не хватает вопроса «эти двое коснулись или нет?». Научим её отвечать на него для прямоугольников: сначала на пальцах, потом функцией в logic.py, потом средствами pygame.
Когда две коробки пересекаются
Представьте две картонные коробки на полу, вид сверху. Как понять, что они перекрываются, не двигая их и не меряя площадь? Проще спросить наоборот: когда они точно не пересекаются? Когда одна стоит целиком левее другой. Или целиком правее. Или целиком выше. Или целиком ниже. Хватит любого одного «целиком» — и пересечения нет. Значит, коробки пересекаются тогда и только тогда, когда не верно ни одно из четырёх «целиком».
Теперь переведём это на язык pygame. Прямоугольник описывается четырьмя числами: x и y — левый верхний угол, w — ширина, h — высота. Помните: x растёт вправо, а y растёт вниз. Поэтому правый край прямоугольника лежит в x + w, а нижний — в y + h. Пересечение областей двух объектов в играх называют столкновением. Назовём прямоугольники A и B и запишем четыре условия, которые должны выполниться одновременно:
| Условие | Что оно говорит |
|---|---|
ax < bx + bw | левый край A левее правого края B: A не стоит целиком правее B |
ax + aw > bx | правый край A правее левого края B: A не стоит целиком левее B |
ay < by + bh | верх A выше низа B: A не стоит целиком под B |
ay + ah > by | низ A ниже верха B: A не стоит целиком над B |
Если все четыре верны, прямоугольники пересекаются; одно неверное — и зазор есть.
Касание краем — не столкновение
Знаки в таблице строгие: только < и >, без равенства. Если правый край A ровно совпал с левым краем B, условие ax + aw > bx не выполняется, и столкновения нет. Так же ведёт себя pygame: две коробки, поставленные вплотную, друг на друге не стоят.
Функция rects_overlap в logic.py
Превратим четыре условия в функцию-судью: ей отдают два прямоугольника, она отвечает True (пересекаются) или False. Прямоугольники будем передавать кортежами (x, y, w, h). Кортеж — это как список, только в круглых скобках и неизменяемый: после создания его содержимое не поменять. Для нас важно другое: это просто четыре числа, и pygame для них не нужен. Поэтому судья живёт в logic.py — рядом с clamp и wrap — и его можно проверять отдельно от окна.
def rects_overlap(a, b):
ax, ay, aw, ah = a
bx, by, bw, bh = b
return (ax < bx + bw and ax + aw > bx
and ay < by + bh and ay + ah > by)
Разбор по строкам
def rects_overlap(a, b):— функция принимает два кортежа:aиb.ax, ay, aw, ah = a— распаковка: четыре числа кортежа раскладываются по четырём переменным. Мы так уже делали в цикле по звёздам:for x, y, size in stars.bx, by, bw, bh = b— то же самое для второго прямоугольника.return (ax < bx + bw and ax + aw > bx… — четыре условия из таблицы, соединённые словомand:Trueполучится, только если верны все четыре. Скобки позволяют перенести длинное выражение на вторую строку.
Проверим судью на четырёх примерах. logic.py не импортирует pygame, поэтому проверку можно запустить в обычной консоли Python.
from logic import rects_overlap
box = (0, 0, 100, 100)
print(rects_overlap(box, (50, 50, 100, 100)))
print(rects_overlap(box, (100, 0, 100, 100)))
print(rects_overlap(box, (300, 300, 50, 50)))
print(rects_overlap(box, (10, 10, 20, 20)))
True
False
False
True
Разбор по строкам
box = (0, 0, 100, 100)— квадрат 100 на 100 в левом верхнем углу; его правый и нижний края лежат на отметке 100.- Первый вызов — квадрат сдвинут на 50 по обеим осям, углы перекрываются:
True. - Второй вызов — квадрат начинается ровно в
x = 100, то есть касается краем:False. - Третий вызов — квадрат далеко справа и внизу:
False. - Четвёртый вызов — маленький квадрат целиком внутри большого:
True. Пересечением считается и такой случай.
Готовые средства pygame
Rect.colliderect
В pygame прямоугольник — это объект pygame.Rect, он уже знаком вам по спрайтам: именно он лежит в sprite.rect. У него есть метод colliderect(other), и делает он ровно то же, что наша rects_overlap: возвращает True, если прямоугольники пересекаются, и False, если нет или если они только касаются краем.
import pygame
a = pygame.Rect(0, 0, 100, 100)
b = pygame.Rect(50, 50, 100, 100)
c = pygame.Rect(100, 0, 100, 100)
print(a.colliderect(b))
print(a.colliderect(c))
True
False
Зачем тогда своя функция? Теперь вы знаете, что внутри colliderect нет магии, а ещё работает главный приём курса — логика отдельно от отрисовки: функцию на кортежах можно проверить без окна. В самой игре будем пользоваться готовыми методами pygame.
spritecollide: сразу всех из группы
В игре метеоров много, и сравнивать корабль с каждым вручную в цикле неудобно. Представьте охранника у ворот: вы говорите ему «проверь всю очередь и назови тех, кто задел корабль; если надо, сразу выведи их с площадки». В pygame эту роль играет функция pygame.sprite.spritecollide(sprite, group, dokill). Она берёт sprite.rect, сравнивает его с rect каждого спрайта из группы group и возвращает список задетых спрайтов. Если никто не задет, список пустой.
Третий параметр, dokill («убить ли»), отвечает за судьбу задетых. При True каждый из них сразу получает kill() и исчезает из всех групп, а значит, и с экрана. При False все остаются на месте.
hits = pygame.sprite.spritecollide(player, meteors, True)
if hits:
print("Удар!")
Разбор по строкам
pygame.sprite.spritecollide(player, meteors, True)— проверить корабльplayerпротив каждого метеора из группыmeteors; задетые метеоры сразу удалить.hits = …— результат — список задетых метеоров. Если метеоров задето два, в списке будет два элемента, иlen(hits)вернёт2.if hits:— пустой список в условии считается «нет», непустой — «да». Так мы спрашиваем: «был ли хоть один удар?».print("Удар!")— пока просто сообщаем в консоль. Минус жизнь и очки придут позже.
Осторожно с dokill=False
Метеор перекрывает корабль не один кадр, а несколько. При dokill=False он находился бы в каждом кадре, до 60 раз в секунду, и один пролёт превратился бы в десятки ударов. Пока оставляем True.
Почему прямоугольник немного нечестный
Корабль — треугольник, но его rect прямоугольный и охватывает его целиком, так что углы рамки пустые. Метеор круглый, а его rect квадратный. Когда угол квадрата метеора заходит в пустой угол рамки корабля, colliderect честно отвечает True, хотя на экране между фигурами зазор. Игрок скажет: «Это нечестно, я не касался!».
Для простых игр это допустимо: прямоугольники считаются очень дёшево. Но круглым метеорам нужна проверка точнее, и в следующем уроке мы возьмём круги.
Практика: метеоры больше не привидения
Сделаем так, чтобы корабль «чувствовал» метеоры. Правим два файла: logic.py и main.py. В sprites.py менять ничего не нужно: и у корабля, и у метеоров уже есть rect, а это всё, что нужно для spritecollide.
Шаг 1. Судья в logic.py
Откройте logic.py и допишите в самый конец файла, после SpawnTimer:
def rects_overlap(a, b):
ax, ay, aw, ah = a
bx, by, bw, bh = b
return (ax < bx + bw and ax + aw > bx
and ay < by + bh and ay + ah > by)
Проверим судью в терминале (из папки kometa, с активированным окружением). Первый вызов — перекрытие, второй — касание краем:
python -c "from logic import rects_overlap; print(rects_overlap((0, 0, 100, 100), (50, 50, 100, 100))); print(rects_overlap((0, 0, 100, 100), (100, 0, 100, 100)))"
True
False
Шаг 2. Счётчик ударов
В main.py между строками show_boxes = False и running = True (до главного цикла) заведите счётчик ударов:
show_boxes = False
hits_total = 0
running = True
Шаг 3. Проверка столкновений
Внутри цикла найдите строку all_sprites.update(dt, keys) и сразу после неё вставьте проверку. Порядок важен: сначала все двигаются, потом мы смотрим, кто с кем пересёкся, и только потом рисуем.
while running:
# ... всё, что выше, остаётся как есть ...
all_sprites.update(dt, keys)
hits = pygame.sprite.spritecollide(player, meteors, True)
if hits:
hits_total += len(hits)
print("Удар!")
# ... ниже по-прежнему идёт цикл for star in stars ...
Разбор по строкам
spritecollide(player, meteors, True)— ищем метеоры, пересекающиеся с кораблём, и сразу убираем их из всех групп.if hits:— блок выполняется только в кадрах, где список не пустой.hits_total += len(hits)— прибавляем к счётчику столько ударов, сколько метеоров задето в этом кадре (обычно один, но иногда два сразу).print("Удар!")— сообщение в консоль, чтобы увидеть момент столкновения.
Шаг 4. Счётчик в заголовке окна
Число метеоров в заголовке нам больше не нужно, выводим удары. Найдите строку с set_caption(TITLE + " | метеоров: " …) и замените её:
while running:
# ... рисование звёзд, спрайтов и рамок остаётся как есть ...
pygame.display.set_caption(TITLE + " | ударов: " + str(hits_total))
pygame.display.flip()
Запуск и что должно получиться
python main.py
Направьте корабль под метеор: в момент касания метеор исчезает, в консоли появляется Удар!, а число в заголовке растёт на единицу. Пролетевшие мимо метеоры остаются невредимыми.
Теперь нажмите F1: зелёные рамки покажут настоящие прямоугольники столкновений. Пусть угол рамки метеора едва зайдёт в угол рамки корабля: удар засчитается, хотя картинки не коснулись. Это та самая «нечестность» прямоугольников.
Проверьте себя
- При касании метеора кораблём в консоли появляется
Удар!, метеор исчезает, а число в заголовке растёт. - За один пролёт сквозь корабль удар засчитывается ровно один раз, а не десятки раз подряд.
- С F1 видно, что рамка корабля больше самого треугольника: удар засчитывается и по пустым углам рамки.
- Поставьте в
spritecollideFalseвместоTrueи пролетите сквозь метеор: счётчик взлетит на много единиц. ВернитеTrue.
Частые ошибки
Касание краем считается ударом
Что видно: в тестах или при проверке на примерах ровно прижатые друг к другу прямоугольники дают True. Почему: в условиях стоит <= или >= вместо строгих < и >. Как исправить: оставить строгие знаки, как у Rect.colliderect.
Проверили только одну ось
Что видно: удар засчитывается, когда метеор ещё высоко над кораблём. Почему: в функции остались условия только по x; без условий по y любой метеор в том же вертикальном столбце считается задетым. Как исправить: все четыре условия через and.
dokill=False, а метеор не убирается
Что видно: один пролёт метеора сквозь корабль добавляет десятки ударов. Почему: метеор остаётся в группе и находится в каждом кадре, пока перекрывается с кораблём, а кадров в секунду 60. Как исправить: передавать True третьим аргументом, чтобы задетый метеор исчезал сразу.
NameError: name 'hits_total' is not defined
Что видно: игра падает на первом же столкновении или на выводе заголовка. Почему: счётчик hits_total = 0 не добавлен перед циклом. Как исправить: завести его рядом с show_boxes, до while running:.
Что важно запомнить
- Прямоугольники
(x, y, w, h)пересекаются, когда верны четыре условия сразу: по два на каждую ось. Достаточно одного неверного — и зазор есть. - Касание краем не считается столкновением: знаки строгие,
<и>. Так же устроенRect.colliderect. rects_overlap(a, b)вlogic.pyработает на кортежах без pygame, поэтому её удобно проверять отдельно от окна.pygame.sprite.spritecollide(sprite, group, dokill)возвращает список задетых спрайтов; пустой список вifозначает «ударов нет».- С
dokill=Trueзадетые спрайты исчезают сразу; сFalseони остаются, и удар будет находиться в каждом кадре. - Прямоугольник быстрый, но неточный: он засчитывает удар и по пустым углам.
Дальше — круги: как проверить столкновение по расстоянию между центрами и почему так честнее для круглых метеоров.