【问题标题】:Collision detection between circle and rectangle圆形和矩形之间的碰撞检测
【发布时间】:2023-01-26 04:56:58
【问题描述】:

我写了一些代码在屏幕上随机显示一个圆和一个矩形PyQt6.我想检测这两个物体是否发生碰撞,然后将它们设为红色,否则将它们设为绿色。

但是我应该如何检测是否有碰撞呢?

这是我的代码

from random import randint
from sys import argv
from PyQt6.QtCore import QRect, QTimer, Qt, QMimeData
from PyQt6.QtGui import QColor, QKeyEvent, QMouseEvent, QPainter, QPen, QPaintEvent, QBrush, QDrag
from PyQt6.QtWidgets import QApplication, QVBoxLayout, QMainWindow, QPushButton

class Window(QMainWindow):
    def __init__(self) -> None:
        super().__init__()
        screenWidth = 1920
        screenHeight = 1080
        self.isRunning = True
        self.windowWidth = 1200
        self.windowHeight = 800
        self.clockCounterVariable = 0
        self.milSec = 0
        self.seconds = 0
        self.minutes = 0
        self.hours = 0
        self.setWindowTitle("Smart rockets")
        self.setGeometry((screenWidth - self.windowWidth) // 2, (screenHeight - self.windowHeight) // 2, self.windowWidth, self.windowHeight)
        self.setLayout(QVBoxLayout())
        self.setStyleSheet("background-color:rgb(20, 20, 20);font-size:20px;")
        self.clock = QTimer(self)
        self.clock.timeout.connect(self.clockCounter)
        self.clock.start(10)
        button = QPushButton("Refresh", self)
        button.setGeometry(20,self.windowHeight - 60,self.windowWidth - 40,40)
        button.setStyleSheet("background-color:rgb(80, 80, 80);font-size:20px;")
        button.setCheckable(True)
        button.clicked.connect(self.refreshRectAndCircle)
        rectangleWidth = randint(50, 500)
        rectangleHeight = randint(50, 500)
        self.rectangle = QRect(randint(0, self.windowWidth - rectangleWidth), randint(0, self.windowHeight - rectangleHeight - 80), rectangleWidth, rectangleHeight)
        circleRadius = randint(50, 200)
        self.circle = QRect(randint(0, self.windowWidth - circleRadius), randint(0, self.windowHeight - circleRadius - 80), circleRadius, circleRadius)
        self.show()

    def dragEnterEvent(self, event) -> super:
        event.accept()

    def keyPressEvent(self, event: QKeyEvent) -> super:
        key = QKeyEvent.key(event)
        if key == 112 or key == 80: # P/p
            if self.isRunning:
                self.clock.stop()
                print("pause process")
                self.isRunning = False
            else:
                print("continue process")
                self.isRunning = True
                self.clock.start(10)
        elif (key == 115) or (key == 83): # S/s
            self.closeWindow()
        return super().keyPressEvent(event)

    def mousePressEvent(self, event: QMouseEvent) -> super:
        if event.buttons() == Qt.MouseButton.LeftButton:
            if self.isRunning:
                self.clock.stop()
                print("pause process")
                self.isRunning = False
            else:
                print("continue process")
                self.isRunning = True
                self.clock.start(10)
        return super().mousePressEvent(event)

    def clockCounter(self) -> None:
        self.clockCounterVariable += 1
        self.update()

    def paintEvent(self, a0: QPaintEvent) -> super:
        painter = QPainter()
        self.milSec = self.clockCounterVariable
        self.seconds, self.milSec = divmod(self.milSec, 100)
        self.minutes, self.seconds = divmod(self.seconds, 60)
        self.hours, self.minutes = divmod(self.minutes, 60)
        painter.begin(self)
        painter.setPen(QPen(QColor(255, 128, 20),  1, Qt.PenStyle.SolidLine))
        painter.drawText(QRect(35, 30, 400, 30), Qt.AlignmentFlag.AlignLeft, "{:02d} : {:02d} : {:02d} : {:02d}".format(self.hours, self.minutes, self.seconds, self.milSec))
        if self.collided():
            painter.setPen(QPen(QColor(255, 20, 20),  0, Qt.PenStyle.SolidLine))
            painter.setBrush(QBrush(QColor(128, 20, 20), Qt.BrushStyle.SolidPattern))
        else:
            painter.setPen(QPen(QColor(20, 255, 20),  0, Qt.PenStyle.SolidLine))
            painter.setBrush(QBrush(QColor(20, 128, 20), Qt.BrushStyle.SolidPattern))
        painter.drawRect(self.rectangle)
        painter.drawEllipse(self.circle)
        painter.end()
        return super().paintEvent(a0)
    
    def refreshRectAndCircle(self) -> None:
        rectangleWidth = randint(50, 500)
        rectangleHeight = randint(50, 500)
        self.rectangle = QRect(randint(0, self.windowWidth - rectangleWidth), randint(0, self.windowHeight - rectangleHeight - 80), rectangleWidth, rectangleHeight)
        circleRadius = randint(50, 200)
        self.circle = QRect(randint(0, self.windowWidth - circleRadius), randint(0, self.windowHeight - circleRadius - 80), circleRadius, circleRadius)
        self.update()

    def collided(self) -> bool:
        # return True if collided and return False if not collided
        circle = self.circle
        rect = self.rectangle

if __name__ == "__main__":
    App = QApplication(argv)
    window = Window()
    App.exec()

我应该如何检测圆形和矩形之间是否存在碰撞?

【问题讨论】:

标签: python math pyqt


【解决方案1】:

虽然您可以使用数学函数来实现这一点,但幸运的是 Qt 提供了一些有用的函数可以使这更容易。

您可以通过三个步骤实现这一目标——甚至只需一个步骤(参见上一节)。

检查圆心

如果圆心在矩形的边界内,你总是可以假设它们发生了碰撞。您使用的是 QRect,它是一个矩形总是与轴对齐,使事情变得容易得多。

从数学上讲,你只需要确保中心的X在矩形左右垂直线的最小和最大X之间,那么Y也一样。

Qt 允许我们检查圆圈的QRect.contains()QRect.center()

    def collided(self) -> bool:
        center = self.circle.center()
        if self.rectangle.contains(center):
            return True

检查矩形的顶点

如果圆心到矩形任意一个顶点的长度小于半径,则可以确定它们在圆区域内。

使用基本的勾股方程,可以知道矩形的圆心和每个顶点之间产生的斜边,如果斜边小于半径,则表示它们在圆内。

在 Qt 中,我们可以使用带有中心和顶点的 QLineFtopLeft()topRight()bottomRight()bottomLeft()),只要任何长度小于半径,就意味着顶点在圈子。使用QPolygonF,我们可以轻松地遍历 for 循环中的所有顶点。

        # ...
        center = QPointF(center)
        radius = self.circle.width() / 2
        corners = QPolygonF(QRectF(self.rectangle))[:4]
        for corner in corners:
            if QLineF(center, corner).length() < radius:
                return True

检查矩形的最近边

圆圈可能只与一个碰撞矩形的:圆的中心在矩形的外部,并且所有顶点都不在圆内。

考虑这种情况:

在这种情况下,只要矩形最近边的垂直线小于半径,就会发生碰撞:

使用数学,我们需要得到垂直于最近边的线,朝向圆心,计算边和连接中心与每个顶点的线之间的角度(上面以橙色显示),然后用借助一些三角学,得到其中一个三角形的直角(以红色显示):如果该线的长度小于半径,则形状会发生碰撞。

幸运的是,Qt 可以帮助我们。我们可以使用上面“检查矩形的顶点”部分中创建的线来获取两个最近的点,获取这些点的边并计算将用于创建“直径”的垂直角:从中心开始,我们用fromPolar()创建两条角度相反的线和半径,然后用这些线的外点创建实际直径。最后,我们检查直径 intersects() 是否与侧面。

这是最终功能:

    def collided(self) -> bool:
        center = self.circle.center()
        if self.rectangle.contains(center):
            return True

        # use floating point based coordinates
        center = QPointF(center)
        radius = self.circle.width() / 2
        corners = QPolygonF(QRectF(self.rectangle))[:4]

        lines = []
        for corner in corners:
            line = QLineF(center, corner)
            if line.length() < radius:
                return True
            lines.append(line)

        # sort lines by their lengths
        lines.sort(key=lambda l: l.length())
        # create the side of the closest points
        segment = QLineF(lines[0].p2(), lines[1].p2())
        # the perpendicular angle, intersecting with the center of the circle
        perpAngle = (segment.angle() + 90) % 360

        # the ends of the "diameter" per pendicular to the side
        d1 = QLineF.fromPolar(radius, perpAngle).translated(center)
        d2 = QLineF.fromPolar(radius, perpAngle + 180).translated(center)
        # the actual diameter line
        diameterLine = QLineF(d1.p2(), d2.p2())
        # get the intersection type
        intersection = diameterLine.intersects(segment, QPointF())
        return intersection == QLineF.BoundedIntersection

进一步的考虑

  • 在处理几何形状时,您应该考虑使用QPainterPath,这实际上使上面的代码变得极其简单:
    def collided(self) -> bool:
        circlePath = QPainterPath()
        circlePath.addEllipse(QRectF(self.circle))
        return circlePath.intersects(QRectF(self.rectangle))
  • Qt 有一个强大(但复杂)的Graphics View Framework,它使图形和用户交互更加直观和有效;虽然 QPainter API 对于更简单的情况肯定更容易,但一旦您的程序要求变得复杂,它可能会导致代码繁琐(且难以调试);

  • QMainWindow 有自己的、私有的和不可访问的布局管理器,你不能在上面调用setLayout();使用 setCentralWidget() 并最终为该小部件设置布局;

  • 绝不为父窗口部件使用通用样式表属性(就像您对主窗口所做的那样),因为它可能导致复杂窗口部件(如滚动区域)的绘制笨拙;始终使用 selector types 代替窗口和容器;

  • 除非你真的需要在 QMainWindow 内容上绘画(这种情况很少见),否则你应该始终在其中央小部件上实现 paintEvent();否则,如果您不需要 QMainWindow 功能(菜单栏、状态栏、停靠小部件和工具栏),只需使用 QWidget;

  • QTimer 对于精确的时间测量是不可靠的:如果在它运行时调用的任何函数需要的时间超过超时间隔,连接的函数将总是在之后被调用;请改用QElapsedTimer

  • paintEvent()中,只需使用painter = QPainter(self),删除painter.begin(self)(使用上面的隐式)和painter.end()(不必要,因为它在函数返回时自动销毁);

  • 不要创建不必要的实例属性(self.milSecself.seconds 等),它们几乎肯定迟早会被覆盖,并且您不会在其他地方使用;绘画事件必须始终尽快返回,并且必须始终尽可能优化;

【讨论】:

    猜你喜欢
    • 2014-05-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 2013-03-27
    • 1970-01-01
    • 2014-09-03
    • 1970-01-01
    相关资源
    最近更新 更多