Корабль у нас уже летает плавно и не вылетает за экран. Но для программы он пока всего лишь две цифры, ship_x и ship_y: точка без размера. Она не знает, сколько места занимает корабль, где у него правый бок и во что он может врезаться. В этом уроке мы дадим объектам «коробки» — прямоугольники Rect, на которых держится почти всё остальное в Pygame.
Зачем объекту рамка
Представьте посылку на почте. Адрес говорит, куда её нести, но, чтобы найти место на полке, нужно знать и размер коробки. В играх так же: координаты отвечают на вопрос «где?», а размер — на вопрос «сколько места занимает?». Без размера не получится ни поймать столкновение, ни поставить картинку ровно по центру, ни понять, дошёл ли объект до края окна.
Самая простая форма «коробки» — прямоугольник со сторонами, параллельными краям окна: его не нужно поворачивать, а проверять и двигать очень легко. Для этого в Pygame есть готовый тип данных — Rect (от rectangle, прямоугольник). Объект pygame.Rect хранит положение и размер одной такой коробки. Кораблю, метеору, кристаллу, лазеру и даже кнопке меню в нашей игре достанется по своему Rect.
Окно для него ничего особенного не значит: Rect живёт и без окна, это просто четыре числа с удобными свойствами. Поэтому большинство примеров ниже можно запускать прямо в обычном Python с установленным pygame-ce, без окна.
Создаём Rect: четыре числа и много свойств
Прямоугольник задаётся четырьмя числами: где его левый верхний угол (x, y) и сколько он занимает в ширину и в высоту (w, h). Напомним, что x растёт вправо, а y вниз, и точка (0, 0) — левый верхний угол окна.
import pygame
box = pygame.Rect(100, 50, 48, 40)
print(box.left, box.top)
print(box.right, box.bottom)
print(box.center)
print(box.size)
Разбор по строкам
pygame.Rect(100, 50, 48, 40)— создаём прямоугольник: левый верхний угол в точке (100, 50), ширина 48, высота 40.box.left, box.top— координаты левой и верхней сторон: это те самые 100 и 50, которые мы передали.box.right, box.bottom— правая и нижняя стороны; Rect считает их сам:left + ширинаиtop + высота.box.center— пара (x, y) центра прямоугольника.box.size— пара (ширина, высота).
100 50
148 90
(124, 70)
(48, 40)
Ширина 48 от левой стороны 100 даёт правую сторону 148, высота 40 от верхней 50 даёт нижнюю 90. Центр лежит посередине: 100 + 24 и 50 + 20. Все эти значения — атрибуты (свойства) прямоугольника, и самые нужные собраны в таблице.
| Атрибут | Что это |
|---|---|
x, y (они же left, top) | координаты левого верхнего угла |
w, h (они же width, height) | ширина и высота |
right, bottom | правая и нижняя стороны: left + w и top + h |
centerx, centery | координаты центра по отдельности |
center, size | центр и размер как пары чисел |
Правая сторона не входит в прямоугольник
У прямоугольника с left = 100 и шириной 48 крайний левый пиксель имеет x = 100, а крайний правый — x = 147. Значение right = 148 — это уже первый пиксель за коробкой. Так считает и Pygame, когда проверяет столкновения: если два прямоугольника лишь касаются сторонами, это не пересечение.
Ставим Rect за нужную точку
Когда вешаете картину, можно сказать «левый верхний угол должен быть вот здесь», а можно «середина картины должна быть на уровне глаз». Итог один: рамка висит там, где нужно, а остальные края находятся сами. Для кораблей и метеоров второй способ удобнее: мы думаем о нём как о точке-центре, а края пусть считает Rect.
У Rect для этого есть точки привязки — атрибуты, которым можно присвоить пару координат. Присвоили новую точку — Rect сам сдвинулся так, чтобы эта точка оказалась на месте, а размер остался прежним.
| Точка | Где находится |
|---|---|
topleft, topright | верхние углы |
bottomleft, bottomright | нижние углы |
midtop, midbottom | середины верхней и нижней сторон |
midleft, midright | середины левой и правой сторон |
center | центр |
box = pygame.Rect(0, 0, 48, 40)
box.center = (480, 320)
print(box.topleft)
box.midbottom = (480, 600)
print(box.topleft)
print(box.size)
Разбор по строкам
pygame.Rect(0, 0, 48, 40)— положение пока не важно, главное — размер 48 на 40; поэтому и нули в начале.box.center = (480, 320)— ставим центр коробки в середину окна; левый верх пересчитывается как (480 − 24, 320 − 20).print(box.topleft)— покажет, куда в итоге переехал левый верхний угол.box.midbottom = (480, 600)— теперь за точку взята середина нижней стороны: коробка «стоит» на линии y = 600.print(box.size)— размер при всех этих перестановках не менялся.
(456, 300)
(456, 560)
(48, 40)
Метеору, который катится по земле, или персонажу платформера удобнее midbottom: тогда координата говорит, где объект стоит ногами. Нашему кораблю подходит center, потому что его ship_x и ship_y уже и есть центр.
x и y — это не центр
Самая частая путаница: box.x и box.y всегда означают левый верхний угол. Если нужно поставить объект по центру, пишите box.center = (...), а не box.x = .... А ещё менять ширину box.w без последствий не выйдет: левый верхний угол остаётся на месте, и центр уезжает вправо.
Rect хранит только целые числа
Пиксель на экране нельзя разрезать, и Rect с этим согласен: все его числа — целые. Если записать в него дробное значение, дробная часть теряется. Представьте шахматную доску: фигура стоит на клетке, а «на четверти клетки» она стоять не умеет.
Посмотрим, чем это грозит движению. Пусть корабль медленно дрейфует со скоростью 20 пикселей в секунду. За один кадр при 60 FPS это 20 × 0,0167 ≈ 0,33 пикселя. Если делать rect.x += 0.33, то получится 100,33, Rect отбросит дробь, и коробка так и останется на 100. Каждый кадр одно и то же: объект стоит, хотя по расчёту должен ехать.
Решение простое, и оно уже есть в нашем проекте: настоящую позицию храним отдельно, в обычных числах с дробью (у нас это ship_x и ship_y), а в Rect каждый кадр переносим округлённое значение. Функция round(число) округляет до ближайшего целого. Движение считает формула в logic.py, а Rect только отвечает на вопрос «какую область объект занимает на экране сейчас?».
Логика отдельно, рамка отдельно
Не двигайте корабль прямо через rect.x: тогда плавное движение пропадёт. Правило такое: позиция живёт в числах, Rect — её пиксельная копия. Дальше в курсе у этой пары чисел появится более удобное хранилище, но правило останется тем же.
Двигаем и проверяем
С Rect-ом можно делать четыре полезные вещи. Начнём с движения. Метод move_ip(dx, dy) сдвигает прямоугольник на dx вправо и dy вниз. Окончание ip — от in place, «на месте»: меняется сам объект. Есть и близнец move(dx, dy), он ничего не меняет, а возвращает новый сдвинутый прямоугольник.
Второе: clamp_ip(другой_rect) загоняет прямоугольник внутрь другого, как наш clamp загонял число в диапазон. Для окна подойдёт screen.get_rect() — он возвращает прямоугольник размером с окно, то есть Rect(0, 0, 960, 640).
screen_rect = pygame.Rect(0, 0, 960, 640)
box = pygame.Rect(900, 300, 48, 40)
box.move_ip(30, 0)
print(box.topleft)
box.clamp_ip(screen_rect)
print(box.topleft)
Разбор по строкам
screen_rect = pygame.Rect(0, 0, 960, 640)— прямоугольник всего окна; в игре его даётscreen.get_rect().box = pygame.Rect(900, 300, 48, 40)— коробка у самого правого края.box.move_ip(30, 0)— сдвигаем на 30 вправо; теперь правая сторона на 978, то есть торчит за окном.box.clamp_ip(screen_rect)— возвращаем внутрь окна: коробка упирается правой стороной в границу.
(930, 300)
(912, 300)
Третье: collidepoint((x, y)) отвечает True или False на вопрос «лежит ли эта точка внутри прямоугольника?». Так позже будем проверять, куда нажала мышь. Помните про правую сторону, которая в коробку не входит:
box = pygame.Rect(100, 50, 48, 40)
print(box.collidepoint((130, 60)))
print(box.collidepoint((148, 60)))
True
False
Четвёртое: Rect можно нарисовать. Функция pygame.draw.rect(поверхность, цвет, rect, толщина) рисует его контур; если толщину не указать, прямоугольник закрасится целиком. Именно такая рамка поможет нам увидеть «коробку» поверх корабля:
pygame.draw.rect(screen, (80, 255, 120), box, 2)
Здесь screen — поверхность, на которой рисуем, (80, 255, 120) — зелёный цвет, box — что рисуем, а 2 — толщина линии в пикселях. Такую рамку программисты называют отладочной: она нужна не игроку, а нам, чтобы видеть невидимое.
Практика: рамка вокруг корабля
Дадим кораблю коробку и научимся показывать её по клавише F1. Весь урок мы будем править только main.py; logic.py остаётся как есть. Ниже показаны только новые строки и места, куда их вставить: остальной ваш файл не трогайте.
Шаг 1. Цвет рамки
Рядом с другими цветами (WHITE, SHIP_COLOR) добавьте ещё один:
BOX_COLOR = (80, 255, 120)
Шаг 2. Коробка и переключатель
До цикла, сразу после строк, где создаются ship_x и ship_y, добавьте:
ship_rect = pygame.Rect(0, 0, 48, 40)
show_boxes = False
Разбор по строкам
pygame.Rect(0, 0, 48, 40)— коробка 48 на 40; ширина равна двумSHIP_HALF, то есть ровно тому, сколько места мы оставляли у стенок. Положение не важно: его поставим каждый кадр.show_boxes = False— переключатель «показывать ли рамки»; в начале выключен.
Шаг 3. Клавиша F1
В цикле событий сейчас стоит elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:. Чтобы рядом с Esc поместилась ещё одна клавиша, разделим проверку на два вложенных условия: сначала «это нажатие клавиши?», потом «какой именно?». Замените весь цикл событий на этот:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_F1:
show_boxes = not show_boxes
Выражение not show_boxes меняет True на False и обратно, поэтому каждое нажатие F1 включает или выключает рамки. Событие KEYDOWN приходит один раз за нажатие, так что рамка не будет мигать, пока вы держите клавишу.
Шаг 4. Коробка следует за кораблём
Найдите строку, где корабль получает новые координаты: ship_x, ship_y = step_ship(...) — это весь шаг корабля, зажим в границах делается внутри функции. Сразу после неё добавьте одну строку:
ship_rect.center = (round(ship_x), round(ship_y))
Центр рамки всегда равен центру корабля, округлённому до целых пикселей. Размер рамки при этом не меняется.
Шаг 5. Рисуем рамку
Сразу после вызова draw_ship(...) (но до pygame.display.flip()) добавьте:
if show_boxes:
pygame.draw.rect(screen, BOX_COLOR, ship_rect, 2)
Рисуем после корабля, чтобы рамка легла поверх корпуса и пламени. Когда show_boxes равно False, условие не выполняется, и игра выглядит как раньше.
Запуск и что должно получиться
python main.py
Сначала всё как обычно: корабль, звёзды, плавный полёт. Нажмите F1: вокруг корабля появится тонкая зелёная рамка 48 на 40, которая летит вместе с ним. Ещё раз F1 — рамка исчезла. Эта клавиша останется в игре до самого конца курса: коробки пригодятся при отладке столкновений. А вот сам ship_rect временный: в уроке 3.3 корабль станет спрайтом и получит собственный rect внутри себя, и отдельная коробка в main.py будет не нужна, а F1 продолжит показывать рамки.
Проверьте себя
- Рамка включается и выключается по F1; если держать клавишу зажатой, рамка не мигает.
- Рамка обнимает корабль симметрично: нос, крылья и пламя лежат внутри или у самых её сторон.
- Прижмите корабль к правой стенке: правая сторона рамки совпадает с краем окна. Расчёт: центр стоит на x = 936 (то есть
WIDTH - SHIP_HALF), а полуширина рамки — 24, поэтомуship_rect.rightравно 960. - В стартовой позиции (
ship_x = 480,ship_y = 560) левый верхний угол рамки — (456, 540). Чтобы убедиться, временно добавьте в цикл после строки сcenterвызовprint(ship_rect.topleft), посмотрите первую строку вывода и сразу удалите вызов. - Самый низкий центр корабля
SHIP_MAX_Yравен 600, значит, нижняя сторона рамки доходит до 620, то есть не касается нижнего края окна.
Частые ошибки
Рамка застыла в углу окна
Что видно: зелёный прямоугольник стоит слева сверху (или посередине) и не двигается. Почему: строку ship_rect.center = ... поставили до цикла или вовсе забыли: Rect сам за кораблём не следит. Как исправить: внести строку внутрь цикла, после обновления ship_x и ship_y.
Рамка смещена от корабля
Что видно: рамка висит правее и ниже корабля. Почему: координаты центра записали в x и y (ship_rect.x = ship_x), а они — левый верхний угол. Как исправить: присваивать пару в center.
Движение «залипает»
Что видно: медленный объект стоит на месте, хотя скорость не нулевая. Почему: к rect.x прибавляют меньше пикселя, и Rect отбрасывает дробь. Как исправить: хранить позицию в обычных числах, а в Rect каждый кадр писать round(...).
move вместо move_ip
Что видно: написали box.move(5, 0), а прямоугольник не сдвинулся. Почему: move возвращает новый Rect, а старый остаётся прежним. Как исправить: использовать move_ip(5, 0) или записать результат: box = box.move(5, 0).
Поменяли ширину, и центр уехал
Что видно: после box.w = 100 объект съехал вправо. Почему: при смене размера левый верхний угол остаётся на месте. Как исправить: после смены размера снова поставить нужную точку привязки, например box.center = (...).
Что важно запомнить
pygame.Rect(x, y, w, h)хранит положение и размер прямоугольной «коробки» объекта;xиy— левый верхний угол.- Из четырёх чисел Rect сам считает
right,bottom,center,sizeи остальные атрибуты; правая и нижняя стороны в коробку не входят. - Ставить Rect можно за любую точку привязки:
center,midbottom,topleftи другие; размер при этом не меняется. - Rect хранит только целые числа, поэтому настоящую позицию держим отдельно, а в Rect каждый кадр переносим
round(...). move_ipиclamp_ipменяют сам прямоугольник, аmoveвозвращает новый.collidepointпроверяет точку,pygame.draw.rect(..., толщина)рисует контур; F1 в нашей игре показывает рамки.
Дальше — классы: объектов в игре скоро станет много, и каждому понадобятся своя позиция и свой Rect, а класс как раз позволяет собрать всё это в одно целое.