Skip to content

Subgoals

In this lesson we will learn

  • how subgoals give players short-term and optional challenges
  • how to track a streak and reset it when something goes wrong
  • how to use count_object to limit how many objects are in a Room
  • how to write a method that returns a value
  • how to make a mistake cost the player progress

Terminology

  • return value – the value a method or function sends back to the code that called it.

In Game Design we planned two subgoals for Space Rescue:

Subgoal How it works
Shoot asteroids without taking damage - count the asteroids shot in a row (the streak)
- reset the streak when the ship loses a life
- limit the number of lasers on the screen by the streak
Try not to shoot the astronauts each astronaut we shoot takes one off our rescued count

Planning

The streak

The streak is the number of asteroids shot in a row without losing a life. To make the streak worth chasing, it also controls how many lasers can be on the screen at once:

Streak Lasers on screen
0 1
1 2
2 3
3 4
4 or more 5

So the rule is: lasers = 1 + streak, but never more than 5.

Event Input Process Output
Shoot an asteroid a laser hits an asteroid add 1 to the streak, play a sound if the laser limit went up the HUD shows the new streak and laser limit
Lose a life an asteroid hits the unshielded ship reset the streak to 0 the HUD shows a streak of 0 and 1 laser
Shoot the player presses space count the lasers in the Room; only fire if there are fewer than the limit a laser fires, or nothing happens

GameFrame's count_object method counts how many objects of a class are in the Room, so self.room.count_object("Laser") tells us how many lasers are on the screen.

Shooting astronauts

Shooting an astronaut already costs 10 points. Now it will also take one off the rescued count, which pushes the player further from the goal. The count can't go below 0.


Track the streak

Open GameFrame/Globals.py, add the highlighted code below at the bottom and save it.

GameFrame/Globals.py
    # - rescue goal - #
    rescue_goal = 10
    rescued = 0

    # - asteroids shot in a row - #
    streak = 0
Code explanation
  • line 55 → creates the streak counter, starting at 0.

Now we'll add a Streak HUD item that shows the streak and the laser limit. Open Objects/Hud.py, add the highlighted code below at the bottom and save it.

Objects/Hud.py
class Streak(TextObject):
    """
    A class for showing the asteroid streak and the laser limit
    """
    def __init__(self, room, x: int, y: int):
        """
        Initialises the streak counter
        """
        TextObject.__init__(self, room, x, y)

        # set values
        self.size = 40
        self.font = 'Arial Black'
        self.colour = (255,255,255)
        self.update_streak()

    def max_lasers(self):
        """
        Returns how many lasers can be on the screen at once
        """
        return min(1 + Globals.streak, 5)

    def update_streak(self):
        """
        Shows the streak and the laser limit
        """
        self.text = f"Streak: {Globals.streak}  Lasers: {self.max_lasers()}"
        self.update_text()
Code explanation
  • line 77 → defines the Streak class as a subclass of TextObject.
  • lines 78–80 → a docstring that explains what the class is for.
  • line 81 → defines the __init__ method.
  • lines 82–84 → a docstring that explains what the method does.
  • line 85 → runs TextObject's __init__ method.
  • lines 88–90 → set the font size, font and colour.
  • line 91 → draws the text for the first time.
  • line 93 → defines the max_lasers method.
  • lines 94–96 → a docstring that explains what the method does.
  • line 97 → returns the laser limit: 1 + Globals.streak, but min picks the smaller of that and 5, so it never goes above 5.
  • line 99 → defines the update_streak method.
  • lines 100–102 → a docstring that explains what the method does.
  • line 103 → builds the text with an f-string, calling max_lasers to get the laser limit.
  • line 104 → redraws the text on the screen.

Methods that return a value

Most of our methods do something, like moving or deleting an object. max_lasers returns a value instead, so other code can ask for it: self.room.streak.max_lasers(). Keeping the rule in one method means the HUD and the Ship always agree about the limit.

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

Objects/__init__.py
1
2
3
4
5
6
7
8
9
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
from Objects.Astronaut import Astronaut
from Objects.Hud import Score, Lives, Rescued, Streak
from Objects.DifficultyMenu import DifficultyMenu
from Objects.Bonus import RepairKit, Shield
Code explanation
  • line 7 → imports the Streak class as well.

Open Rooms/GamePlay.py, add the highlighted code below and save it.

Rooms/GamePlay.py
from GameFrame import Level, Globals
from Objects.Ship import Ship
from Objects.Zork import Zork
from Objects.Hud import Score, Lives, Rescued, Streak

class GamePlay(Level):
    def __init__(self, screen, joysticks):
        Level.__init__(self, screen, joysticks)

        # set background image
        self.set_background_image("background.png")

        # add objects
        self.add_room_object(Ship(self, 25, 50))
        self.add_room_object(Zork(self,1120, 50))

        # add HUD items
        self.score = Score(self, 
                           Globals.SCREEN_WIDTH/2 - 20, 20, 
                           str(Globals.SCORE))
        self.add_room_object(self.score)
        self.lives = Lives(self, Globals.SCREEN_WIDTH - 150, 20)
        self.add_room_object(self.lives)
        self.rescued = Rescued(self, 20, Globals.SCREEN_HEIGHT - 60)
        self.add_room_object(self.rescued)
        self.streak = Streak(self, Globals.SCREEN_WIDTH - 480, Globals.SCREEN_HEIGHT - 60)
        self.add_room_object(self.streak)

        # load sound files
        self.shoot_laser = self.load_sound("Laser_shot.ogg")
        self.asteroid_shot = self.load_sound("Asteroid_shot.wav")
        self.astronaut_saved = self.load_sound("Astronaut_saved.ogg")
        self.asteroid_collision = self.load_sound("Ship_damage.ogg")
        self.astronaut_shot = self.load_sound("Astronaut_hit.ogg")
        self.goal_reached = self.load_sound("Bonus_score.mp3")
        self.life_gained = self.load_sound("Life_increase.ogg")
        self.shield_up = self.load_sound("Shields.ogg")
        self.max_shot = self.load_sound("Max_shot_increase.wav")
Code explanation
  • line 4 → imports the Streak class.
  • line 26 → creates a Streak counter in the bottom-right corner, and stores it in self.streak so other objects can use it.
  • line 27 → adds the counter to the Room.
  • line 38 → loads the sound for the laser limit going up.

Use the streak

Limit the lasers

Open Objects/Ship.py, change the highlighted code below in shoot_laser and save it.

Objects/Ship.py
    def shoot_laser(self):
        """
        Shoots a laser from the ship
        """
        max_lasers = self.room.streak.max_lasers()
        if self.can_shoot and self.room.count_object("Laser") < max_lasers:
            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)
            self.room.shoot_laser.play()
Code explanation
  • line 60 → asks the Streak counter for the current laser limit.
  • line 61 → only fires if the ship can shoot and there are fewer lasers in the Room than the limit.

Build the streak

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

Objects/Laser.py
    # --- Event handlers
    def handle_collision(self, other, other_type):
        """
        Handles laser collisions with other registered objects
        """
        if other_type == "Asteroid":
            self.room.asteroid_shot.play()
            self.room.delete_object(other)
            self.room.score.update_score(5)
            Globals.streak += 1
            self.room.streak.update_streak()
            if Globals.streak <= 4:
                self.room.max_shot.play()
        elif other_type == "Astronaut":
            self.room.astronaut_shot.play()
            self.room.delete_object(other)
            self.room.score.update_score(-10)
        self.room.delete_object(self)
Code explanation
  • line 48 → adds 1 to the streak when a laser hits an asteroid.
  • line 49 → updates the Streak counter on the screen.
  • line 50 → checks if the streak is 4 or less, which means the laser limit just went up…
  • line 51 → …and plays a sound to tell the player.

Lose the streak

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

Objects/Asteroid.py
        if other_type == "Ship":
            self.room.delete_object(self)
            if other.shielded:
                self.room.asteroid_shot.play()
            else:
                self.room.asteroid_collision.play()
                Globals.LIVES -= 1
                Globals.streak = 0
                self.room.streak.update_streak()
                if Globals.LIVES > 0:
                    self.room.lives.update_image()
                else:
                    self.room.running = False
Code explanation
  • line 65 → resets the streak to 0 when the ship loses a life.
  • line 66 → updates the Streak counter on the screen.

PRIMM

  1. Predict how many lasers you can fire at the start of a game, and after shooting three asteroids.
  2. Run MainController.py and test it. Then let an asteroid hit you.
  3. Investigate: what happens to the streak when an asteroid hits you while you're shielded? Why?

Don't shoot the astronauts

Go back to Objects/Laser.py, add the highlighted code below and save it.

Objects/Laser.py
        elif other_type == "Astronaut":
            self.room.astronaut_shot.play()
            self.room.delete_object(other)
            self.room.score.update_score(-10)
            if Globals.rescued > 0:
                Globals.rescued -= 1
                self.room.rescued.update_rescued()
        self.room.delete_object(self)
Code explanation
  • line 56 → checks that at least one astronaut has been rescued, so the count can't go below 0…
  • line 57 → …takes one off the rescued count…
  • line 58 → …and updates the Rescued counter on the screen.

Finally, the streak needs to start at 0 in each new game. Open Objects/Title.py, add the highlighted code below and save it.

Objects/Title.py
        if key[pygame.K_SPACE]:
            # reset the score and lives for a new game
            Globals.SCORE = 0
            Globals.LIVES = 3
            Globals.rescued = 0
            Globals.streak = 0
            self.room.running = False
Code explanation
  • line 28 → resets the streak when a new game starts.

PRIMM

  1. Predict what will happen to the Rescued counter when you shoot an astronaut.
  2. Run MainController.py and test both subgoals with a testing table.
  3. Modify: is a maximum of 5 lasers right? Try changing the rule in max_lasers.

Commit and push

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