Search code examples
pythonpyqtpyqt6

QGraphicsTextItem is not updating the score


I want to build a simple game in pyqt, i want when the enemy collides with the bullet increase the score, I have created a Score class that extends from QGraphicsTextItem, I have created the score text also I have added an increase method. after that I have added this class in my Bullet.py file, because in the Bullet.py collision occurs, also I have added this class in my Window.py file, because I want that text should be in the scene, but when I run the game the score is 0 and after colliding nothing happens, these are my files

Window.py

from PyQt6.QtWidgets import QGraphicsScene, QApplication, QGraphicsView, QGraphicsItem
from PyQt6.QtCore import Qt, QTimer
import sys
from Player import Player
from Enemy import Enemy
from Score import Score

class Window(QGraphicsView):

    def __init__(self):
        super().__init__()

        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)

        self.setFixedSize(800, 600)
        self.create_scene()

        self.show()


    def create_scene(self):
        self.scene = QGraphicsScene()

        # create an item to put in the scene
        player = Player()

        # make rect focusable
        player.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
        player.setFocus()

        # by default QGraphicsRectItem has 0 length and width
        player.setRect(0, 0, 100, 100)

        # add item to the scene
        self.scene.addItem(player)

        # set size of the scene
        self.scene.setSceneRect(0, 0, 800, 600)

        # set the player at the botoom
        player.setPos(self.width() / 2, self.height() - player.rect().height())
        

        #adding the score to the scene
        score = Score()
        self.scene.addItem(score)


        self.setScene(self.scene)

        self.timer = QTimer()
        self.timer.timeout.connect(self.spawn)
        self.timer.start(2000)


    def spawn(self):
        enemy = Enemy()
        self.scene.addItem(enemy)


App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())

Player.py

from PyQt6.QtWidgets import QGraphicsRectItem
from PyQt6.QtGui import QKeyEvent
from PyQt6.QtCore import Qt
from Bullet import MyBullet




class Player(QGraphicsRectItem):

    def __init__(self):
       super().__init__()



    def keyPressEvent(self, event: QKeyEvent):
        if (event.key() == Qt.Key.Key_Left):

            if self.pos().x() > 0:
                self.setPos(self.x() - 10, self.y())

        elif (event.key() == Qt.Key.Key_Right):
            if (self.pos().x() + 100 < 800):
                self.setPos(self.x() + 10, self.y())


        elif (event.key() == Qt.Key.Key_Space):
            mybullet = MyBullet()
            mybullet.setPos(self.x(), self.y())
            self.scene().addItem(mybullet)

Enemy.py

from PyQt6.QtWidgets import QGraphicsRectItem
from random import randint
from PyQt6.QtCore import QTimer


class Enemy(QGraphicsRectItem):
    def __init__(self):
        super().__init__()


        random_number = randint(10,1000) % 700
        self.setPos(random_number , 0)


        self.setRect(0,0,100,100)

        self.timer = QTimer()
        self.timer.timeout.connect(self.move)
        self.timer.start(50)




    def move(self):
        #move enemy to down
        self.setPos(self.x(), self.y()+5)

        if self.pos().y() + self.rect().height() < 0:
            self.scene().removeItem(self)
            print("Bullet deleted")

Bullet.py

from PyQt6.QtWidgets import QGraphicsRectItem, QGraphicsItem
from PyQt6.QtCore import QTimer
from Enemy import Enemy
from Score import Score


class MyBullet(QGraphicsRectItem):
    def __init__(self):
        super().__init__()


        self.setRect(0, 0, 10, 50)

        self.timer = QTimer()
        self.timer.timeout.connect(self.move)
        self.timer.start(50)


    def move(self):
        # This is the place for the collision
        colliding = self.collidingItems()
        for item in colliding:
            if isinstance(item, Enemy):

                # increase the score
                score = Score()
                score.increase()

                self.scene().removeItem(item)
                self.scene().removeItem(self)

        self.setPos(self.x(), self.y() - 10)

        if self.pos().y() + self.rect().height() < 0:
            self.scene().removeItem(self)
            print("Bullet deleted")

And this is my Score.py file

from PyQt6.QtWidgets import QGraphicsTextItem
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont

class Score(QGraphicsTextItem):

    def __init__(self):
       super().__init__()
       self.score = 0
       # draw the text
       self.setPlainText("Score : " + str(self.score))
       self.setDefaultTextColor(Qt.GlobalColor.red)
       self.setFont(QFont("Sanserif", 18))


    def increase(self):
        self.score += 1
        self.setPlainText(str(self.score))

Solution

  • The problem is that you're creating a score item every time the collision is detected, but that item is not added to the scene, so it gets deleted at the end of move, making it useless; but, even if you did add that item to the scene, it would be pointless, since a score item already exists on the scene, but you didn't keep a persistent reference to it, so you cannot call it's increase method.

    So, three changes are required:

    1. find a way to communicate to the scene that a collision happened and the score has to increase; the correct way to do so would be through a signal, but since basic QGraphicsItems do not inherit from QObject, you cannot create a signal for them, so you can use a class that acts as a "signal proxy";
    class BulletSignals(QObject):
        enemyHit = pyqtSignal()
    
    class MyBullet(QGraphicsRectItem):
        def __init__(self):
            super().__init__()
            self.proxy = BulletSignals()
            self.enemyHit = self.proxy.enemyHit
            # ...
    
        def move(self):
            colliding = self.collidingItems()
            isHit = False
            for item in colliding:
                if isinstance(item, Enemy):
                    self.scene().removeItem(item)
                    isHit = True
            if isHit:
                self.scene().removeItem(self)
                self.enemyHit.emit()
    
            else:
                self.setY(self.y() - 10)
    
                if self.pos().y() + self.rect().height() < 0:
                    self.scene().removeItem(self)
                print("Bullet deleted")
    
    1. connect the signal of the bullet; this requires a small but important change in the logic, and that's because a better and more correct OOP implementation would make the scene's responsibility (or, better, the "main controller") to add the bullet; so, we will add a signal proxy to the player too;
    class PlayerSignals(QObject):
        fire = pyqtSignal(QPointF)
    
    class Player(QGraphicsRectItem):
    
        def __init__(self):
            super().__init__()
            self.proxy = PlayerSignal()
            self.fire = self.proxy.fire
    
        def keyPressEvent(self, event: QKeyEvent):
            if (event.key() == Qt.Key.Key_Left):
                if self.pos().x() > 0:
                    self.setX(max(0, self.x() - 10))
    
            elif (event.key() == Qt.Key.Key_Right):
                if (self.pos().x() + 10 < 800):
                    self.setX(min(800, self.x() + 10))
    
            elif (event.key() == Qt.Key.Key_Space):
                self.fire.emit(self.pos()
    
    1. make both score and player instance members, and connect the signals when required:
    class Window(QGraphicsView):
        # ...
        def create_scene(self):
            self.scene = QGraphicsScene()
    
            # create an item to put in the scene
            self.player = Player()
            self.player.fire.connect(self.fire)
    
            # ...
            self.score = Score()
            self.scene.addItem(self.score)
            # ...
    
        def fire(self, pos):
            bullet = MyBullet()
            bullet.setPos(pos)
            self.scene.addItem(bullet)
            bullet.enemyHit.connect(self.score.increase)
    

    Note that:

    1. I changed some small things (most notably, if you only need to move on a single axis, just use setX() or setY())
    2. as much as it should be the scene/controller responsibility to add items, it should also be to remove them; I didn't change that to avoid complicating things, but remember that an item should be removed only by the scene or one of its ancestors; also, you should not try to remove an item more than once;
    3. a timer that could potentially remove an item should be used with care, as the result is that sometimes move will be called right after the item has been removed from the scene, which could raise an exception (since self.scene() would return None); the following change would fix this:
    class Enemy(QGraphicsRectItem):
        # ...
        def itemChange(self, change, value):
            if change == self.ItemSceneChange:
                if value:
                    self.timer.start(50)
                else:
                    self.timer.stop()
            return super().itemChange(change, value)
    

    the same obviously is required for MyBullet too, and you should remove self.timer.start(50) from both the __init__ of those classes;

    Finally, while the scene of a view is generally always the same during the lifespan of the program, that's not always true; scene() is an existing dynamic function of QGraphicsView, so you should not overwrite it with self.scene.