PyQt5 pixel level collision detection

531 views Asked by At

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_())
1

There are 1 answers

1
musicamante On BEST ANSWER

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:

  • I'd avoid to use the name "parent" if you're intending the scene (the parent item of a QGraphicsItem could only be another QGraphicsItem), and an item usually shouldn't "add itself" to a scene;
  • don't overwrite self.x and self.y, as they are QGraphicsItem existing properties;
  • if you have multiple items in a scene and want to control just one using the keyboard, it's usually better to overwrite the key event methods of the scene or the view (especially if you're using arrow keys: even if the scrollbars are hidden, they can still intercept those movements);
  • if you still want to use the keyboard events on the item, ensure that the item keeps the keyboard focus: using setFocus is not enough, as it will lose focus as soon as the user clicks elsewhere; a possible solution is to use QGraphicsItem.grabKeyboard() (which works only as soon as the item is added to a scene);
  • if you set a stylesheet on a QFrame, it's very likely that it will at least partially ignore any frame option set (shape, shadow, etc), as they are style/platform dependant; it's usually better to not mix stylesheets with settings of Qt widgets that are related to visualization, so in your case you'll probably only need to add a simple QFrame with the correct stylesheet parameters set;
  • while technically it's not a problem, it's usually preferable to be consistent with the import "styles", especially when using complex modules like Qt is: you either import the single classes (from PyQt5.QtWidgets import QApplication, ...) or the submodules (from PyQt5 import QtCore, ...), otherwise it will make the code just confusing;