Класс Rect

Еще одни ключевым классом в Pygame является Rect. Его экземпляры представляют собой прямоугольные области. Они не имеют графического представления в окне игры. Ценность класса заключается в свойствах и методах, позволяющих управлять размещением поверхностей, выполнять проверку их перекрытия и др.

Rect'ы можно передавать в функцию pygame.display.update(). В этом случае будут обновляться только соответствующие им области.

Экземпляры Rect создаются не только напрямую от класса. Однако начнем с этого варианта.

import pygame
pygame.init()
 
sc = pygame.display.set_mode((400, 400))
 
rect1 = pygame.Rect((0, 0, 30, 30))
rect2 = pygame.Rect((30, 30, 30, 30))
 
print(rect1.bottomright)  # вывод (30, 30)
print(rect2.bottomright)  # (60, 60)
 
rect2.move_ip(10, 10)
print(rect2.topleft)  # (40, 40)
 
rect1.union_ip(rect2)
print(rect1.width)  # 70
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()

В конструктор класса Rect передаются четыре числа – координаты x и y, ширина и высота. Мы создаем два квадрата со сторонами в 30 пикселей. Верхний левый угол первого находится в точке (0, 0), второго – (30, 30).

У объектов Rect есть более десятка свойств, связанных с их координатами и размерами. Свойство bottomright одно из них, в нем хранится координата нижнего правого угла. Понятно, что если второй квадрат начинается в точке (30, 30) и его сторона равна 30, то нижний правый угол будет в точке (60, 60).

Кроме свойств, у объектов Rect есть множество методов. Метод move_ip() смещает прямоугольную область по оси x (первый аргумент) и y (второй аргумент) на указанное количество пикселей. В данном случае если второй прямоугольник смещается на 10 пикселей по обоим осям, то его левый верхний угол окажется в точке (40, 40).

Метод union_ip() присоединяет к тому прямоугольнику, к которому применяется, другой – который передается аргументом. Когда мы отодвинули второй прямоугольник на 10 пикселей, то область, заключающая в себе оба, уже будет шириной 70 пикселей, а не 60.

Методы, у которых есть суффикс _ip, изменяют тот экземпляр Rect, к которому применяются. Есть аналогичные методы без _ip (например, move(), union()), которые возвращают новый экземпляр, т. е. старый остается без изменений.

В метод blit() можно передавать не координаты места размещения Surface, а экземпляр Rect. Метод blit() сам возьмет из Rect координаты его верхнего левого угла:

import pygame
pygame.init()
 
sc = pygame.display.set_mode((300, 300))
sc.fill((200, 255, 200))
 
surf1 = pygame.Surface((200, 200))
surf1.fill((220, 200, 0))  # желтая
surf2 = pygame.Surface((100, 100))
surf2.fill((255, 255, 255))  # белая
 
rect = pygame.Rect((70, 20, 0, 0))
 
surf1.blit(surf2, rect)
sc.blit(surf1, rect)
 
pygame.display.update()
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()

Использование Rect для отрисовки Surface

Мы размещаем желтую поверхность на зеленой, а белую – на желтой. В обоих случаях – в координатах (70, 20). Однако в каждом случае точка берется относительно своей родительской поверхности.

Еще один момент, на который надо обратить внимание. Прямоугольная область была определена нулевой размерностью. При этом поверхности отобразились соответственно своим собственным размерам. Это значит, что поверхности не располагаются внутри rect'ов. Они к ним никакого отношения не имеют. Из прямоугольников blit() взял только координаты.

С другой стороны, экземпляры Rect предназначены для хранения не только координат, но и размеров поверхностей. Размеры в основном нужны для проверки коллизий. В большинстве случаев сначала создается поверхность. Далее с нее снимается "маска", т. е. создается экземпляр Rect, который будет иметь те же размеры, что и она. Все дальнейшее "взаимодействие" поверхности с другими объектами (размещение, проверка столкновений и вхождений) происходит через "связанный" с ней Rect.

import pygame
pygame.init()
 
sc = pygame.display.set_mode((300, 300))
sc.fill((200, 255, 200))
 
surf2 = pygame.Surface((100, 100))
surf2.fill((255, 255, 255))  # белая
 
rect = surf2.get_rect()  # создается Rect
 
print(surf2.get_width())  # вывод 100
print(rect.width)  # 100
print(rect.x, rect.y)  # 0 0
 
sc.blit(surf2, rect)
pygame.display.update()
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
 
    rect.x += 1
 
    sc.fill((200, 255, 200))
    sc.blit(surf2, rect)
    pygame.display.update()
 
    pygame.time.delay(20)

Метод поверхности get_rect() возвращает экземпляр Rect, ширина и высота которого совпадают с таковыми поверхности. В примере метод get_width() возвращает ширину поверхности, также выводится ширина прямоугольника (rect.width), чтобы показать, что они равны.

Если в get_rect() не передавать аргументы, то верхний левый угол экземпляра Rect будет в точке (0, 0).

В цикле мы изменяем координату x прямоугольной области, после чего передаем уже измененный rect в метод blit(). В результате поверхность будет двигаться.

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

В get_rect() можно передавать именованные аргументы, являющиеся свойствами Rect, и устанавливать им значения. Например, surf.get_rect(topleft=(100, 50)) вернет прямоугольник, чей левый угол будет в точке (100, 50), а размер совпадать с размерами surf. Выражение surf.get_rect(centerx=100) вернет прямоугольник, координата x центра которого будет иметь значение 100. При этом остальные координаты будут вычислены, исходя из размеров поверхности.

Перепишем программу с двумя ракетами из предыдущего урока, используя экземпляры Rect.

import pygame
pygame.init()
 
WIN_WIDTH = 800
WIN_HEIGHT = 600
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
 
class Rocket:
    width_rocket = 20
    height_rocket = 50
 
    def __init__(self, surface, color):
        self.surf = surface
        self.color = color
        self.x = surface.get_width()//2 - Rocket.width_rocket//2
        self.y = surface.get_height()
 
    def fly(self):
        pygame.draw.rect(self.surf, self.color, (self.x, self.y,
                        Rocket.width_rocket, Rocket.height_rocket))
        self.y -= 3
        if self.y < -Rocket.height_rocket:
            self.y = WIN_HEIGHT
 
sc = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT))
 
rect_left = pygame.Rect((0, 0), (WIN_WIDTH//2, WIN_HEIGHT))
rect_right = pygame.Rect((WIN_WIDTH//2, 0), (WIN_WIDTH//2, WIN_HEIGHT))
 
surf_left = pygame.Surface((rect_left.width, rect_left.height))
surf_left.fill(WHITE)
 
surf_right = pygame.Surface((rect_right.width, rect_right.height))
 
sc.blit(surf_left, rect_left)
 
sc.blit(surf_right, rect_right)
 
rocket_left = Rocket(surf_left, BLACK)
rocket_right = Rocket(surf_right, WHITE)
 
pygame.display.update()
 
active_left = False
active_right = False
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
        elif i.type == pygame.MOUSEBUTTONUP:
            if rect_left.collidepoint(i.pos):
                active_left = True
                active_right = False
            elif rect_right.collidepoint(i.pos):
                active_right = True
                active_left = False
 
    if active_left:
        surf_left.fill(WHITE)
        rocket_left.fly()
        sc.blit(surf_left, rect_left)
        pygame.display.update(rect_left)
    elif active_right:
        surf_right.fill(BLACK)
        rocket_right.fly()
        sc.blit(surf_right, rect_right)
        pygame.display.update(rect_right)
 
    pygame.time.delay(20)

Главное, на что здесь следует обратить внимание, – вызов функции pygame.display.update() с аргументом-прямоугольником. Таким образом, на каждой итерации главного цикла while в памяти компьютера "перерисовывается" только часть окна, что экономит его ресурсы.

rect_left = pygame.Rect((0, 0), (WIN_WIDTH//2, WIN_HEIGHT))
rect_right = pygame.Rect((WIN_WIDTH//2, 0), (WIN_WIDTH//2, WIN_HEIGHT))

Создаются два экземпляра Rect. Левый начинается в верхнем левом углу окна, правый – от цента по оси x, вверху по оси y.

surf_left = pygame.Surface((rect_left.width, rect_left.height))
surf_left.fill(WHITE)
surf_right = pygame.Surface((rect_right.width, rect_right.height))

При создании экземпляров Surface мы указываем такую же ширину и высоту как у соответствующих им прямоугольников.

sc.blit(surf_left, rect_left)
sc.blit(surf_right, rect_right)

Левая и правая поверхности прорисовываются на главном окне. Координаты берутся из соответствующих экземпляров Rect.

rocket_left = Rocket(surf_left, BLACK)
rocket_right = Rocket(surf_right, WHITE)

Создаются два экземпляра нашего самописного класса Rocket. Конструктору надо передать поверхность и цвет.

active_left = False
active_right = False

Переменные определяют, какую анимацию проигрывать. Пока не будет произведено кликов, то никакую.

elif i.type == pygame.MOUSEBUTTONUP:
    if rect_left.collidepoint(i.pos):
        active_left = True
        active_right = False
    elif rect_right.collidepoint(i.pos):
        active_right = True
        active_left = False

Метод collidepoint() объекта Rect проверяет, находится ли точка, координаты которой были переданы в качестве аргумента, в пределах прямоугольника, к которому применяется метод. Здесь точкой являются координаты клика мыши. Если клик происходит в левом прямоугольнике, то в True устанавливается одна переменная, если в правом – то другая.

if active_left:
    surf_left.fill(WHITE)
    rocket_left.fly()
    sc.blit(surf_left, rect_left)
    pygame.display.update(rect_left)
elif active_right:
    surf_right.fill(BLACK)
    rocket_right.fly()
    sc.blit(surf_right, rect_right)
    pygame.display.update(rect_right)

В зависимости от того, какая переменная в статусе True, изменения происходят только на одной из двух дочерних поверхностей. Также как update() метод blit() вызывается на каждой итерации, иначе изменения не будут видны.

Практическая работа

Напишите программу по следующему описанию. В центре окна находится круг, изменяющий свой цвет на каждой итерации цикла. Окно условно поделено на четверти: верхнюю левую, верхнюю правую, нижнюю левую, нижнюю правую. Если нажимается клавиша 1, то обновляются только две четверти по диагонали. Если 2 – то только две другие. Нажатие нуля возобновляет обновление всей поверхность.

Примечание. Функция pygame.display.update() может принимать не только один экземпляр Rect, но и список таковых.

Пример обновления частей окна

Курс с примерами решений практических работ: android-приложение, pdf-версия.

Создано

Обновлено