Skip to content

10. Laser

In this lesson we will learn

  • how to plan a new object with an IPO table
  • how to spawn an object when a key is pressed
  • how to split a long instruction over several lines
  • how to use a flag variable to track the state of the program

Terminology

  • style guide – a set of rules for how code should be written and laid out so it is easy to read, such as keeping lines to 79 characters or fewer.
  • flag variable – a variable that is either True or False and records the state of something, which our code checks to decide what to do.
  • state – the current condition of something in a program, such as whether the ship is allowed to shoot.

Zork is hurling asteroids at our defenceless spaceship, so we'd better give it a way to fight back. In this lesson we'll arm the ship with a laser.

Shooting the laser

Planning

We want the ship to spawn a laser whenever we press the space key.

Laser IPO

This involves:

  • creating a new Laser class
  • adding a space-key event handler to the Ship, which creates a Laser
  • making the laser move across the Room when it spawns
  • deleting the laser when it leaves the Room

We already know how to do all of this, so let's get coding.

Create the Laser class

Create a new file in the Objects folder, add the code below and save it as Laser.py.

Objects/Laser.py
from GameFrame import RoomObject, Globals

class Laser(RoomObject):
    """
    Class for the lasers shot by the Ship
    """

    def __init__(self, room, x, y):
        """
        Initialise the laser
        """
        # include attributes and methods from RoomObject
        RoomObject.__init__(self, room, x, y)

        # set image
        image = self.load_image("laser.png")
        self.set_image(image, 33, 9)

        # set movement
        self.set_direction(0, 20)

    def step(self):
        """
        Determine what happens to the laser on each tick of the game clock
        """
        self.outside_of_room()

    def outside_of_room(self):
        """
        removes laser if it has exited the room
        """
        if self.x > Globals.SCREEN_WIDTH:
            self.room.delete_object(self)
Code explanation
  • line 1 → imports RoomObject and Globals.
  • line 3 → defines the Laser class as a subclass of RoomObject.
  • lines 4–6 → a docstring that explains what the class is for.
  • line 8 → defines the __init__ method.
  • lines 9–11 → a docstring that explains what the method does.
  • line 13 → runs RoomObject's __init__ method.
  • lines 16–17 → loads laser.png and gives it to the laser at 33 × 9 pixels.
  • line 20 → starts the laser moving at 0° (right) at 20 pixels per frame.
  • line 22 → defines the step method, which runs on every tick.
  • lines 23–25 → a docstring that explains what the method does.
  • line 26 → calls outside_of_room on every tick.
  • line 28 → defines the outside_of_room method.
  • lines 29–31 → a docstring that explains what the method does.
  • line 32 → checks if the laser has gone past the right edge of the screen. The laser moves right, so we check its x against the screen width…
  • line 33 → …and deletes the laser.

Open Objects/__init__.py, add the highlighted code below and save it.

Objects/__init__.py
1
2
3
4
5
from Objects.Title import Title
from Objects.Ship import Ship
from Objects.Zork import Zork
from Objects.Asteroid import Asteroid
from Objects.Laser import Laser
Code explanation
  • line 5 → imports the Laser class, so GameFrame can find it.

Shoot from the Ship

Open Objects/Ship.py and add the highlighted code below.

Objects/Ship.py
from GameFrame import RoomObject, Globals
from Objects.Laser import Laser
import pygame

class Ship(RoomObject):
    """
    A class for the player's avatar (the Ship)
    """

    def __init__(self, room, x, y):
        """
        Initialise the Ship object
        """
        RoomObject.__init__(self, room, x, y)

        # set image
        image = self.load_image("Ship.png")
        self.set_image(image,100,100)

        # register events
        self.handle_key_events = True

    def key_pressed(self, key):
        """
        Respond to keypress up and down
        """

        if key[pygame.K_w]:
            self.y_speed = -10
        elif key[pygame.K_s]:
            self.y_speed = 10
        if key[pygame.K_SPACE]:
            self.shoot_laser()

    def keep_in_room(self):
        """
        Keeps the ship inside the room
        """
        if self.y < 0:
            self.y = 0
        elif self.y + self.height > Globals.SCREEN_HEIGHT:
            self.y = Globals.SCREEN_HEIGHT - self.height

    def step(self):
        """
        Determine what happens to the Ship on each tick of the game clock
        """
        self.keep_in_room()

    def shoot_laser(self):
        """
        Shoots a laser from the ship
        """
        new_laser = Laser(self.room, 
                          self.x + self.width, 
                          self.y + self.height/2 - 4)
        self.room.add_room_object(new_laser)
Code explanation
  • line 2 → imports the Laser class, because the Ship creates lasers.
  • line 32 → checks if the space key is pressed…
  • line 33 → …and calls the Ship's shoot_laser method.
  • line 50 → defines the shoot_laser method.
  • lines 51–53 → a docstring that explains what the method does.
  • lines 54–56 → create a new Laser in the Ship's Room, at the right-hand side of the ship (self.x + self.width) and halfway down it (self.y + self.height/2, moved up 4 pixels because the laser is 9 pixels high).
  • line 57 → adds the new laser to the Room.

Splitting instructions over several lines

Lines 54–56 are one instruction split over three lines to make it easier to read.

The Python style guide recommends lines of no more than 79 characters, so we don't have to scroll sideways to read them. We can start a new line after any , between arguments (like in the code above), and after any , in a list, dictionary or tuple. Putting a long expression inside brackets ( ) lets us start a new line after any operator too.

PRIMM

  1. Predict what will happen when you press space.
  2. Run MainController.py and press space. Then hold space down.
  3. Investigate: what happens when you hold space down? Why?

Limiting the laser

When you held down space, did you see a constant stream of lasers? Some of the other objects may have even frozen.

The Ship's key_pressed method runs every frame, so holding space spawns a laser 30 times a second. That's a lot of lasers! We need to limit how often the ship can shoot.

Planning

We'll use a flag variable to limit how often the Ship can spawn a Laser.

Flag variables

A flag variable (or Boolean flag) is a variable that's either True or False. It records the state of something in the program, like a flag that's either up or down, and our code checks it to decide what to do.

We'll create a flag called can_shoot, starting as True. A laser can only spawn when can_shoot is True. Every time a laser spawns, can_shoot is set to False and a timer starts. When the timer ends, can_shoot goes back to True.

Laser with flag IPO

Coding

This is all about spawning lasers, which happens in the Ship, so go back to Objects/Ship.py and change the highlighted code below.

Objects/Ship.py
from GameFrame import RoomObject, Globals
from Objects.Laser import Laser
import pygame

class Ship(RoomObject):
    """
    A class for the player's avatar (the Ship)
    """

    def __init__(self, room, x, y):
        """
        Initialise the Ship object
        """
        RoomObject.__init__(self, room, x, y)

        # set image
        image = self.load_image("Ship.png")
        self.set_image(image,100,100)

        # register events
        self.handle_key_events = True

        self.can_shoot = True

    def key_pressed(self, key):
        """
        Respond to keypress up and down
        """

        if key[pygame.K_w]:
            self.y_speed = -10
        elif key[pygame.K_s]:
            self.y_speed = 10
        if key[pygame.K_SPACE]:
            self.shoot_laser()

    def keep_in_room(self):
        """
        Keeps the ship inside the room
        """
        if self.y < 0:
            self.y = 0
        elif self.y + self.height > Globals.SCREEN_HEIGHT:
            self.y = Globals.SCREEN_HEIGHT - self.height

    def step(self):
        """
        Determine what happens to the Ship on each tick of the game clock
        """
        self.keep_in_room()

    def shoot_laser(self):
        """
        Shoots a laser from the ship
        """
        if self.can_shoot:
            new_laser = Laser(self.room, 
                            self.x + self.width, 
                            self.y + self.height/2 - 4)
            self.room.add_room_object(new_laser)
            self.can_shoot = False
            self.set_timer(10,self.reset_shot)

    def reset_shot(self):
        """
        Allows ship to shoot again
        """
        self.can_shoot = True
Code explanation
  • line 23 → creates the can_shoot flag and sets it to True, so the ship can shoot when the game starts.
  • line 56 → checks if the ship is allowed to shoot.
  • lines 57–60 → create and add the laser like before. They haven't changed, except they're indented one more level so they're inside the if.
  • line 61 → sets can_shoot to False, so no more lasers can spawn yet.
  • line 62 → starts a 10-tick timer (one third of a second) that calls reset_shot when it finishes.
  • line 64 → defines the reset_shot method, which the timer calls.
  • lines 65–67 → a docstring that explains what the method does.
  • line 68 → sets can_shoot back to True, so the ship can shoot again.

PRIMM

  1. Predict what will happen when you hold space down now.
  2. Run MainController.py and hold space down.
  3. Investigate: change the 10 on line 62 to 3, then to 30. How does it change the game? Choose the value you like best.

Commit and push

  1. In GitHub Desktop, type Added laser in the Summary box.
  2. Click Commit to main.
  3. Click Push origin.