Search code examples
pythonpygameselfrect

How do I make a projectile in pygame that will move upward?


I wrote a game that is meant to shoot a blue square up the screen when the spacebar is pressed. However, all I get is the blue square right above my shooter. Here is my code:

import pygame, sys, random
from gameobjects import *


def main():

    pygame.init()

    screen = pygame.display.set_mode((800, 800))

    numberOfColumns = 5
    columnWidth = screen.get_width() / numberOfColumns

    numberOfRows = 5
    rowWidth = screen.get_width() / numberOfRows

    magazineImage = pygame.image.load("images/Magazine.bmp")
    magazineIssueOneImage = pygame.image.load("images/Magazine #1.bmp")

    monsterState1 = pygame.image.load("images/Zombie Pos 1.bmp")
    monsterState2 = pygame.image.load("images/Zombie Pos 2.bmp")
    monsterState3 = pygame.image.load("images/Zombie Pos 3.bmp")
    monsters = []

    forryState1 = pygame.image.load("images/Forry Pos 1.bmp")    
    forryState2 = pygame.image.load("images/Forry Pos 2.bmp")
    forryDelayTime = 8

    gameObjects = []
    gameObjectBlittingCounter = 0

    monsterSpawnSpotList = [(rowWidth * -5), (rowWidth * -4), (rowWidth * -3), (rowWidth * -2), (rowWidth * -1)]

    for monster in range(0, 5):
        monster = Monster(monsterState1, monsterState2, monsterState3, ((columnWidth / 2 - monsterState1.get_width() / 2) + gameObjectBlittingCounter * columnWidth), random.choice(monsterSpawnSpotList))
        monsters.append(monster)
        gameObjects.append(monster)
        gameObjectBlittingCounter += 1

    forry = Forry(forryState1, forryState2, columnWidth, rowWidth, screen, forryDelayTime, magazineImage, magazineIssueOneImage)
    gameObjects.append(forry)


    while True:


        screen.fill((0,255,0))

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        #Update Projectiles

        if forry.projectileCall == True:
            magazine = Magazine(magazineImage, forry)
            gameObjects.append(magazine)

        #Update Game Objects

        for gameObject in gameObjects:
            gameObject.update()


        #Render
        gameObjectBlittingCounter = 0

        for gameObject in gameObjects:
            screen.blit(gameObjects[gameObjectBlittingCounter].image, ((gameObjects[gameObjectBlittingCounter].rect.x), (gameObjects[gameObjectBlittingCounter].rect.y)))
            gameObjectBlittingCounter += 1


        pygame.display.flip()




if __name__ == "__main__":
    main()

The file gameobjects.py looks like this:

import pygame


class Monster(pygame.sprite.Sprite):
    def __init__(self, monsterState1, monsterState2, monsterState3, xCoord, yCoord):
        self.monsterState1 = monsterState1
        self.monsterState2 = monsterState2
        self.monsterState3 = monsterState3
        self.image = self.monsterState1
        self.rect = self.image.get_rect()
        self.rect.y = yCoord
        self.rect.x = xCoord


    def update(self):
        self.moveDown()

    def moveDown(self):
        self.rect.y += 1

    def onSpawn(self):
        return

    def onDeath(self):
        return

    def reset(self):
        return



class Forry(pygame.sprite.Sprite):
    def __init__(self, forryState1, forryState2, columnWidth, rowWidth, screen, delayTime, magazineImage, magazineIssueOneImage):
        self.forryState1 = forryState1
        self.forryState2 = forryState2
        self.image = self.forryState1
        self.columnWidth = columnWidth
        self.rowWidth = rowWidth
        self.rect = self.image.get_rect()
        self.rect.y = self.columnWidth / 2 - self.image.get_width() / 2
        self.rect.y = screen.get_height() - (self.rowWidth / 2 - self.image.get_height() / 2)
        self.delayTime = delayTime
        self.delay = 0
        self.magazineImage = magazineImage
        self.magazineIssueOneImage = magazineIssueOneImage
        self.projectileCall = False
        self.rightBound = screen.get_width() - (self.columnWidth / 2 + self.image.get_width() / 2)
        self.leftBound = (self.columnWidth / 2 - self.image.get_width() / 2)


    def update(self):
        #Check Player Input
        playerInput = self.checkPlayerInput()

        #Update Position
        if self.delay > 0:
            self.delay -= 1
        elif self.delay < 0:
            self.delay = 0
        else:
            self.updatePosition(playerInput)

        #Keep on the Screen
        self.keepOnScreen()

    def checkPlayerInput(self):
        left = pygame.key.get_pressed() [pygame.K_LEFT]
        right = pygame.key.get_pressed() [pygame.K_RIGHT]
        shoot = pygame.key.get_pressed() [pygame.K_SPACE]
        return (left, right, shoot)

    def updatePosition(self, playerInput):
        left = playerInput[0]
        right = playerInput[1]
        shoot = playerInput[2]
        if self.projectileCall == True:
            self.projectileCall = False
            self.image = self.forryState1
        if shoot:
            self.image = self.forryState2
            self.projectileCall = True
            self.delay = self.delayTime
        if left:
            self.rect.x -= self.columnWidth
            self.delay = self.delayTime
        if right:
            self.rect.x += self.columnWidth
            self.delay = self.delayTime

    def keepOnScreen(self):
        if self.rect.x >= self.rightBound:
            self.rect.x = self.rightBound
        elif self.rect.x <= self.leftBound:
            self.rect.x = self.leftBound


    def onSpawn(self):
        return

    def onDeath(self):
        return

    def reset(self):
        return


class Magazine(pygame.sprite.Sprite):
    def __init__(self, image, shooter):
        self.image = image
        self.rect = image.get_rect()
        self.rect.x = shooter.rect.x
        self.rect.y = shooter.rect.y - shooter.image.get_height()

        def update(self):
            self.rect.y -= 5
            if self.rect.y <= 0 - self.image.get_height():
                self.kill()

My magazine is a blue square

So why doesn't my projectile go up the screen?

++++++++

With all the fixes in the answer below, now I have another error:

When the blue box goes off screen, it is supposed to be killed because of self.kill() in the update function of the Magazine class.

However, instead of just being killed, the minute one of them gets off screen, it gives me these errors:

Traceback (most recent call last):
  File "main.py", line 106, in <module>
    main()
  File "main.py", line 78, in main
    projectileObject.update()
  File "/Users/number1son100/Desktop/Famous Monsters Game/gameobjects.py", line 117, in update
    self.kill()
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/pygame/sprite.py", line 174, in kill
    for c in self.__g.keys():
AttributeError: 'Magazine' object has no attribute '_Sprite__g'

Solution

  • Answer

    Stumped me for a while, a classic case of "right under our noses":

    Look at the indentation of your "update" function under Magazine, it is indented one too far to the right, which means it is a function defined -inside- __init__() instead of as a method of Magazine as you wanted. If this didnt make much sense to you, just "dedent" that block so it looks like this

    class Magazine(pygame.sprite.Sprite):
        def __init__(self, image, shooter):
            self.image = image
            self.rect = image.get_rect()
            self.rect.x = shooter.rect.x
            self.rect.y = shooter.rect.y - shooter.image.get_height()
    
        def update(self):
            self.rect.y -= 5
            if self.rect.y <= 0 - self.image.get_height():
                self.kill()
    

    some explaination why nothing happened but no error: As your update function was not a method of Magazine, it did not overwrite the one that was inherited from pygame.sprite.Sprite. As a result when it was called from your main loop, the placehold/basic method was called, not your custom one. This meant you didnt get an error/exception but it didnt work!

    Moving on, there are a few major things the issue here too that could do with correction:

    Addendum

    1)

    Not neccessarily an issue, but you can use the python "for x in y" syntax like you do here:

        for gameObject in gameObjects:
            gameObject.update()
    

    later on in the render section, i.e.:

        #Render
        gameObjectBlittingCounter = 0
    
        for gameObject in gameObjects:
            screen.blit(gameObjects[gameObjectBlittingCounter].image, ((gameObjects[gameObjectBlittingCounter].rect.x), (gameObjects[gameObjectBlittingCounter].rect.y)))
            gameObjectBlittingCounter += 1
    

    becomes:

        for gameObject in gameObjects:
            screen.blit(gameObject.image, gameObject.rect)
    

    note also you can just pass the whole rect instead of splitting to rect.x and rect.y

    2)

    You will notice this once you fix the update function, your current code spawns multiple shots when you press the spacebar, this is because your spawn mechanic looks if spacebar is held and spawns a projectile every loop, try removing this spawning from the loop and just do it once when space is pressed or released

    3)

    Cap the framerate

    In my testing the framerate could spike very high and make the game run oddly. Cap the framerate using a pygame.Clock object and calling clock.tick(framerate) in your loop

    4)

    In preparation for further complexity, consider splitting your "gameObjects" lists up into several different types (enemyObjects, projectileObjects, etc), because these may need different treatment at some point, or saves you looping the entire list for something that only effects enemies if that occurs.


    Finally a small plug of my website, I have source and docs for some pygame libraries that you can use and may be help with you in future.