繁体   English   中英

圆形和矩形之间的碰撞检测

[英]Collision detection between circle and rectangle

我用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()

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

虽然您可以使用数学函数来实现这一点,但幸运的是 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 我们可以将QLineF与中心和顶点( topLeft()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()

这是最终的 function:

    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 具有强大(但复杂)的图形视图框架,使图形和用户交互更加直观和有效; 虽然 QPainter API 对于更简单的情况当然更容易,但一旦您的程序要求变得复杂,它可能会导致代码繁琐(且难以调试);

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

  • 永远不要为父窗口部件使用通用样式表属性(就像您对主窗口所做的那样),因为它可能导致复杂窗口部件(如滚动区域)的绘制笨拙; 始终对 windows 和容器使用选择器类型

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

  • QTimer 不能可靠地进行精确的时间测量:如果在运行时调用任何 function 需要的时间超过超时间隔,则连接的 function 将始终在之后被调用; 使用QElapsedTimer代替;

  • paintEvent()中,只需使用painter = QPainter(self) ,删除painter.begin(self) (使用上述内容是隐含的)和painter.end() (不必要,因为当 function 返回时它会自动销毁);

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM