Firing Mechanic

In this lesson, you will create the ability for player's to fire bullets.


Before you get started, create a new version of your program. Save the spaceGame_v2.py file and then re-save the file as spaceGame_v3.py.


Controls

You should already have all the movement controls working from the previous lessons. Setting up the event listener and event handlers for the firing mechanic will be very similar. However, you will need to detect mouse clicks as well because the second player will be using their mouse to fire.

Mac Users: if you have been following along with the Mac instructions for this lesson, some of the line numbers in the following sections may be different. Pay attention to the surrounding code to place the code in the right place.

Add the event listeners to detect when the spacebar is pressed and when the mouse is clicked.

# Player 1 Event Listeners
screen.onkeypress(wPressed, "w")
screen.onkeyrelease(wReleased, "w")
screen.onkeypress(aPressed, "a")
screen.onkeyrelease(aReleased, "a")
screen.onkeypress(dPressed, "d")
screen.onkeyrelease(dReleased, "d")
screen.onkeypress(spacePressed, "space")

# Player 2 Event Listeners
screen.onkeypress(upArrowPressed, "Up")
screen.onkeyrelease(upArrowReleased, "Up")
screen.onkeypress(leftArrowPressed, "Left")
screen.onkeyrelease(leftArrowReleased, "Left")
screen.onkeypress(rightArrowPressed, "Right")
screen.onkeyrelease(rightArrowReleased, "Right")
screen.onclick(mouseClicked)

screen.listen()

gameloop()

Create the event handlers using the function names that you passed to the onkeypress and onclick event listeners. Call the appropriate player's fire method from each event handler.

# Player 1 Event Handlers
def wPressed(): p1.isAccelerating = True
def wReleased(): p1.isAccelerating = False
def aPressed(): p1.isTurningLeft = True
def aReleased(): p1.isTurningLeft = False
def dPressed(): p1.isTurningRight = True
def dReleased(): p1.isTurningRight = False
def spacePressed() : p1.fire()

# Player 2 Event Handlers
def upArrowPressed(): p2.isAccelerating = True
def upArrowReleased(): p2.isAccelerating = False
def leftArrowPressed(): p2.isTurningLeft = True
def leftArrowReleased(): p2.isTurningLeft = False
def rightArrowPressed(): p2.isTurningRight = True
def rightArrowReleased(): p2.isTurningRight = False
def mouseClicked(x, y): p2.fire()

Notice that the x and y coordinates are sent as arguments from the onclick event listeners. Knowing where the player clicked on the screen can sometimes be useful, but for our purposes there is no need to use these values.

Now, add the fire method to the Player class right after the move method.

        # Move Forward
        self.turtle.fd(self.speed)

    def fire(self):
        print(self.color + " ship fired.")

def gameloop():
    p1.turtle.clear()
    p2.turtle.clear()

Run the program. You should see the appropriate messages print to the console when the spacebar is pressed or the mouse is clicked. Notice that in python, the onclick method fires immediately when the left mouse button is pressed down instead of when it is released.


Bullet Class

For the ships to be able to fire bullets, we need Bullet objects; and to create Bullet objects, we need a Bullet Class.

import turtle
import time

screen = turtle.Screen()

class Bullet:
    SPEED = 100

    def __init__(self, owner):
        self.turtle = turtle.Turtle()
        self.turtle.ht()
        self.turtle.speed(0)
        self.turtle.setpos(owner.turtle.pos())
        self.turtle.setheading(owner.turtle.heading())

    def move(self):
        self.turtle.fd(Bullet.SPEED)

    def drawBullet(self):
        t = self.turtle
        startPos = t.pos()
        startHeading = t.heading()
        t.fillcolor("white")
        t.pencolor("white")
        t.begin_fill()
        t.pd()
        t.fd(1)
        t.lt(90)
        t.circle(2)
        t.end_fill()
        
        t.pu()
        t.setpos(startPos)
        t.setheading(startHeading)

class Player:
    MAX_SPEED = 10
    ROTATION_SPEED = 5

Most of this code should be very familiar is it is quite similar to the code that you used for the Player class. There are a few differences to make not of.

The SPEED constant is created as a class variable that all Bullet obejects will share. The speed of 100 indicates that it will move 100 pixels per frame.

The __init__ function has two parameters. The first parameter should be expected, but you may be surprised by the second parameter named owner.

The owner serves as a reference to the Player object that fired the bullet.

Remember the __init__ method is used to initialize an object when it is created. For the Bullet, that includes creating a Turtle that will be used to control the Bullets postion and heading and to draw the Bullet. This turtle is then hidden using the ht method and set to draw without delay using the speed method.

The last two lines of the __init__ method set the position and heading of the Bullet object's turtle to be the same as the turtle belonging to the player the Bullet belongs to.

The move method of the Bullet class simply moves the bullet forward.

Finally, the drawBullet method draws the bullet as a small white circle.

Having the Bullet class without instantiating any Bullet objects should have no effect on your program. Still, this is a good time to run the program to ensure there are no errors.


Many Bullets

Variables (p1 and p2) were used to store each Player created. This approach is fine when there are a small and unchanging number of objects to store.

However, because the ships will be able to fire many Bullet objects, using a different variable for each Bullet object will not suffice. Time to use a list!

A list is being used instead of a tuple because the number of Bullets will fluctuate during the game. Recall that, unlike tuples, lists are able to grow and shrink to store any number of objects.

Add bullets as a property of the Player class. For each player, this property will serve as a reference to a list of bullets they have fired.

class Player:
    MAX_SPEED = 10
    ROTATION_SPEED = 5

    def __init__(self, color, position, heading):
        self.isAccelerating = False
        self.isTurningLeft = False
        self.isTurningRight = False
        self.speed = 0
        self.color = color
        self.turtle = turtle.Turtle()
        self.turtle.ht()
        self.turtle.speed(0)
        self.turtle.setpos(position)
        self.turtle.setheading(heading)
        self.bullets = list()

Next, remove the print statement from the fire method of the Player class. In its place, add code to create add a new Bullet object to the player's bullets list.

    def fire(self):
        self.bullets.append(Bullet(self))

When function calls are nested, work from the inner most function outward. Here, the Bullet(self) is evaluated first. This constructor returns a Bullet object. Next, the append method is evaluated using the Bullet that was created.

This same task could have been done in two separate statements:

    def fire(self):
        b = Bullet(self)
        self.bullets.append(b)

While new programmers might find this more readable, experienced programmers would likely find the use of an additional variable unnecessary.

Notice the call to the Bullet constructor is passing self as an argument. Take a moment to think about this, recalling how the Bullet classes __init__ method was defined.

    def __init__(self, owner):

Within the fire method of the Player class, self is a reference to the Player who is creating the Bullet.

Within the __init__ method of the Bullet class, self is a reference to the Bullet that is being created.

The self in the fire method is completely unrelated to the self of the Bullet's __init__ method.

This is because the first argument given in the call to the constructor is received as the second parameter. Recall that the first parameter is automatically used to store a reference to the object that is being created.


Now that we have bullets being created, we just need to animate them the same way the ships are animated. That is, each frame the bullets need to be erased, moved, and then redrawn in their new location.

Step 1: Erase the bullets

This could be done from in the gameloop where the ships are erased. However, creating a method for the Player to handle the erasing of all objects belonging to a player might be a more elegant solution. This approach will keep related code with a single purpose grouped together and make the program more readable.

Create a clear method for the Player class that erases everything belonging to the player, including the ship and bullets.

    def fire(self):
        self.bullets.append(Bullet(self))

    def clear(self):
        self.turtle.clear()
        for b in self.bullets:
            b.turtle.clear()

The first line of the code clears the turtle of the Player (the turtle used to draw the player's ship). The for loop is used to clear each bullet that is stored in the player's bullets list.

Has for loops been covered yet? If not, need to add more here.

Update the gameloop to utilize this method instead of clearing only the player's ships.

def gameloop():
    p1.clear()
    p2.clear()
    
    p1.move()
    p2.move()

Step 2: Move the bullets

def gameloop():
    p1.clear()
    p2.clear()

    p1.move()
    for b in p1.bullets:
        b.move()
    p2.move()
    for b in p2.bullets:
        b.move()

Step 3: Draw the bullets at their new positions

def gameloop():
    p1.clear()
    p2.clear()

    p1.move()
    for b in p1.bullets:
        b.move()
    p2.move()
    for b in p2.bullets:
        b.move()

    p1.drawShip()
    for b in p1.bullets:
        b.drawBullet()
    p2.drawShip()
    for b in p2.bullets:
        b.drawBullet()
    
    turtle.update() # refresh screen
    screen.ontimer(gameloop, 10)

Play the game to ensure that both players are able to fire bullets.



Notice that bullets are dealing no damage... yet!


Destroying Bullets

Currently, your game is spawning bullets every time a player fires. However, the bullets are never destroyed. This can become problematic.

For each bullet, the code for erasing, moving, and drawing the bullet executes every single frame. Code that will be added later to detect collisions will also run every frame, for each bullet.

As you might imagine, if too many bullets will cause lag and eventually crash your program. Additionally, every Bullet object created takes up memory on the computer, and you do not want the program using more memory than needed.

The solution of course is to destroy Bullets. You might do this when the bullet exceeds some distance from the center of the game, or after some amount of time has passed.

The approach used below is to simply limit the number of bullets that are stored in the bullets list, destroying a bullet when the number of bullets exceeds the limit.

First, create a constant to store the maximum number of bullets allowed per player. Define the property so that its value will be shared by all players. For testing purposes, set the maximum bullets to two.

class Player:
    MAX_SPEED = 10
    ROTATION_SPEED = 5
    MAX_BULLETS = 2

Then update the fire method to check how long the list is right after a new Bullet is appended to the list. If the bullets list exceeds the limit, erase the first bullet of the list.

    def fire(self):
        self.bullets.append(Bullet(self))
        if len(self.bullets) > Player.MAX_BULLETS:
            self.bullets[0].turtle.clear()
            self.bullets.pop(0)

Because new bullets are always added at the end of the list, the first bullet in the list will always be the bullet that was fired earlier than any other bullets in the list.

Play the game to test that the bullets are being removed from the list. You will need to repeatedly click the mouse or press the spacebar quickly to see the bullets removed before they fly off the screen.



Once you know the bullets list is being constrained, set the maximum to 10.

class Player:
    MAX_SPEED = 10
    ROTATION_SPEED = 5
    MAX_BULLETS = 10


Stop Auto Keypress

As discussed in a previous lesson, holding down a key on the keyboard for a few moments causes the operating system to begin generating automatic keypresses. You can see this occur in the game when you hold down the spacebar and the first players ship soon begins to fire repeatedly.

Mac Users: if you are using a Mac computer, you will not be able to set this instructions because of the onkeyrelease issue. However, you can lower the max number of bullets in the Player class to achieve a similar effect.

To fix this issue, add an isReadyToFire property to the Player class with an initial value of True.

class Player:
    MAX_SPEED = 10
    ROTATION_SPEED = 5
    MAX_BULLETS = 10
    
    def __init__(self, color, position, heading):
        self.isReadyToFire = True
        self.isAccelerating = False

This property will be used when the player attempts to fire to determine if they are allowed to or not.

Add an event listener to detect when the player releases the spacebar.

screen.onkeypress(spacePressed, "space")
screen.onkeyrelease(spaceReleased, "space")

Add the spaceReleased event handler to reset the first player's isReadyToFire property to True anytime the spacebar is released.

def spacePressed() : p1.fire()
def spaceReleased() : p1.isReadyToFire = True

Finally, modify the spacePressed event handler to not fire immediately, but instead to fire only when the first player is ready to fire. When the first player does fire, the isReadyToFire property should be set to false so they are unable to fire again until they release the spacebar.

def spacePressed() :
    if p1.isReadyToFire :
        p1.fire()
        p1.isReadyToFire = False
def spaceReleased() : p1.isReadyToFire = True

Play the game again to ensure that the first player is unable to hold down the spacebar to autofire.

The last step needed to achieve the core game functionality will be to make the bullets cause damage. This code will be added in the next lesson, wrapping up the Space Game project.