Balloon Ninja: Start Screen and Game Over

Back to Tutorial Contents

Start Screen

In the last section we added some game logic to Balloon Ninja, which made it more interesting to play. Now let’s add a start button, so the game does not start as soon as we open the game.

In pygame, a button can be thought of like any other object on the screen. So we will make a file Button.py, which is similar to Balloon.py, Scoreboard.py, and Sword.py:

import pygame.font
from pygame.sprite import Sprite

class Button(Sprite):

    def __init__(self, screen, x_pos, y_pos, settings, msg):

        Sprite.__init__(self)
        self.screen = screen
        self.settings = settings

        # Set dimensions and properties of button
        self.width, self.height = settings.button_width, settings.button_height
        self.x_position, self.y_position = x_pos, y_pos
        self.rect = pygame.Rect(x_pos, y_pos, self.width, self.height)
        self.font = pygame.font.SysFont(settings.button_font, settings.button_font_size)
        self.msg = msg

        # Button message only needs to be prepped once, not on every blit
        self.prep_msg()

    def prep_msg(self):
        # Turn msg into image that can be rendered
        self.msg_image = self.font.render(self.msg, True, self.settings.button_text_color)
        # Determine offset to center text on button
        self.msg_x = self.x_position + (self.width - self.msg_image.get_width()) / 2
        self.msg_y = self.y_position + (self.height - self.msg_image.get_height()) / 2

    def blitme(self):
        # Draw blank button, and draw message
        self.screen.fill(self.settings.button_bg, self.rect)
        self.screen.blit(self.msg_image, (self.msg_x, self.msg_y))

On line 1 we import pygame.font so that we can add some text to our button. On line 6 we specify the parameters that are needed to make a button. A button needs to know what screen it will be drawn on, and it needs to know where it will be drawn. It needs access to the overall game settings, and it needs to know what text to display on the button. The rest of this file is similar to Scoreboard.py, except that the call to prep_msg() only needs to be done once. Since the message on our button never changes, we don’t need to process the text into an image every time the button is blitted.

We need to add some settings specific to our buttons:

class Settings():

    def __init__(self):
        # screen parameters
        self.screen_width, self.screen_height = 800, 600
        self.bg_color = 200, 200, 200
        self.scoreboard_height = 50

        self.button_width, self.button_height = 250, 50
        self.button_bg = (0,163,0)
        self.button_text_color = (235,235,235)
        self.button_font, self.button_font_size = 'Arial', 24

        self.initialize_game_parameters()

    def initialize_game_parameters(self):
        # game play parameters
        self.balloon_speed = 0.1
        # How quickly the speed of balloons rises
        #  ~1.05 during testing and ~1.01 for actual play
        self.speed_increase_factor = 1.05
        self.points_per_balloon = 10

        # Number of balloons to release in a spawning:
        self.batch_size = 1
        # Number of balloons that need to be popped before increasing batch_size
        #  For actual play, probably want ~10; for testing, ~3
        self.pops_needed = 3

We are setting things up so that we will easily be able to add more buttons. We include settings that cover a buttons width, height, background color, text color, font, and font size. These can be overridden for individual buttons, but for now we have an easy way to play around with the overall look and feel of any buttons we decide to implement.

Finally, we need to create the actual button in our main file:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard
from Button import Button

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)
    play_button = Button(screen, settings.screen_width/2-settings.button_width/2,
                            settings.screen_height/2-settings.button_height/2, settings, "Play Balloon Ninja")

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(sword, mouse_x, mouse_y)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        # Update the sword's position and check for popped or disappeared balloons
        update_sword(sword, mouse_x, mouse_y, settings)
        check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

        # If all balloons have disappeared, either through popping or rising,
        #  release a new batch of balloons.
        #  Increase the balloon speed for each new batch of balloons.
        if len(balloons) == 0:
            settings.balloon_speed *= settings.speed_increase_factor
            release_batch(screen, settings, balloons)

        # Display updated scoreboard
        scoreboard.blitme()

        # Show play button
        play_button.blitme()

        # Show the redrawn screen
        pygame.display.flip()

def release_batch(screen, settings, balloons):
    for x in range(0, settings.batch_size):
        spawn_balloon(screen, settings, balloons)

def check_balloons(balloons, sword, scoreboard, screen, settings, time_passed):
    # Find any balloons that have been popped,
    #  or have disappeared off the top of the screen
    for balloon in balloons:
        balloon.update(time_passed)

        if balloon.rect.colliderect(sword.rect):
            pop_balloon(scoreboard, settings, balloon, balloons)
            continue

        if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
            miss_balloon(scoreboard, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        balloon.blitme()

def update_sword(sword, mouse_x, mouse_y, settings):
    # Update the sword's position, and draw the sword on the screen
    sword.x_position = mouse_x
    if sword.grabbed:
        sword.y_position = mouse_y
    else:
        sword.y_position = sword.image_h/2 + settings.scoreboard_height
    sword.update_rect()
    sword.blitme()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # If we have popped enough balloons, increase batch_size:
    if scoreboard.balloons_popped % settings.pops_needed == 0:
        settings.batch_size += 1

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings))

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

On line 6, we import our new Button. On lines 17 and 18, we create our play button. We want to center the button at first, so its x-position is half of the screen width, minus half the button width. The y-position is calculated similarly. For the moment, we just want to display our button and make sure it looks reasonable. So we blit the button on line 52, just after the scoreboard, and just before we flip the screen. Here is what we see when we run the game:

The play button appears at the center of the screen, but it doesn't do anything yet.

The play button appears at the center of the screen, but it doesn’t do anything yet.

Now we want to make sure the game does not start until we click the button. We start out by creating a variable in Settings.py called game_active, which will keep track of whether a game is currently being played:

class Settings():

    def __init__(self):
        # screen parameters
        self.screen_width, self.screen_height = 800, 600
        self.bg_color = 200, 200, 200
        self.scoreboard_height = 50

        self.button_width, self.button_height = 250, 50
        self.button_bg = (0,163,0)
        self.button_text_color = (235,235,235)
        self.button_font, self.button_font_size = 'Arial', 24

        # game status
        self.game_active = False

        self.initialize_game_parameters()

    def initialize_game_parameters(self):
        # game play parameters
        self.balloon_speed = 0.1
        # How quickly the speed of balloons rises
        #  ~1.05 during testing and ~1.01 for actual play
        self.speed_increase_factor = 1.05
        self.points_per_balloon = 10

        # Number of balloons to release in a spawning:
        self.batch_size = 1
        # Number of balloons that need to be popped before increasing batch_size
        #  For actual play, probably want ~10; for testing, ~3
        self.pops_needed = 3

We initialize this variable to False, so the game will not run immediately when the program loads. Now we need to modify our main file to use this setting:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard
from Button import Button

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)
    play_button = Button(screen, settings.screen_width/2-settings.button_width/2,
                            settings.screen_height/2-settings.button_height/2, settings, "Play Balloon Ninja")

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(settings, sword, play_button, mouse_x, mouse_y)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        if settings.game_active:
            # Update the sword's position and check for popped or disappeared balloons
            update_sword(sword, mouse_x, mouse_y, settings)
            check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

            # If all balloons have disappeared, either through popping or rising,
            #  release a new batch of balloons.
            #  Increase the balloon speed for each new batch of balloons.
            if len(balloons) == 0:
                settings.balloon_speed *= settings.speed_increase_factor
                release_batch(screen, settings, balloons)
        else:
            # Show play button
            play_button.blitme()

        # Display updated scoreboard
        scoreboard.blitme()

        # Show the redrawn screen
        pygame.display.flip()

def release_batch(screen, settings, balloons):
    for x in range(0, settings.batch_size):
        spawn_balloon(screen, settings, balloons)

def check_balloons(balloons, sword, scoreboard, screen, settings, time_passed):
    # Find any balloons that have been popped,
    #  or have disappeared off the top of the screen
    for balloon in balloons:
        balloon.update(time_passed)

        if balloon.rect.colliderect(sword.rect):
            pop_balloon(scoreboard, settings, balloon, balloons)
            continue

        if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
            miss_balloon(scoreboard, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        balloon.blitme()

def update_sword(sword, mouse_x, mouse_y, settings):
    # Update the sword's position, and draw the sword on the screen
    sword.x_position = mouse_x
    if sword.grabbed:
        sword.y_position = mouse_y
    else:
        sword.y_position = sword.image_h/2 + settings.scoreboard_height
    sword.update_rect()
    sword.blitme()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # If we have popped enough balloons, increase batch_size:
    if scoreboard.balloons_popped % settings.pops_needed == 0:
        settings.batch_size += 1

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings))

def check_events(settings, sword, play_button, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
            if play_button.rect.collidepoint(mouse_x, mouse_y):
                settings.game_active = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

We have been calling check_events on line 32. This function now needs to watch for clicks on the play button, so it needs access to settings and to play_button. On line 104 the check_events function needs to accept these parameters. When the mouse button is pressed, we need to check whether the play button has been clicked. Line 111 makes this check, and if the button is pressed line 112 sets game_active to True.

On lines 37-50 we make sure the code that actually runs the game only executes when a game is active. We need the rest of the code in the main event loop to keep running, so that we can monitor mouse clicks and keep the screen refreshed. We move the code that displays the play button into an else clause, so it only appears when game_active is False.

The game doesn’t look much different now, but when you run the game it doesn’t start until you press play. This is better, but the game still runs forever once you press play.

Game Over

With our game_active flag, we can now easily decide when a game is over. To start out, let’s make the game stop when three balloons have been missed. The basic implementation of this only takes 2 new lines in our main file:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard
from Button import Button

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)
    play_button = Button(screen, settings.screen_width/2-settings.button_width/2,
                            settings.screen_height/2-settings.button_height/2, settings, "Play Balloon Ninja")

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(settings, sword, play_button, mouse_x, mouse_y)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        if settings.game_active:
            # Update the sword's position and check for popped or disappeared balloons
            update_sword(sword, mouse_x, mouse_y, settings)
            check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

            # If all balloons have disappeared, either through popping or rising,
            #  release a new batch of balloons.
            #  Increase the balloon speed for each new batch of balloons.
            if len(balloons) == 0:
                settings.balloon_speed *= settings.speed_increase_factor
                release_batch(screen, settings, balloons)
        else:
            # Show play button
            play_button.blitme()

        # Display updated scoreboard
        scoreboard.blitme()

        # Show the redrawn screen
        pygame.display.flip()

def release_batch(screen, settings, balloons):
    for x in range(0, settings.batch_size):
        spawn_balloon(screen, settings, balloons)

def check_balloons(balloons, sword, scoreboard, screen, settings, time_passed):
    # Find any balloons that have been popped,
    #  or have disappeared off the top of the screen
    for balloon in balloons:
        balloon.update(time_passed)

        if balloon.rect.colliderect(sword.rect):
            pop_balloon(scoreboard, settings, balloon, balloons)
            continue

        if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
            miss_balloon(scoreboard, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        if scoreboard.balloons_missed > 3:
            settings.game_active = False

        balloon.blitme()

def update_sword(sword, mouse_x, mouse_y, settings):
    # Update the sword's position, and draw the sword on the screen
    sword.x_position = mouse_x
    if sword.grabbed:
        sword.y_position = mouse_y
    else:
        sword.y_position = sword.image_h/2 + settings.scoreboard_height
    sword.update_rect()
    sword.blitme()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # If we have popped enough balloons, increase batch_size:
    if scoreboard.balloons_popped % settings.pops_needed == 0:
        settings.batch_size += 1

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings))

def check_events(settings, sword, play_button, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
            if play_button.rect.collidepoint(mouse_x, mouse_y):
                settings.game_active = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

In the function check_balloons, we just add a quick check to see if the number of balloons missed has exceeded 3. If it has, we change game_active to False. This works to stop the game, but there are some problems. We don’t see any message that the game is over, and we can’t restart the game because balloons_missed has not been reset.

Let’s make a few changes to clean up these issues. We will move the number of balloons that can be missed to settings, create a “Game Over” button, and reset game stats when the user presses the play button. Let’s deal with settings first:

class Settings():

    def __init__(self):
        # screen parameters
        self.screen_width, self.screen_height = 800, 600
        self.bg_color = 200, 200, 200
        self.scoreboard_height = 50

        self.button_width, self.button_height = 250, 50
        self.button_bg = (0,163,0)
        self.button_text_color = (235,235,235)
        self.button_font, self.button_font_size = 'Arial', 24

        # game status
        self.game_active = False

        # game over conditions
        self.misses_allowed = 3
        self.games_played = 0

        self.initialize_game_parameters()

    def initialize_game_parameters(self):
        # game play parameters
        self.balloon_speed = 0.1
        # How quickly the speed of balloons rises
        #  ~1.05 during testing and ~1.01 for actual play
        self.speed_increase_factor = 1.05
        self.points_per_balloon = 10

        # Number of balloons to release in a spawning:
        self.batch_size = 1
        # Number of balloons that need to be popped before increasing batch_size
        #  For actual play, probably want ~10; for testing, ~3
        self.pops_needed = 3

On line 18 we set the number of balloons the player is allowed to miss. On line 19, we start keeping track of the number of games that have been played. This will let us only display the “Game Over” button once a game has been completed.

Now let’s modify our main file to make a Game Over button, and to use our settings to determine when a game is over:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard
from Button import Button

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)
    play_button = Button(screen, settings.screen_width/2-settings.button_width/2,
                            settings.screen_height/2-settings.button_height/2, settings, "Play Balloon Ninja")
    game_over_button = Button(screen, play_button.x_position, play_button.y_position-2*settings.button_height,
                            settings, "Game Over")

    # Create a list to hold our balloons, and create our first balloon
    balloons = []

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(settings, scoreboard, sword, play_button, mouse_x, mouse_y, balloons)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        if settings.game_active:
            # Update the sword's position and check for popped or disappeared balloons
            update_sword(sword, mouse_x, mouse_y, settings)
            check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

            # If all balloons have disappeared, either through popping or rising,
            #  release a new batch of balloons.
            #  Increase the balloon speed for each new batch of balloons.
            if len(balloons) == 0:
                settings.balloon_speed *= settings.speed_increase_factor
                release_batch(screen, settings, balloons)
        else:
            # Show play button
            play_button.blitme()
            # If a game has just ended, show Game Over button
            if settings.games_played > 0:
                game_over_button.blitme()

        # Display updated scoreboard
        scoreboard.blitme()

        # Show the redrawn screen
        pygame.display.flip()

def release_batch(screen, settings, balloons):
    for x in range(0, settings.batch_size):
        spawn_balloon(screen, settings, balloons)

def check_balloons(balloons, sword, scoreboard, screen, settings, time_passed):
    # Find any balloons that have been popped,
    #  or have disappeared off the top of the screen
    for balloon in balloons:
        balloon.update(time_passed)

        if balloon.rect.colliderect(sword.rect):
            pop_balloon(scoreboard, settings, balloon, balloons)
            continue

        if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
            miss_balloon(scoreboard, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        balloon.blitme()

    if scoreboard.balloons_missed > settings.misses_allowed:
        # Set game_active to false, empty the list of balloons, and increment games_played
        settings.game_active = False
        settings.games_played += 1

def update_sword(sword, mouse_x, mouse_y, settings):
    # Update the sword's position, and draw the sword on the screen
    sword.x_position = mouse_x
    if sword.grabbed:
        sword.y_position = mouse_y
    else:
        sword.y_position = sword.image_h/2 + settings.scoreboard_height
    sword.update_rect()
    sword.blitme()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # If we have popped enough balloons, increase batch_size:
    if scoreboard.balloons_popped % settings.pops_needed == 0:
        settings.batch_size += 1

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings))

def check_events(settings, scoreboard, sword, play_button, mouse_x, mouse_y, balloons):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
            if play_button.rect.collidepoint(mouse_x, mouse_y):
                # Play button has been pressed.  Empty list of balloons,
                #  initialize scoreboard and game parameters, and make game active.
                del balloons[:]
                scoreboard.initialize_stats()
                settings.initialize_game_parameters()
                settings.game_active = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

There are a lot of changes here, so let’s go through them one at a time. (Then we’ll do some code cleanup, as our main file is getting pretty long.) On lines 19 and 20 we create our Game Over button. It has the same x-position as the Play button, but its y-position is moved upwards by twice the height of a button. Lines 22 and 23 are highlighted because we no longer want to spawn a balloon as soon as the program starts. That is now taken care of within the main loop, after the play button has been pressed. So we just create an empty list of balloons here.

On line 33, the call to check_events now needs the list of balloons. Let’s look at the actual function to see why. On line 113 we need to make sure the function receives the list of balloons. Once the player clicks the play button, we need to clear the existing list of balloons. Line 123 does this. Then we need to reset the game stats, such as the number of balloons popped and missed. We don’t do this as soon as a game is over, because we want those stats to be displayed while the Game Over message is being displayed. Finally, we set the game_active status to True, which will activate the game on the next pass through the main loop.

Lines 53 and 54 make sure the Game Over button is displayed when game_active is False. Remember that game_active starts out as False, so we only want to display this button if the player has already completed at least one game. It would be awkward to see a Game Over message as soon as you start the program.

Finally, line 83 uses the maximum number of balloons specified in settings instead of a hard-coded 3. It also increments the number of games played as soon as a game is completed.

It’s time for an engine

There are two more things we’d like to do in this section of the tutorial, but we need to do another round of code cleanup first. Our main file is now 130 lines, and just getting longer. Most of this file length comes from the series of functions we have outside of the main loop. Let’s move those functions to a file called Engine.py (our “game engine”). We are going to copy and paste most of these functions to our new file. For the purposes of the tutorial, I will show our current version of balloon_ninja.py, with the functions we want to move to Engine.py highlighted:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard
from Button import Button

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)
    play_button = Button(screen, settings.screen_width/2-settings.button_width/2,
                            settings.screen_height/2-settings.button_height/2, settings, "Play Balloon Ninja")
    game_over_button = Button(screen, play_button.x_position, play_button.y_position-2*settings.button_height,
                            settings, "Game Over")

    # Create a list to hold our balloons, and create our first balloon
    balloons = []

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(settings, scoreboard, sword, play_button, mouse_x, mouse_y, balloons)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        if settings.game_active:
            # Update the sword's position and check for popped or disappeared balloons
            update_sword(sword, mouse_x, mouse_y, settings)
            check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

            # If all balloons have disappeared, either through popping or rising,
            #  release a new batch of balloons.
            #  Increase the balloon speed for each new batch of balloons.
            if len(balloons) == 0:
                settings.balloon_speed *= settings.speed_increase_factor
                release_batch(screen, settings, balloons)
        else:
            # Show play button
            play_button.blitme()
            # If a game has just ended, show Game Over button
            if settings.games_played > 0:
                game_over_button.blitme()

        # Display updated scoreboard
        scoreboard.blitme()

        # Show the redrawn screen
        pygame.display.flip()

def release_batch(screen, settings, balloons):
    for x in range(0, settings.batch_size):
        spawn_balloon(screen, settings, balloons)

def check_balloons(balloons, sword, scoreboard, screen, settings, time_passed):
    # Find any balloons that have been popped,
    #  or have disappeared off the top of the screen
    for balloon in balloons:
        balloon.update(time_passed)

        if balloon.rect.colliderect(sword.rect):
            pop_balloon(scoreboard, settings, balloon, balloons)
            continue

        if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
            miss_balloon(scoreboard, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        balloon.blitme()

    if scoreboard.balloons_missed > settings.misses_allowed:
        # Set game_active to false, empty the list of balloons, and increment games_played
        settings.game_active = False
        settings.games_played += 1

def update_sword(sword, mouse_x, mouse_y, settings):
    # Update the sword's position, and draw the sword on the screen
    sword.x_position = mouse_x
    if sword.grabbed:
        sword.y_position = mouse_y
    else:
        sword.y_position = sword.image_h/2 + settings.scoreboard_height
    sword.update_rect()
    sword.blitme()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # If we have popped enough balloons, increase batch_size:
    if scoreboard.balloons_popped % settings.pops_needed == 0:
        settings.batch_size += 1

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings))

def check_events(settings, scoreboard, sword, play_button, mouse_x, mouse_y, balloons):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
            if play_button.rect.collidepoint(mouse_x, mouse_y):
                # Play button has been pressed.  Empty list of balloons,
                #  initialize scoreboard and game parameters, and make game active.
                del balloons[:]
                scoreboard.initialize_stats()
                settings.initialize_game_parameters()
                settings.game_active = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

We will copy these functions into a new file called Engine.py. Once copied into the new file, we need to make a few changes to coordinate how the functions are called:

import pygame, sys
from Balloon import Balloon

class Engine():

    def __init__(self):
        pass

    def release_batch(self, screen, settings, balloons):
        for x in range(0, settings.batch_size):
            self.spawn_balloon(screen, settings, balloons)

    def check_balloons(self, balloons, sword, scoreboard, screen, settings, time_passed):
        # Find any balloons that have been popped,
        #  or have disappeared off the top of the screen
        for balloon in balloons:
            balloon.update(time_passed)

            if balloon.rect.colliderect(sword.rect):
                self.pop_balloon(scoreboard, settings, balloon, balloons)
                continue

            if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
                self.miss_balloon(scoreboard, balloon, balloons)
                self.spawn_balloon(screen, settings, balloons)
                continue

            balloon.blitme()

        if scoreboard.balloons_missed > settings.misses_allowed:
            # Set game_active to false, empty the list of balloons, and increment games_played
            settings.game_active = False
            settings.games_played += 1

    def update_sword(self, sword, mouse_x, mouse_y, settings):
        # Update the sword's position, and draw the sword on the screen
        sword.x_position = mouse_x
        if sword.grabbed:
            sword.y_position = mouse_y
        else:
            sword.y_position = sword.image_h/2 + settings.scoreboard_height
        sword.update_rect()
        sword.blitme()

    def miss_balloon(self, scoreboard, balloon, balloons):
        scoreboard.balloons_missed += 1
        balloons.remove(balloon)

    def pop_balloon(self, scoreboard, settings, balloon, balloons):
        scoreboard.balloons_popped += 1
        scoreboard.score += settings.points_per_balloon
        balloons.remove(balloon)
        # If we have popped enough balloons, increase batch_size:
        if scoreboard.balloons_popped % settings.pops_needed == 0:
            settings.batch_size += 1

    def spawn_balloon(self, screen, settings, balloons):
        balloons.append(Balloon(screen, settings))

    def check_events(self, settings, scoreboard, sword, play_button, mouse_x, mouse_y, balloons):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            if event.type == pygame.MOUSEBUTTONDOWN:
                if sword.rect.collidepoint(mouse_x, mouse_y):
                    sword.grabbed = True
                if play_button.rect.collidepoint(mouse_x, mouse_y):
                    # Play button has been pressed.  Empty list of balloons,
                    #  initialize scoreboard and game parameters, and make game active.
                    del balloons[:]
                    scoreboard.initialize_stats()
                    settings.initialize_game_parameters()
                    settings.game_active = True
            if event.type == pygame.MOUSEBUTTONUP:
                sword.grabbed = False

On lines 1 and 2 the Engine class needs access to pygame and sys, and it needs to know how to make a Balloon. On line 4 we define our new class, and on lines 6 and 7 we have an empty __init__ function. For now, we don’t need to do anything specific when we create an engine, since the class is just a collection of functions right now. Since we are structuring this as a class, every line that defines a function needs the self variable passed to it as the first parameter. After that is taken care of, we need to modify any line that includes a call to an engine function, such as line 20. These calls now need to specify that they are calling a function within this class, which reads as self.pop_balloon(…) instead of just pop_balloon(…).

Finally, we need to modify balloon_ninja to use our new Engine. Here is the simplified version of balloon_ninja.py:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard
from Button import Button
from Engine import Engine

def run_game():
    # Get access to our game settings
    settings = Settings()
    engine = Engine()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)
    play_button = Button(screen, settings.screen_width/2-settings.button_width/2,
                            settings.screen_height/2-settings.button_height/2, settings, "Play Balloon Ninja")
    game_over_button = Button(screen, play_button.x_position, play_button.y_position-2*settings.button_height,
                            settings, "Game Over")

    # Create a list to hold our balloons, and create our first balloon
    balloons = []

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        engine.check_events(settings, scoreboard, sword, play_button, mouse_x, mouse_y, balloons)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        if settings.game_active:
            # Update the sword's position and check for popped or disappeared balloons
            engine.update_sword(sword, mouse_x, mouse_y, settings)
            engine.check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

            # If all balloons have disappeared, either through popping or rising,
            #  release a new batch of balloons.
            #  Increase the balloon speed for each new batch of balloons.
            if len(balloons) == 0:
                settings.balloon_speed *= settings.speed_increase_factor
                engine.release_batch(screen, settings, balloons)
        else:
            # Show play button
            play_button.blitme()
            # If a game has just ended, show Game Over button
            if settings.games_played > 0:
                game_over_button.blitme()

        # Display updated scoreboard
        scoreboard.blitme()

        # Show the redrawn screen
        pygame.display.flip()

run_game()

These changes are really simple. On line 7 we import the Engine class, and on line 12 we make an engine object. Lines 35,42,43, and 50 are the only lines within our main run_game function that call engine functions, so these lines need a the “engine.” prefix on those functions.

We now have a much simpler main file to work with, and our overall program is organized reasonably well.

That’s enough for this section. In the next section we will improve our game over conditions, and implement a more interesting scoring system.

Next: A better scoring system

Back to Tutorial Contents

Advertisements

About ehmatthes

Teacher, hacker, new dad, outdoor guy
This entry was posted in programming and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s