簡體   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