Rect, спрайты и столкновения в PyGame: get_rect, colliderect и группы

Rect хранит позицию и размер объекта, спрайт добавляет к нему картинку, а группы рисуют и сталкивают их одной строкой. Разбираем атрибуты Rect, get_rect, move_ip и clamp_ip, colliderect и spritecollide с масками. Каждый сниппет запущен на pygame 2.6.1, в конце рабочая игра на 70 строк и таблица из семи ошибок.
Статью написал:
Ваня Буявец, продюсер, основатель Checkroi
Ваня Буявец
Основатель Checkroi, продюсер, эксперт в выборе онлайн-курсов
Все 2560 статей автора Подписаться на Телеграм-канал
Одобрено экспертом:
Наташа Буявец, основатель Checkroi, эксперт по онлайн-курсам
Наташа Буявец
Основательница Checkroi, продюсер Youtube-каналов, эксперт по онлайн-курсам
Все 3218 экспертных мнений Подписаться на Телеграм-канал
Обложка: Rect, спрайты и столкновения в PyGame: get_rect, colliderect и группы

Rect в PyGame — это прямоугольник из четырёх целых чисел: x, y, ширина и высота. Сам он ничего не рисует: хранит, где объект стоит и сколько места занимает. Через него двигают персонажа (rect.x += 5), прижимают к краям экрана (clamp_ip) и проверяют столкновения (colliderect). Спрайт — это класс с двумя полями, image и rect, а группа спрайтов рисует и обновляет их одной строкой и сама ищет пересечения через spritecollide. Ниже каждый метод с кодом и выводом, который я снял на pygame 2.6.1. Если у вас ещё нет окна и игрового цикла, начните с инструкции про первое окно в PyGame, а сюда вернитесь за движением и столкновениями.

CheckroiCheckroiПодборка курсов по Python560 курсов • 46 школСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить

Что такое Rect и зачем он каждому объекту игры

Рой-разработчик игр рисует прямоугольник хитбокса вокруг персонажа на экране

Создаётся прямоугольник вызовом pygame.Rect(x, y, width, height). Первые два числа — левый верхний угол, вторые два — размер. Всё остальное PyGame считает сам: правую границу, центр, середину нижней стороны. Примеры ниже работают на любой версии pygame 2; если библиотека ещё не стоит, есть короткая инструкция, как установить PyGame через pip.

import pygame

player = pygame.Rect(100, 50, 40, 60)
print(player)                    # <rect(100, 50, 40, 60)>
print(player.x, player.y, player.width, player.height)   # 100 50 40 60
print(player.left, player.top, player.right, player.bottom)  # 100 50 140 110
print(player.center, player.topleft, player.bottomright)     # (120, 80) (100, 50) (140, 110)

Любой из этих атрибутов можно не только читать, но и присваивать. Напишете player.center = (320, 240) — прямоугольник переедет так, чтобы центр оказался в этой точке, а размер не изменится. Это удобнее, чем пересчитывать x и y руками.

Атрибут Что хранит Типичное применение
x, y (то же, что left, top) Левый верхний угол Движение по осям
right, bottom Правая и нижняя границы Проверка выхода за экран
width, height (w, h, size) Размер Хитбокс, масштаб
center, centerx, centery Центр Поставить объект посередине
topleft, topright, bottomleft, bottomright Углы парой чисел Привязка к углу экрана
midtop, midbottom, midleft, midright Середины сторон Персонаж «стоит» на полу: midbottom

Два свойства, о которые спотыкаются чаще всего. Первое: координаты Rect всегда целые. Документация говорит об этом одной строкой, а на практике это значит, что дробное движение молча обнуляется (разбираем ниже, в разделе про ошибки). Второе: right и bottom не входят в прямоугольник. У Rect(100, 50, 40, 60) правая граница 140, но точка с x = 140 уже снаружи. Поэтому collidepoint(140, 80) вернёт False.

pygame.Rect(0, 0, 0, 0) — законный объект, его часто заводят как заглушку, а потом задают size и center. Но пока размер нулевой, ни одна проверка столкновений с ним не сработает: так написано в справке по pygame.Rect, и мой прогон это подтвердил.

Как нарисовать прямоугольник: pygame.draw.rect

Сам по себе Rect невидим. Чтобы его показать, нужен pygame.draw.rect(surface, color, rect, width=0, border_radius=0). Параметр width при нуле заливает фигуру целиком, при положительном числе рисует только рамку такой толщины. Скруглённые углы через border_radius появились в pygame 2.0.

screen = pygame.display.set_mode((640, 480))

pygame.draw.rect(screen, (255, 0, 0), player)                       # заливка
pygame.draw.rect(screen, "white", player, width=2, border_radius=8)  # рамка 2 px
pygame.draw.rect(screen, "green", (10, 10, 50, 20))                 # можно кортежем

Цвет принимает и кортеж RGB, и строку с именем вроде "dodgerblue". Функция возвращает Rect с областью, которую изменила, поэтому её результат можно тут же сохранить в переменную. Рядом в том же модуле живут draw.circle, draw.line и draw.polygon, но для столкновений вам всё равно понадобится прямоугольник.

get_rect: прямоугольник из картинки или экрана

Когда у объекта есть изображение, Rect удобнее спросить у поверхности, чем собирать руками. image.get_rect() вернёт прямоугольник размера картинки в точке (0, 0), а именованные аргументы сразу расставят его по месту.

img = pygame.image.load("hero.png").convert_alpha()   # или pygame.Surface((32, 48))
rect = img.get_rect(center=(320, 240))
print(rect)          # <rect(304, 216, 32, 48)>

screen_rect = screen.get_rect()
print(screen_rect)   # <rect(0, 0, 640, 480)>

Второй вызов, screen.get_rect(), даёт прямоугольник всего окна. Он нужен постоянно: центр экрана для стартовой позиции, границы для проверки «улетел ли объект», рамка для clamp_ip. Сохраните его в переменную один раз, вызывать каждый кадр незачем.

Движение и границы экрана: move, move_ip и clamp_ip

Двигать прямоугольник можно тремя способами, и разница между ними в том, меняется ли исходный объект.

  1. rect.x += 5 или rect.y -= 3. Прямое присваивание, самый частый вариант в игровом цикле.
  2. new = rect.move(5, 0). Возвращает новый Rect, старый не трогает. Полезно, когда нужно проверить «а что будет, если сдвинуться», не сдвигаясь.
  3. rect.move_ip(5, 0). Суффикс _ip означает in place: метод меняет сам объект и ничего не возвращает.
a = pygame.Rect(10, 10, 20, 20)
b = a.move(5, 0)
print(a, b)      # <rect(10, 10, 20, 20)> <rect(15, 10, 20, 20)>

a.move_ip(5, 0)
print(a)         # <rect(15, 10, 20, 20)>

a.topleft = (-30, 700)      # улетел за экран
a.clamp_ip(screen_rect)
print(a)         # <rect(0, 460, 20, 20)>

Последний фрагмент решает задачу «ограничить позицию rect пределами экрана» одной строкой. clamp_ip(screen_rect) вталкивает прямоугольник внутрь другого: если он вылез влево, x станет 0, если провалился вниз, bottom совпадёт с нижней границей окна. Никаких четырёх if с проверкой каждой стороны. Тот же приём без _ip вернёт копию, а исходник оставит на месте.

Обработку клавиш для движения удобно совместить с этим: читаете pygame.key.get_pressed(), меняете rect.x, в конце кадра вызываете clamp_ip. Сам цикл с событиями и clock.tick(60) разобран в статье про окно и игровой цикл, а если конструкция while running вызывает вопросы, загляните в разбор цикла while в Python.

Канал основателя Checkroi Вани Буявца3 700 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединиться

Столкновения прямоугольников: colliderect, collidepoint и collidelist

Два полупрозрачных прямоугольника пересекаются на координатной сетке, область пересечения подсвечена

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

p = pygame.Rect(0, 0, 50, 50)
e = pygame.Rect(40, 40, 50, 50)
f = pygame.Rect(50, 0, 50, 50)

print(p.colliderect(e))   # True  — перекрываются на 10 px
print(p.colliderect(f))   # False — только касаются границей
print(p.collidepoint(10, 10))   # True
print(p.collidepoint(50, 50))   # False — правая и нижняя границы снаружи

Второй результат многих удивляет: прямоугольники, которые стоят вплотную, по мнению PyGame не сталкиваются. Документация формулирует это как «любая часть пересекается, кроме общих границ». Для платформера это как раз то, что нужно: персонаж стоит на полу, и ложное столкновение с ним не срабатывает каждый кадр.

Метод Что проверяет Что возвращает
colliderect(other) Пересечение с одним Rect True / False
collidepoint(x, y) Точка внутри (например, курсор мыши над кнопкой) True / False
collidelist(list) Пересечение со списком Rect Индекс первого совпадения или -1
collidelistall(list) То же, но все совпадения Список индексов, пустой при промахе
contains(other) Другой Rect целиком внутри True / False
clip(other) Область пересечения Новый Rect (нулевой, если нет пересечения)
enemies = [pygame.Rect(200, 200, 30, 30), pygame.Rect(30, 30, 30, 30), pygame.Rect(45, 45, 10, 10)]
print(p.collidelist(enemies))      # 1
print(p.collidelistall(enemies))   # [1, 2]
print(p.clip(e))                   # <rect(40, 40, 10, 10)>

Пока врагов десяток, списка и collidelist хватает. Когда объекты начинают появляться и исчезать пачками, список превращается в ручную бухгалтерию: удалить пулю, удалить врага, не сломать цикл, который по этому же списку идёт. Ровно для этого в PyGame есть спрайты и группы.

Спрайты и группы: класс Sprite и Group

Спрайт в PyGame — это класс, наследник pygame.sprite.Sprite, у которого есть два обязательных атрибута: image и rect. Первый — что рисовать, второй — где. Группа pygame.sprite.Group хранит такие объекты и умеет два действия: update() вызывает одноимённый метод у каждого спрайта, draw(screen) делает blit каждого image в его rect.

class Enemy(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.Surface((30, 30))
        self.image.fill("red")
        self.rect = self.image.get_rect(topleft=(x, y))

    def update(self):
        self.rect.y += 3
        if self.rect.top > 480:
            self.kill()

enemies = pygame.sprite.Group(Enemy(300, 350), Enemy(100, 100))

# в игровом цикле:
enemies.update()
enemies.draw(screen)

Три детали, без которых это не заработает. super().__init__() обязателен: без него объект нельзя положить в группу, PyGame упадёт с загадочной AttributeError: '_Sprite__g'. Метод называется именно update, и группа вызовет его с теми аргументами, которые вы передали в Group.update(). И self.kill() удаляет спрайт из всех групп сразу: враг, который улетел за нижний край, перестанет рисоваться и обновляться, а сам объект останется жив, если на него есть другие ссылки.

На практике заводят две группы: all_sprites для отрисовки всего и отдельные (enemies, bullets) для логики столкновений. Один спрайт спокойно живёт в нескольких группах. Так что персонаж из all_sprites и враги из enemies рисуются одной строкой, а сталкиваются только те, кому положено. Если слова «наследник» и super() звучат непривычно, классы стоит подтянуть раньше спрайтов: почти любые курсы по Python проходят их до графики.

Анимация спрайта: смена кадров по таймеру

Раз image — обычный атрибут, анимация сводится к его подмене. Загрузите кадры в список, а в update переключайте индекс по таймеру, раз в N миллисекунд. Если менять его каждый кадр, при 60 FPS персонаж «замигает».

class Hero(pygame.sprite.Sprite):
    def __init__(self, frames):
        super().__init__()
        self.frames = frames          # список Surface
        self.frame = 0
        self.image = frames[0]
        self.rect = self.image.get_rect()
        self.last = pygame.time.get_ticks()
        self.delay = 120              # мс на кадр

    def update(self):
        now = pygame.time.get_ticks()
        if now - self.last >= self.delay:
            self.last = now
            self.frame = (self.frame + 1) % len(self.frames)
            self.image = self.frames[self.frame]

В моём прогоне при задержке 120 мс и вызовах каждые 60 мс индексы шли как [0, 1, 1, 2, 2, 3], то есть кадр сменялся через раз. Сами кадры для 2D-игры обычно рисуют в пиксельных редакторах; если хотите делать их сами, посмотрите курсы по пиксель-арту.

Столкновения спрайтов: spritecollide, groupcollide и маски

С группами проверка столкновений становится короче, чем с голыми Rect. Три функции модуля pygame.sprite закрывают почти все случаи.

hits = pygame.sprite.spritecollide(player, enemies, True)   # игрок против группы
score += len(hits)

pairs = pygame.sprite.groupcollide(bullets, enemies, True, True)  # группа против группы

if pygame.sprite.spritecollideany(player, enemies):         # только да или нет
    game_over = True

Третий аргумент, dokill, решает судьбу столкнувшихся: при True спрайты из группы удаляются сами, как будто вы вызвали kill() на каждом. У groupcollide таких флага два, по одному на каждую группу, а возвращает она словарь «спрайт первой группы → список спрайтов второй, которых он задел». spritecollideany отдаёт первый попавшийся спрайт или None и по документации работает быстрее, когда список столкнувшихся вам не нужен.

В моём тесте игрок 40 × 40 в точке (320, 400) и три врага после одного update(): spritecollide вернул двоих, в группе врагов остался один, и в all_sprites убитые тоже пропали. Это важно: dokill=True удаляет спрайт из всех групп, не только из той, что вы передали.

Функция Кого с кем dokill Возвращает
spritecollide(sprite, group, dokill) Один против группы 1 флаг Список спрайтов группы
groupcollide(g1, g2, dokill1, dokill2) Группа против группы 2 флага Словарь спрайт → список
spritecollideany(sprite, group) Один против группы нет Спрайт или None

У всех трёх есть необязательный аргумент collided: функция, которая решает, что считать столкновением. По умолчанию это collide_rect, то есть тот же colliderect. Два других варианта пригодятся, когда прямоугольники врут.

  • pygame.sprite.collide_rect_ratio(0.5) сравнивает уменьшенные копии Rect. Хитбокс становится мягче: враг задевает игрока, только когда зашёл глубоко, касание краем не считается.
  • pygame.sprite.collide_mask сравнивает маски, то есть непрозрачные пиксели. Круглая пуля и круглый астероид по Rect столкнутся углами, по маске нет.
class Ship(pygame.sprite.Sprite):
    def __init__(self, pos):
        super().__init__()
        self.image = pygame.Surface((40, 40), pygame.SRCALPHA)
        pygame.draw.circle(self.image, "white", (20, 20), 20)
        self.rect = self.image.get_rect(topleft=pos)
        self.mask = pygame.mask.from_surface(self.image)

s1, s2 = Ship((0, 0)), Ship((34, 34))
print(pygame.sprite.collide_rect(s1, s2))   # True  — углы Rect перекрылись
print(pygame.sprite.collide_mask(s1, s2))   # None  — круги не касаются

hits = pygame.sprite.spritecollide(s1, group, False, pygame.sprite.collide_mask)

Маску можно не создавать руками: если атрибута mask нет, collide_mask построит её из image на лету. Но это дорого, и в справке по модулю pygame.sprite отдельно сказано: при смене картинки маску нужно пересоздать. Для анимированного спрайта это значит обновлять self.mask в том же update, где меняется кадр. В Godot или Unity за это отвечают готовые коллайдеры, и если PyGame для вас первая остановка, стоит заранее посмотреть, какие движки и языки есть дальше.

Собираем всё вместе: игра «Поймай звезду» на 70 строк

Рой с азартом ловит падающие звёзды в собственной игре

Ниже полный файл, который использует каждый приём из статьи: get_rect(midbottom=…) для старта, clamp_ip для границ, kill() для улетевших звёзд, spritecollide с dokill для счёта. Звёзды появляются по таймеру pygame.time.set_timer, чтобы не привязываться к номеру кадра.

import random
import pygame

WIDTH, HEIGHT = 640, 480


class Player(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.Surface((50, 20))
        self.image.fill("dodgerblue")
        self.rect = self.image.get_rect(midbottom=(WIDTH // 2, HEIGHT - 20))
        self.speed = 6

    def update(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            self.rect.x -= self.speed
        if keys[pygame.K_RIGHT]:
            self.rect.x += self.speed
        self.rect.clamp_ip(screen.get_rect())


class Star(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.Surface((16, 16))
        self.image.fill("gold")
        self.rect = self.image.get_rect(
            midtop=(random.randint(8, WIDTH - 8), -16)
        )
        self.speed = random.randint(3, 6)

    def update(self):
        self.rect.y += self.speed
        if self.rect.top > HEIGHT:
            self.kill()


pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
font = pygame.font.Font(None, 36)

player = Player()
stars = pygame.sprite.Group()
all_sprites = pygame.sprite.Group(player)
score = 0

SPAWN = pygame.USEREVENT + 1
pygame.time.set_timer(SPAWN, 700)

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == SPAWN:
            star = Star()
            stars.add(star)
            all_sprites.add(star)

    all_sprites.update()
    score += len(pygame.sprite.spritecollide(player, stars, True))

    screen.fill("black")
    all_sprites.draw(screen)
    screen.blit(font.render(f"Счёт: {score}", True, "white"), (10, 10))
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

Сохраните как stars.py и запустите; если не помните, как это делается из редактора, есть инструкция про запуск файла Python в VS Code. Я прогнал файл на 400 кадров с зажатой стрелкой вправо: игрок упёрся в правый край на x = 590 (640 минус ширина 50), счёт рос ровно на число пойманных звёзд. Дальше игру легко расширить: замените Surface на картинки через pygame.image.load, добавьте группу пуль и groupcollide, повесьте на промахи spritecollideany с полом.

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

Ваня БуявецКанал основателя Checkroi Вани БуявцаЗабирайте промпты и обучение по нейросетям в моём Телеграм-каналеБольше 3 700 человек уже применяют Claude Code, ChatGPT и другие нейросети в работе, учёбе, бизнесе и жизниПерейти в канал

Частые ошибки с Rect и спрайтами

Каждую строку таблицы я воспроизвёл на pygame 2.6.1, текст ошибки скопирован из терминала.

Симптом Причина Что сделать
Объект не двигается при rect.x += 0.3 Rect хранит только целые: 0 + 0.3 усекается до 0 каждый кадр Храните позицию в отдельной переменной float (или pygame.Vector2) и записывайте в rect.x = round(pos_x)
TypeError: rect argument is invalid в draw.rect Передан кортеж из трёх чисел или не Rect Четыре числа: (x, y, w, h), либо готовый Rect
TypeError: Invalid rect, all 4 fields must be numeric в colliderect В метод передана точка (x, y), а не прямоугольник Для точки нужен collidepoint
AttributeError: 'Enemy' object has no attribute 'rect' В __init__ спрайта нет self.rect (или он назван иначе) Ровно self.image и self.rect, других имён группа не знает
AttributeError: 'Enemy' object has no attribute '_Sprite__g' Забыт super().__init__() Первой строкой конструктора вызвать родительский __init__
Столкновение никогда не срабатывает Картинка рисуется по своим x, y, а Rect стоит на месте; либо у Rect нулевой размер Двигать только rect, рисовать через screen.blit(image, rect); проверить rect.size
ValueError: invalid color name Опечатка в строковом имени цвета Проверить имя или задать RGB-кортеж

Первая ошибка самая коварная, потому что программа не падает. Скорость 0.3 пикселя в кадр выглядит разумно для плавного движения, а объект стоит как вкопанный. В моём прогоне десять прибавлений по 0.3 дали rect.x == 0, а через отдельную переменную — 3. Проверяйте это первым делом, когда «медленный» объект не едет.

Где освоить Python и разработку игр системно

Rect и спрайты закрывают механику 2D-игры, но дальше упираешься в то, что вокруг: классы и наследование, структура проекта, отладка. Это уже вопросы к самому языку. Если хотите пройти его по порядку, а не кусками, сравните курсы по Python: в каталоге есть и короткие интенсивы, и программы с проектами. Тем, кто целится именно в геймдев, подойдут курсы по разработке игр, а чем занимается разработчик игр в студии и сколько получает, разобрано в отдельном обзоре профессии.

КурсШколаСтоимость со скидкойВ рассрочкуДлитель­ностьОбзор курса от Checkroi
Профессия «Python-разработчик»
Перейти на сайт курса
SkillboxSkillbox157 335 ₽5987 ₽/мес.10 месяцевОбзор курса
Fullstack-разработчик на Python
Перейти на сайт курса
НетологияНетология175 800 ₽7125 ₽/мес.21 месяцОбзор курса
ДО Профессия Python-разработчик
Перейти на сайт курса
GeekBrainsGeekBrains149 001 ₽3167 ₽/мес.10 месяцевОбзор курса
Автоматизированное тестирование на Python
Перейти на сайт курса
SkillboxSkillbox118 666 ₽4108 ₽/мес.9 месяцевОбзор курса
Программирование на Python ПРО
Перейти на сайт курса
SkillboxSkillbox119 600 ₽250 000 ₽/мес.17 месяцевОбзор курса

Больше программ — в полном каталоге курсов по Python

Часто задаваемые вопросы

Как работает colliderect в PyGame?

a.colliderect(b) проверяет, перекрываются ли два прямоугольника хотя бы на один пиксель, и возвращает True или False. Общая граница пересечением не считается: прямоугольники, стоящие вплотную, по этому методу не сталкиваются. Для точки (например, курсора мыши) нужен collidepoint, для списка прямоугольников — collidelist.

Чем Rect отличается от спрайта?

Rect — это только координаты и размер, четыре целых числа без картинки. Спрайт — класс-наследник pygame.sprite.Sprite, у которого есть и rect (где рисовать), и image (что рисовать). Группы спрайтов умеют обновлять, рисовать и сталкивать их одной строкой, а с голыми Rect всё это пишется руками.

Почему столкновение не срабатывает, хотя объекты явно накладываются?

Чаще всего картинка рисуется по своим переменным x и y, а Rect объекта остаётся на старом месте. Двигайте именно rect и рисуйте через screen.blit(image, rect). Вторая причина — прямоугольник нулевого размера: pygame.Rect(0, 0, 0, 0) не сталкивается ни с чем, пока ему не задали size.

Как ограничить движение Rect границами экрана?

Одной строкой: rect.clamp_ip(screen.get_rect()) в конце обработки движения. Метод вталкивает прямоугольник внутрь окна, если он вылез за любую сторону. Вариант без _ip вернёт новую копию, а исходный Rect не изменит.

Что делает dokill в spritecollide?

Третий аргумент spritecollide(sprite, group, dokill). При True все спрайты группы, которые задели первый аргумент, удаляются из всех своих групп, как после вызова kill(). Так за одну строку и считают пойманные предметы, и убирают их с экрана. У groupcollide таких флага два, по одному на каждую группу.

Почему объект не двигается при rect.x += 0.5?

Координаты Rect всегда целые, дробная часть отбрасывается при каждом присваивании: 0 + 0.5 снова становится 0. Храните позицию в отдельной переменной типа float или в pygame.Vector2, а в Rect записывайте округлённое значение: rect.x = round(pos_x).

Читайте Checkroi первым в Google
Добавьте Checkroi в избранные источники — и наши разборы курсов и обзоры школ будут показываться выше в вашей выдаче Google.
Оставить комментарий
0 комментариев
Форма комментария

Оставьте комментарий

Напишите, что думаете. Нам важно ваше мнение!