События мыши

В Pygame обрабатываются три типа событий мыши:

  • нажатие кнопки (значение свойства type события соответствует константе pygame.MOUSEBUTTONDOWN),

  • отпускание кнопки (MOUSEBUTTONUP),

  • перемещение мыши (MOUSEMOTION).

Какая именно кнопка была нажата, записывается в другое свойство события – button. Для левой кнопки это число 1, для средней – 2, для правой – 3, для прокручивания вперед – 4, для прокручивания назад – 5. У событий MOUSEMOTION вместо button используется свойство buttons, в которое записывается состояние трех кнопок мыши (кортеж из трех элементов). 

Другим атрибутом мышиных типов событий является свойство pos, в которое записываются координаты происшествия (кортеж из двух чисел).

Таким образом, если вы нажали правую кнопку мыши точно в середине окна размером 200x200, то будет создан объект типа Event с полями event.type = pygame.MOUSEBUTTONDOWN, event.button = 3, event.pos = (100, 100).

У событий MOUSEMOTION есть еще один атрибут – rel. Он показывает относительное смещение по обоим осям. С помощью него, например, можно отслеживать скорость движения мыши.

Код ниже создает фигуры в местах клика мыши. Нажатие средней кнопки очищает поверхность.

Прорисовка фигур в местах клика мышью

import pygame
 
WHITE = (255, 255, 255)
RED = (225, 0, 50)
GREEN = (0, 225, 0)
BLUE = (0, 0, 225)
 
pygame.init()
sc = pygame.display.set_mode((400, 300))
sc.fill(WHITE)
pygame.display.update()
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
        if i.type == pygame.MOUSEBUTTONDOWN:
            if i.button == 1:
                pygame.draw.circle(sc, RED, i.pos, 20)
                pygame.display.update()
            elif i.button == 3:
                pygame.draw.circle(sc, BLUE, i.pos, 20)
                pygame.draw.rect(sc, GREEN, (i.pos[0]-10, i.pos[1]-10, 20, 20))
                pygame.display.update()
            elif i.button == 2:
                sc.fill(WHITE)
                pygame.display.update()
 
    pygame.time.delay(20)

В функции модуля draw вместо координат передается значение поля pos события. В pos хранятся координаты клика. В случае с функцией rect() извлекаются отдельные элементы кортежа pos. Вычитание числа 10 используется для того, чтобы середина квадрата, сторона которого равна 20-ти пикселям, точно соответствовала месту клика. Иначе в месте клика будет находиться верхний левый угол квадрата.

Функцию update() не обязательно вызывать три раза в ветках if-elif-elif. Ее можно вызвать в основном теле главного цикла. Однако в этом случае, когда кликов не происходит, она будет выполнять зря.

Также как в случае с клавиатурой в pygame есть свой модуль для событий мыши. Если нужно отслеживать длительное зажатие ее кнопок, следует воспользоваться функцией get_pressed() модуля pygame.mouse. Здесь же есть функция для считывания позиции курсора – get_pos(). Следующий код рисует синий круг в местах клика левой кнопкой мыши:

...
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
 
    pressed = pygame.mouse.get_pressed()
    pos = pygame.mouse.get_pos()
    if pressed[0]:
        pygame.draw.circle(sc, BLUE, pos, 5)
        pygame.display.update()
 
    pygame.time.delay(20)

Функция mouse.get_pressed() возвращает трехэлементный кортеж. Первый элемент (с индексом 0) соответствует левой кнопке мыши, второй – средней, третий – правой. Если значение элемента равно единице, значит, кнопка нажата. Если нулю, значит – нет. Так выражение pressed[0] есть истина, если под нулевым индексом содержится единица.

Чтобы скрыть курсор (например, в игре, где управление осуществляется исключительно клавиатурой), надо воспользоваться функцией pygame.mouse.set_visible(), передав в качестве аргумента False.

Так можно привязать графический объект к курсору (в данном случае привязывается квадрат):

...
pygame.mouse.set_visible(False)
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
 
    sc.fill(WHITE)
 
    if pygame.mouse.get_focused():
        pos = pygame.mouse.get_pos()
        pygame.draw.rect(sc, BLUE, (pos[0]-10, pos[1]-10, 20, 20))
 
    pygame.display.update()
 
    pygame.time.delay(20)

Функцией get_pos() мы можем считывать позицию курсора, даже если он не виден. Далее в этой позиции рисуем фигуру в каждом кадре.

Функция get_focused() проверяет, находится ли курсор в фокусе окна игры. Если не делать эту проверку, то при выходе курсора за пределы окна, квадрат будет постоянно прорисовываться у края окна, где произошел выход, т. е. не будет исчезать.

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

Напишите код в котором имитируется полет снаряда (пусть его роль сыграет круг) в место клика мышью. Снаряд должен вылетать из нижнего края окна и лететь вверх, т. е. изменяться должна только координата y. Пока летит один, другой не должен появляться. Когда снаряд достигает цели, должен имитировать взрыв, например, в этом месте прорисовываться квадрат.

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

Комментарии

import pygame
pygame.init()
 
WHITE = (255, 255, 255)
RED = (225, 0, 50)
YELLOW = (225, 225, 0)
FPS = 6
SPEED = 5
WIN_WIDTH = 400
WIN_HEIGHT = 300
 
# Радиус и начальная позиция по y круга
r = 15
y = WIN_HEIGHT + (r*2)
 
# Ширина квадрата
a = 30
 
sc = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT))
clock = pygame.time.Clock()
pygame.display.set_caption("MyGame")
sc.fill(WHITE)
pygame.display.update()
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
 
    if i.type == pygame.MOUSEBUTTONDOWN:
        if i.button == 1:
            pos = pygame.mouse.get_pos()
            while y > i.pos[1]:
                sc.fill(WHITE)
                pygame.draw.circle(sc, YELLOW, (i.pos[0], y), r)
                y -= SPEED
                pygame.display.update()
 
                clock.tick(FPS)
 
            sc.fill(WHITE)
            pygame.draw.rect(sc, RED, (i.pos[0]-(a/2), y-(a/2), a, a))
            y = WIN_HEIGHT + (r*2)
            pygame.display.update()

Ответ на от Александр

Решил немного упороться и сделал снаряд вылетающим из рандомной точки.
При значении скорости > 1 не всегда отрисовывается взрыв, т.к. конечные координаты
могут не совпасть и снаряд станет "дрожать" на месте

Вот сам код
import pygame
import random
 
FPS = 60
W = 300  # ширина экрана
H = 300  # высота экрана
WHITE = (255, 255, 255)
BLUE = (0, 70, 225)
BLACK = (0, 0, 0)
RED = (255,0,0)
 
radius = 10
speed = 3
x, y = W // 2, H + radius
y_start = H + radius
x_start = 0
 
fire = False
play = True # Переменная для включения главного цикла
 
pygame.init()
sc = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()
 
while play:
    sc.fill(BLACK)
 
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            play = False
            pygame.quit()
            break
        if i.type == pygame.MOUSEBUTTONDOWN:
            pos = pygame.mouse.get_pos()
            y = pos[1]
            x = pos[0]
            y_start = H + radius
            x_start = random.randint(0, W)
            fire = True
 
    if fire:
        if x_start < x:
            x_start += speed
        elif x_start > x:
            x_start -= speed
    if fire:
        if y_start < y:
            y_start += speed
        if y_start > y:
            y_start -= speed
    if fire:
        pygame.draw.circle(sc, BLUE, (x_start, y_start), radius)
    if x_start == x and y_start == y:
        fire = False
        pygame.draw.rect(sc, RED, (pos[0]-10, pos[1]-10, 20, 20))
    pygame.display.update()
 
    clock.tick(FPS)

Ответ на от Winter23Rus

Убрал ненужные if'ы и пофиксил взрыв кружочка
А еще надо бы добавить проверку на уже летящую пулю
import pygame
import random
 
FPS = 60
W = 300  # ширина экрана
H = 300  # высота экрана
WHITE = (255, 255, 255)
BLUE = (0, 70, 225)
BLACK = (0, 0, 0)
RED = (255,0,0)
 
radius = 10
speed = 3
x, y = 0, 0
y_start = 0
x_start = 0
 
fire = False
play = True # Переменная для включения главного цикла
 
pygame.init()
sc = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()
 
while play:
    sc.fill(BLACK)
 
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            play = False
            pygame.quit()
            break
        if i.type == pygame.MOUSEBUTTONDOWN:
            pos = pygame.mouse.get_pos()
            y = pos[1]
            x = pos[0]
            y_start = H + radius
            x_start = random.randint(0, W)
            fire = True
 
    if fire:
        if x_start < x:
            x_start += speed
        elif x_start > x:
            x_start -= speed
        if y_start > y:
            y_start -= speed
 
        pygame.draw.circle(sc, BLUE, (x_start, y_start), radius)
        if x-1 <= x_start <= x+1 and y-2 <= y_start <= y+2: # добавил здесь радиус для проверки 
            fire = False
            pygame.draw.rect(sc, RED, (x-10, y-10, 20, 20))
            pygame.display.update()
            pygame.time.wait(30) # без этой задержки взрывы бывают не отрисовываются (хз почему)
 
    pygame.display.update()
 
    clock.tick(FPS)

Ответ на от Александр

import pygame
 
 
SCREEN = (600, 400)
FPS = 60
RED = (255, 0, 0)
BLACK = (0, 0, 0)
 
launch = False
 
 
pygame.init()
 
sc = pygame.display.set_mode(SCREEN)
pygame.display.set_caption("MY SUPPERR PUPPERR GAME")
play = True
 
pos = tuple()
 
 
while play:
 
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            play = False
        # определяем координаты клика
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and not launch:
            sc.fill(BLACK)
            pygame.display.update()
            pos = event.pos
            x = pos[0]
            y = SCREEN[1]
            launch = True
    if pos:
        if y >= pos[1]:
            pygame.draw.circle(sc, RED, (x, y), 7)
            pygame.display.update()
            y -= 7
        else:
            launch = False
            pygame.draw.rect(sc, RED, (pos[0] - 20, y - 20, 40, 40))
            pygame.display.update()
 
    pygame.time.Clock().tick(FPS)

Ответ на от Александр

В строке "pygame.draw.rect(sc, RED, (i.pos[0]-(a/2), y-(a/2), a, a))" нужно заменить обычное деление ("/")на целочисленное ("//") - ошибка в конце (при взрыве снаряда) исчезнет.

вместо квадратика добавил "взрыв"
import pygame
 
FPS = 60
W = 700  # ширина экрана
H = 600  # высота экрана
WHITE = (255, 255, 255)
BLUE = (0, 70, 225)
RED = (255, 0, 0)
motion = False
 
pygame.init()
sc = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()
 
# координаты и радиус круга
r = 10
x = -r
y = H + r
speed = 10
target_y = int()
 
while 1:
    sc.fill(WHITE)
 
    pygame.draw.circle(sc, BLUE, (x, y), r)
 
    pygame.display.update()
 
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            exit()
        elif event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:
                if not motion:
                    motion = True
                    print(event.pos)
                    x = event.pos[0]
                    target_y = event.pos[1]
 
    if motion:
        y -= speed
        if y <= target_y:
            sc.fill(WHITE)
            for size in range(1, 30):
                pygame.draw.circle(sc, RED, (x, y), size)
                pygame.display.update()
                clock.tick(FPS * 2)
            motion = False
            x = -r
            y = H + r
 
    print(x, y)
    clock.tick(FPS)
<python>

А можно ли сделать схему с привязанным квадратом как-то иначе, без постоянной отрисовки дисплея? Ведь если мне нужно, чтобы курсор двигался не просто по белому полю, а по каким-то картинкам, расположение которых еще и зависит от внутренних состояний, отрисовывать все заново каждый кадр очень затратно. А если этого не делать, курсор оставляет "след".

import pygame, random
 
pygame.init()
 
r = 15
have = False
 
clock = pygame.time.Clock()
win = pygame.display.set_mode((700, 500))
pygame.mouse.set_visible(True)
 
cub = False
run = True
while run:
    win.fill((255, 255, 255))
 
 
    if have:
        if y <= 15:
            tLive = 7
            cub = True
            have = False
        else:
            y -= 5
            pygame.draw.circle(win, (0, 255, 0), (x, y), r)
 
 
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
        if i.type == pygame.MOUSEBUTTONDOWN:
            if i.button == 1 and not have:
                x = random.randint(20, 680)
                y = 520
                pygame.draw.circle(win, (0, 255, 0), (x, y), r)
                have = True
 
    if cub:
        if tLive == 0:
            cub = False
        else:
            pygame.draw.rect(win, (255, 0, 0), (x-18, y-18, 36, 36))
            tLive -= 1
    pygame.display.update()
 
    clock.tick(60)

import pygame
 
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)
crators = []
 
FPS = 60
SCREEN_WIDTH = 640
SCREEN_HEIGHT = 420
 
pygame.init()
sc = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
 
pygame.display.update()
 
class Bully:
    def __init__(self, string):
        self.string = string
        self.stringLen = len(self.string)
        self.editedString = self.string
        self.tick = 0
        self.sec = 0
 
    def tickT(self):
        self.tick += 1
 
        if self.tick == 30:
            self.edit(self.sec)
 
            self.sec += 1
            self.tick = 0
 
        if self.sec == self.stringLen:
            self.sec = 0
 
    def edit(self, charPos):
        a = self.string[:charPos]
        b = self.string[charPos:].capitalize()
        self.editedString = a+b
 
    def __str__(self):
        return self.editedString
 
class Cross:
    def __init__(self, scene, color):
        self.master = scene
        self.color = color
        self.x = 0
        self.y = 0
 
    def updateCross(self, position):
        pygame.draw.rect(self.master, self.color,
                         (position[0] - 10, position[1] - 1, 7, 2))
 
        pygame.draw.rect(self.master, self.color,
                         (position[0] + 3, position[1] - 1, 7, 2))
 
        pygame.draw.rect(self.master, self.color,
                         (position[0] - 1, position[1] - 10, 2, 7))
 
        pygame.draw.rect(self.master, self.color,
                         (position[0] - 1, position[1] + 3, 2, 7))
 
class Bullet:
    inActive = 0
    def __init__(self, scene, radius=3, speed=5, mass=5):
        self.speed = speed
        self.mass = mass
        self.radius = radius
        self.master = scene
        self.color = RED
        self.x = 0
        self.y = 0
        self.yCross = 0
 
    def shoot(self, position):
        if Bullet.inActive == 0:
            Bullet.inActive = 1
            self.x = position[0]
            self.y = SCREEN_HEIGHT
            self.yCross = position[1]
            self.bullet = pygame.draw.circle(self.master, self.color, (self.x, self.y), self.radius)
 
 
    def updateBullet(self):
        if Bullet.inActive == 1:
            self.y -= self.speed
 
            if self.y <= self.yCross + self.mass and self.y >= self.yCross - self.mass:
                Bullet.inActive = 0
                crators.append([self.x, self.y, self.mass])
            else:
                self.bullet = pygame.draw.circle(self.master, self.color, (self.x, self.y), self.radius)
 
 
t = Bully("PyGame Text Animation wat")
c = Cross(sc, RED)
b = Bullet(sc)
 
while True:
    sc.fill(WHITE)
    pygame.mouse.set_visible(0)
    clock.tick(FPS)
 
    for j in crators:
        pygame.draw.circle(sc, BLACK, (j[0], j[1]), j[2])
 
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
 
    if pygame.mouse.get_focused():
        c.updateCross(pygame.mouse.get_pos())
        if i.type == pygame.MOUSEBUTTONDOWN:
            if i.button == 1:
                b.shoot(pygame.mouse.get_pos())
 
    b.updateBullet()
    t.tickT()
 
    pygame.display.set_caption(str(t))
    pygame.display.update()

"""Урок 4 pygame"""
import pygame
 
 
# константы, классы, функции
FPS = 60
RESOLUTION = (640, 480)
GREY = (220, 220, 220)
RED = (255, 0, 0)
WHITE = (255, 255, 255)
 
 
# инициализация, создание обьектов и другое
pygame.init()
screen = pygame.display.set_mode(RESOLUTION)
pygame.display.set_caption("Урок 4")
clock = pygame.time.Clock()
# начальный статус снаряда
shot = "not fly" 
# начальный радиус взрыва
r = 12
 
 
# главный цикл
while True:
 
    # стираем экран
    screen.fill(WHITE)
 
    # цикл обработки событий
    for i in pygame.event.get():
        # выход из программы
        if i.type == pygame.QUIT:
            exit()
        # нажатие клавиш мыши
        if i.type == pygame.MOUSEBUTTONDOWN:
            # если нажата ЛКМ и снаряд не летит
            if i.button == 1 and shot == "not fly":
                # выставляем стартовые значения снаряда
                position = pygame.mouse.get_pos()
                y = 480
                shot = "fly"
    # если снаряд выпущен (летит)
    if shot == "fly":
        # рисуем снаряд и смещаем координату 
        pygame.draw.circle(screen, GREY, (position[0], y), 10)
        y = y - 3
        # если долетел до точки назначения то взрыв
        if position[1] >= y:
            shot = "blast"
 
    # если происходит взрыв снаряда
    if shot == "blast":
        # рисуем взрыв
        pygame.draw.circle(screen, RED, (position[0], y), r)
        # увеличиваем радиус 
        r = r + 2
        # когда взрыв окончен
        if r == 30:
            # выставляем параметры взрыва обратно
            shot = "not fly"
            r = 12
    # обновляем экран
    pygame.display.update()
 
    # задержка
    clock.tick(FPS)