Alien Invasion: Finishing the Game
- 14 SCORING: In this chapter, Alien Invasion will be completed by adding a Play button to start/restart the game, increasing game speed with levels, and implementing a scoring system.
Adding the Play Button
- A Play button will appear before the game starts and after it ends, allowing the player to start a new game.
- Currently, the game starts immediately when
alien_invasion.pyis run. - Modify the
__init__()method inAlienInvasionto start the game in an inactive state:
def __init__(self):
"""Initialize the game, and create game resources."""
pygame.init()
--snip--
# Start Alien Invasion in an inactive state.
self.game_active = False
Creating a Button Class
- Since Pygame doesn't have a built-in button method, a
Buttonclass will be created to make a filled rectangle with a label. - Save as
button.py:
import pygame.font
class Button:
"""A class to build buttons for the game."""
def __init__(self, ai_game, msg):
"""Initialize button attributes."""
self.screen = ai_game.screen
self.screen_rect = self.screen.get_rect()
# Set the dimensions and properties of the button.
self.width, self.height = 200, 50
self.button_color = (0, 135, 0)
self.text_color = (255, 255, 255)
self.font = pygame.font.SysFont(None, 48)
# Build the button's rect object and center it.
self.rect = pygame.Rect(0, 0, self.width, self.height)
self.rect.center = self.screen_rect.center
# The button message needs to be prepped only once.
self._prep_msg(msg)
Imports the
pygame.fontmodule for rendering text.The
__init__()method takesself,ai_gameobject, andmsg(button text).Sets button dimensions, color, text color, and font attributes.
Centers the button by creating a rect and setting its center attribute to match the screen's center.
Calls
_prep_msg()to render the text as an image.- Code for
_prep_msg():
- Code for
def _prep_msg(self, msg):
"""Turn msg into a rendered image and center text on the button."""
self.msg_image = self.font.render(msg, True, self.text_color,
self.button_color)
self.msg_image_rect = self.msg_image.get_rect()
self.msg_image_rect.center = self.rect.center
Takes
selfandmsgparameters.font.render()turns the text into an image, stored inself.msg_image.Antialiasing is turned on for smoother text edges.
Sets the text background to the same color as the button.
Centers the text image on the button by creating a rect from the image and setting its center attribute to match that of the button.
- Code for
draw_button():
- Code for
def draw_button(self):
"""Draw blank button and then draw message."""
self.screen.fill(self.button_color, self.rect)
self.screen.blit(self.msg_image, self.msg_image_rect)
screen.fill()draws the rectangular portion of the button.screen.blit()draws the text image to the screen.
Drawing the Button to the Screen
- Update import statements in
alien_invasion.py:
--snip--
from game_stats import GameStats
from button import Button
- Create the button instance in
__init__():
def __init__(self):
--snip--
self.game_active = False
# Make the Play button.
self.play_button = Button(self, "Play")
- Call
draw_button()in_update_screen():
def _update_screen(self):
--snip--
self.aliens.draw(self.screen)
# Draw the play button if the game is inactive.
if not self.game_active:
self.play_button.draw_button()
pygame.display.flip()
Starting the Game
- Add an
elifblock to_check_events()to monitor mouse events over the button:
def _check_events(self):
"""Respond to keypresses and mouse events."""
for event in pygame.event.get():
if event.type == pygame.QUIT:
--snip--
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pos = pygame.mouse.get_pos()
self._check_play_button(mouse_pos)
pygame.mouse.get_pos()returns a tuple with the x- and y-coordinates of the mouse cursor when clicked.These coordinates are sent to
_check_play_button().- Code for
_check_play_button():
- Code for
def _check_play_button(self, mouse_pos):
"""Start a new game when the player clicks Play."""
if self.play_button.rect.collidepoint(mouse_pos):
self.game_active = True
collidepoint()checks if the mouse click overlaps the Play button's rect.- If so,
game_activeis set toTrue, and the game begins.
Resetting the Game
- To reset the game statistics, clear out the old aliens and bullets, build a new fleet, and center the ship, include this code in
_check_play_button():
def _check_play_button(self, mouse_pos):
"""Start a new game when the player clicks Play."""
if self.play_button.rect.collidepoint(mouse_pos):
# Reset the game statistics.
self.stats.reset_stats()
self.game_active = True
# Get rid of any remaining bullets and aliens.
self.bullets.empty()
self.aliens.empty()
# Create a new fleet and center the ship.
self._create_fleet()
self.ship.center_ship()
- Resets game statistics by calling self.stats.reset_stats().
- Empties existing bullets and aliens groups.
- Creates a new fleet and centers the ship.
Deactivating the Play Button
- To ensure the Play button region only responds to clicks when the Play button is visible, set the game to start only when
game_activeisFalse:
def _check_play_button(self, mouse_pos):
"""Start a new game when the player clicks Play."""
button_clicked = self.play_button.rect.collidepoint(mouse_pos)
if button_clicked and not self.game_active:
# Reset the game statistics.
self.stats.reset_stats()
--snip--
- The game restarts only if Play is clicked and the game is not currently active.
Hiding the Mouse Cursor
- Make the mouse cursor invisible when the game becomes active by editing the
_check_play_button()definition:
def _check_play_button(self, mouse_pos):
"""Start a new game when the player clicks Play."""
button_clicked = self.play_button.rect.collidepoint(mouse_pos)
if button_clicked and not self.game_active:
--snip--
# Hide the mouse cursor.
pygame.mouse.set_visible(False)
- Make the cursor reappear once the game ends:
def _ship_hit(self):
"""Respond to ship being hit by alien."""
if self.stats.ships_left > 0:
--snip--
else:
self.game_active = False
pygame.mouse.set_visible(True)
Leveling Up
- The game's speed will increase each time the player clears the screen.
Modifying the Speed Settings
Reorganize the Settings class to group the game settings into static and dynamic ones.
Ensure any settings that change during the game reset when a new game starts.
__init__()method forsettings.py:
def __init__(self):
"""Initialize the game's static settings."""
# Screen settings
self.screen_width = 1200
self.screen_height = 800
self.bg_color = (230, 230, 230)
# Ship settings
self.ship_limit = 3
# Bullet settings
self.bullet_width = 3
self.bullet_height = 15
self.bullet_color = 60, 60, 60
self.bullets_allowed = 3
# Alien settings
self.fleet_drop_speed = 10
# How quickly the game speeds up
self.speedup_scale = 1.1
self.initialize_dynamic_settings()
Initialize Dynamic Settings
Add a speedup_scale setting to control how quickly the game speeds up.
- A value of 2 will double the game speed every time the player reaches a new level.
- A value of 1 will keep the speed constant.
- A value like 1.1 should increase the speed enough to make the game challenging but not impossible.
Call the
initialize_dynamic_settings()method to initialize the values for attributes that need to change throughout the game.Code for
initialize_dynamic_settings():
def initialize_dynamic_settings(self):
"""Initialize settings that change throughout the game."""
self.ship_speed = 1.5
self.bullet_speed = 2.5
self.alien_speed = 1.0
# fleet_direction of 1 represents right; -1 represents left.
self.fleet_direction = 1
This method sets the initial values for the ship, bullet, and alien speeds.
We'll increase these speeds as the player progresses in the game and reset them each time the player starts a new game.
We include fleet_direction in this method so the aliens always move right at the beginning of a new game.
We don't need to increase the value of fleetdropspeed, because when the aliens move faster across the screen, they'll also come down the screen faster.
- Code for
increase_speed():
- Code for
def increase_speed(self):
"""Increase speed settings."""
self.ship_speed *= self.speedup_scale
self.bullet_speed *= self.speedup_scale
self.alien_speed *= self.speedup_scale
Increase the speeds of the ship, bullets, and aliens each time the player reaches a new level.
This method multiplies each speed setting by the value of
speedup_scale.- Call
increase_speed()in_check_bullet_alien_collisions():
- Call
def _check_bullet_alien_collisions(self):
--snip--
if not self.aliens:
# Destroy existing bullets and create new fleet.
self.bullets.empty()
self._create_fleet()
self.settings.increase_speed()
- This speeds up the entire game.
Resetting the Speed
- Return any changed settings to their initial values each time the player starts a new game:
def _check_play_button(self, mouse_pos):
"""Start a new game when the player clicks Play."""
button_clicked = self.play_button.rect.collidepoint(mouse_pos)
if button_clicked and not self.game_active:
# Reset the game settings.
self.settings.initialize_dynamic_settings()
--snip--
Scoring
- Implement a scoring system to track the game's score in real-time and display the high score, level, and number of ships remaining.
GameStats Class
- Add a score attribute to
GameStats:
class GameStats:
--snip--
def reset_stats(self):
"""Initialize statistics that can change during the game."""
self.ships_left = self.ai_settings.ship_limit
self.score = 0
- To reset the score each time a new game starts, initialize score in
reset_stats()rather than__init__().
Displaying the Score
- Create a new class, Scoreboard, to display the score on the screen. Eventually, we'll use it to report the high score, level, and number of ships remaining as well.
class Scoreboard:
import pygame.font
class Scoreboard:
"""A class to report scoring information."""
def __init__(self, ai_game):
"""Initialize scorekeeping attributes."""
self.screen = ai_game.screen
self.screen_rect = self.screen.get_rect()
self.settings = ai_game.settings
self.stats = ai_game.stats
# Font settings for scoring information.
self.text_color = (30, 30, 30)
self.font = pygame.font.SysFont(None, 48)
# Prepare the initial score image.
self.prep_score()
Imports the
pygame.fontmodule.The
__init__()method takesai_gameparameter, so it can access the settings, screen, and stats objects.Sets a text color and instantiate a font object.
Calls
prep_score()to turn the text to be displayed into an image.- Code for
prep_score():
- Code for
def prep_score(self):
"""Turn the score into a rendered image."""
score_str = str(self.stats.score)
self.score_image = self.font.render(score_str, True,
self.text_color, self.settings.bg_color)
# Display the score at the top right of the screen.
self.score_rect = self.score_image.get_rect()
self.score_rect.right = self.screen_rect.right - 20
self.score_rect.top = 20
turn the numerical value stats.score into a string then pass this string to render(), which creates the image
We'll position the score in the upper-right corner of the screen and have it expand to the left as the score increases and the width of the number grows.
- Code for
show_score():
- Code for
def show_score(self):
"""Draw score to the screen."""
self.screen.blit(self.score_image, self.score_rect)
Displays the score image onscreen at the location
score_rectspecifies.- Create a
Scoreboardinstance in AlienInvasion, update the import statements:
- Create a
from game_stats import GameStats from scoreboard import Scoreboard --snip--
- Then draw the scoreboard onscreen in
_update_screen():
def _update_screen(self):
--snip-- self.aliens.draw(self.screen)
# Draw the score information. self.sb.show_score()
# Draw the play button if the game is inactive.
--snip--
Updating the Score as Aliens Are Shot Down
- To write a live score onscreen, update the value of
stats.scorewhenever an alien is hit, and then callprep_score()to update the score image.
def initialize_dynamic_settings(self):
--snip--
# Scoring settings
self.alien_points = 50
Increase each alien's point value as the game progresses or reset the value each time a new game starts.
- Update the score in
_check_bullet_alien_collisions():
- Update the score in
def _check_bullet_alien_collisions(self):
"""Respond to bullet-alien collisions."""
# Remove any bullets and aliens that have collided.
collisions = pygame.sprite.groupcollide(
self.bullets, self.aliens, True, True)
if collisions:
self.stats.score += self.settings.alien_points
self.sb.prep_score()
--snip--
- When a bullet hits an alien, Pygame returns a
collisionsdictionary. If this dictionary exists, the alien's value is added to the score, andprep_score()is called to update the score.
Resetting the Score
- Prepping score when starting a new game
- Call prep_score() after resetting the game stats when starting a new game.
def _check_play_button(self, mouse_pos):
--snip--
if button_clicked and not self.game_active:
--snip--
# Reset the game statistics.
self.stats.reset_stats()
self.sb.prep_score()
Making sure to score all hits
refine bullet-alien collision detection to award points for each alien hit.
def _check_bullet_alien_collisions(self):
--snip--
if collisions:
for aliens in collisions.values():
self.stats.score += self.settings.alien_points * len(aliens)
self.sb.prep_score()
Rounding The Score
- Most arcade-style shooting games report scores as multiples of 10, so let’s fol- low that lead with our scores. Also, let’s format the score to include comma separators in large numbers. We’ll make this change in Scoreboard:
def prep_score(self):
"""Turn the score into a rendered image."""
rounded_score = round(self.stats.score, -1)
score_str = f"{rounded_score:,}"
self.score_image = self.font.render(score_str, True,
self.text_color, self.settings.bg_color)
--snip--
High Scores
- Every player wants to beat a game’s high score, so let’s track and report high scores to give players something to work toward. We’ll store high scores in GameStats:
def __init__(self, ai_game):
--snip--
# High score should never be reset.
self.high_score = 0
We call checkhighscore() when the collisions dictionary is present, and we do so after updating the score for all the aliens that have been hit.The first time you play Alien Invasion, your score will be the high score, so it will be displayed as the current score and the high score. But when you start a second game, your high score should appear in the middle and your current score should appear at the right, as shown in Figure 14-4.
Displaying the Level
- Update
GameStatsby initializing level:
def reset_stats(self):
"""Initialize statistics that can change during the game."""
self.ships_left = self.settings.ship_limit
self.score = 0
self.level = 1
- Have
Scoreboarddisplay the current level by callingprep_level()from__init__():
def __init__(self, ai_game):
--snip--
self.prep_high_score()
self.prep_level()
- Define prep_level
def prep_level(self):
"""Turn the level into a rendered image."""
level_str = str(self.stats.level)
self.level_image = self.font.render(level_str, True,
self.text_color, self.settings.bg_color)
# Position the level below the score.
self.level_rect = self.level_image.get_rect()
self.level_rect.right = self.score_rect.right
self.level_rect.top = self.score_rect.bottom + 10
- Also need to update show_score
def show_score(self):
"""Draw scores and level to the screen."""
self.screen.blit(self.score_image, self.score_rect)
self.screen.blit(self.high_score_image, self.high_score_rect)
self.screen.blit(self.level_image, self.level_rect)
The prep_level() method creates an image from the value stored in stats.level 1 and sets the image’s right attribute to match the score’s right attribute 2. It then sets the top attribute 10 pixels beneath the bottom of the score image to leave space between the score and the level 3.
We’ll increment stats.level and update the level image in _check_bullet _alien_collisions():
def _check_bullet_alien_collisions(self):
--snip--
if not self.aliens:
# Destroy existing bullets and create new fleet.
self.bullets.empty()
self._create_fleet()
self.settings.increase_speed()
# Increase level.
self.stats.level += 1
self.sb.prep_level()
Non Textual ships
Now we need to draw the ships to the screen:
def show_score(self):
"""Draw scores, level, and ships to the screen."""
self.screen.blit(self.score_image, self.score_rect)
self.screen.blit(self.high_score_image, self.high_score_rect)
self.screen.blit(self.level_image, self.level_rect)
self.ships.draw(self.screen)