pythonpyqt5collision-detectionqgraphicspixmapitem

PyQt5 pixel level collision detection


I'm learning Python and thought to make a simple platform game with PyQ5t. Currently I'm in trouble when I want to make a pixel level collision detection between shapes in my game. I have set the QPixMap to transparent and also tried to use QGraphicsPixmapItem.HeuristicMaskShape shape mode but collision detection does not work.

If I make shape (Mouse in this case) backgroun for example gray and remove the shape mode then there happens rectangular collision detection.

What I'm missing here? I have spent few hours digging around the Internet but no solution yet...

Here is my code to show the problem, please use arrow keys to move the the red "Mouse" around :) I hope to see collision detected text when the first pixel in the red circle touches the brown platform.

import sys
from PyQt5.QtGui import QPen, QBrush
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QApplication, QWidget, QGraphicsView, QGraphicsScene, QLabel, QGraphicsPixmapItem, QFrame

class Mouse(QGraphicsPixmapItem):

    def __init__(self, parent):
        super().__init__()
        self.canvas = QtGui.QPixmap(40,40)
        self.canvas.fill(Qt.transparent)
        self.setPixmap(self.canvas)
        self.x = 100
        self.y = 100
        self.setPos(self.x, self.y)
        self.setFlag(QGraphicsPixmapItem.ItemIsMovable)
        self.setFlag(QGraphicsPixmapItem.ItemIsFocusable)
        self.setShapeMode(QGraphicsPixmapItem.HeuristicMaskShape)
        self.setFocus()
        parent.addItem(self)

    def paint(self, painter, option, widget=None):
        super().paint(painter, option, widget)
        pen = QPen(Qt.black, 4, Qt.SolidLine)
        brush = QBrush(Qt.red, Qt.SolidPattern)
        painter.save()
        painter.setPen(pen)
        painter.setBrush(brush)
        painter.drawEllipse(QPoint(20,20),16,16)
        painter.restore()

    def keyPressEvent(self, e):
        if e.key() == Qt.Key_Right:
            self.x += 5
        if e.key() == Qt.Key_Left:
            self.x -= 5
        if e.key() == Qt.Key_Up:
            self.y -= 5
        if e.key() == Qt.Key_Down:
            self.y += 5
        self.setPos(self.x, self.y)
        collides_with_items = self.collidingItems(mode=Qt.IntersectsItemShape)
        if collides_with_items:
            print("Collision detected!")
            for item in collides_with_items:
                print(item)

class Platform(QFrame):

    PLATFORM_STYLE = "QFrame { color: rgb(153, 0, 0); \
                               background: rgba(0,0,0,0%); }"

    def __init__(self, parent, x, y, width, height):
        super().__init__()
        self.setGeometry(QtCore.QRect(x, y, width, height))
        self.setFrameShadow(QFrame.Plain)
        self.setLineWidth(10)
        self.setFrameShape(QFrame.HLine)
        self.setStyleSheet(Platform.PLATFORM_STYLE)
        parent.addWidget(self)

class GameScreen(QGraphicsScene):
    def __init__(self):
        super().__init__()

        # Draw background
        background = QLabel()
        background.setEnabled(True)
        background.setScaledContents(True)
        background.setGeometry(0, 0, 1280, 720)
        background.setPixmap(QtGui.QPixmap("StartScreen.png"))

        background.setText("")
        background.setTextFormat(QtCore.Qt.RichText)
        self.addWidget(background)
        self.line_5 = Platform(self, 0, 80, 431, 16)
        self.mouse = Mouse(self)

class Game(QGraphicsView):
    def __init__(self):  
        super().__init__()
        self.setWindowTitle("Running Mouse")
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.gamescreen = GameScreen()
        self.setScene(self.gamescreen)
        self.show()

if __name__ == '__main__':
    app = QApplication([])
    game = Game()
    sys.exit(app.exec_())

Solution

  • You're providing an empty pixmap (self.canvas), so no matter what shape mode you select, it will always have a null shape, unless you use BoundingRectShape, which uses the pixmap bounding rect.

    The fact that you are manually painting a circle really doesn't matter, as painting is not considered for collision detection (nor it should).

    You can override the shape() method to provide your own shape (using a QPainterPath):

    def shape(self):
        path = QtGui.QPainterPath()
        path.addEllipse(12, 12, 16, 16)
        return path
    

    But, since you're only drawing an ellipse, just use QGraphicsEllipseItem instead.


    Some unrequested suggestions: