Overview
This is part five of a five-part series of tutorials about making games with Python 3 and PyGame. In part four we detected collisions, responded to the ball hitting various game objects, and created a game menu with custom buttons.
In this last part, we'll cover diverse topics such as the end game, managing lives and score, sound effects, music, and even a flexible special effects system. For dessert, we'll discuss potential improvements and future directions.
The End Game
Eventually, the game has to end. In this version of Breakout, the game ends in one of two ways: either the player loses all their lives or they hit all the bricks. There is no next level (although it would be easy to add).
Game Over!
The game_over field of the Game class is set to False in the __init__()
method of the Game class. The main loop goes round and round until the game_over
variable is set to True:
class Game: def __init__(self, caption, width, height, back_image_filename, frame_rate): ... self.game_over = False ... def run(self): while not self.game_over: self.surface.blit(self.background_image, (0, 0)) self.handle_events() self.update() self.draw() pygame.display.update() self.clock.tick(self.frame_rate)
That all happens in the Breakout class in the following cases:
- The player clicked the QUIT button from the menu.
- The player loses their last life.
- The player cleared all the bricks.
def on_quit(button): self.game_over = True self.is_game_running = False def handle_ball_collisions(self): ... # Hit floor if self.ball.top > c.screen_height: self.lives -= 1 if self.lives == 0: self.game_over = True if not self.bricks: self.show_message('YOU WIN!!!', centralized=True) self.is_game_running = False self.game_over = True return def update(self): ... if not self.bricks: self.show_message('YOU WIN!!!', centralized=True) self.is_game_running = False self.game_over = True return
Display the End Game Message
Usually, when the game ends, we don't want the game window to just disappear into thin air. The exception is if you clicked the QUIT button in the menu. When the player loses their last life, Breakout displays the traditional 'GAME OVER!' message, and when the player wins, it displays 'YOU WIN!'
The show_message()
function is used in both cases. It displays the text on top of the current screen (the game will be paused) and waits for a few seconds before returning. In the next iteration of the game loop, the check for the game_over
field will determine it is True, and the program will exit.
Here is the show_message()
function:
def show_message(self, text, color=colors.WHITE, font_name='Arial', font_size=20, centralized=False): message = TextObject(c.screen_width // 2, c.screen_height // 2, lambda: text, color, font_name, font_size) self.draw() message.draw(self.surface, centralized) pygame.display.update() time.sleep(c.message_duration)
Keeping the High Score Between Games
In this version, I don't keep the high score because there is just one level, and everybody's score will be the same if they clear all the bricks. In general, it can be done locally by storing the high score in a file and then displaying another message if the player broke the high score.
Adding Sound Effects and Music
Games are an audio-visual experience. Most games have sound effects that are short sound bytes that are played when the player kills a monster, finds some treasure, or explodes horribly. Some games have background music too, which contributes to the atmosphere. Breakout has only sound effects, but I'll show you how to play background music in your games.
Sound Effects
You need sound files (similar to image files) to play as sound effects. These files can be in .wav, .mp3, or .ogg formats. Breakout keeps its sound effects in the sound_effects
folder:
~/git/pygame-breakout > tree sound_effects/ sound_effects/ ├── brick_hit.wav ├── effect_done.wav ├── level_complete.wav └── paddle_hit.wav
Let's see how these sound effects are loaded and played at the right time. First, to play sound effects (or background music) you need to initialize the sound system of Pygame. That happens in the Game class: pygame.mixer.pre_init(44100, 16, 2, 4096)
Then, in the Breakout class, all the sound effects are loaded from the config into the pygame.mixer.Sound
object and are stored in a dictionary:
# In config.py sounds_effects = dict( brick_hit='sound_effects/brick_hit.wav', effect_done='sound_effects/effect_done.wav', paddle_hit='sound_effects/paddle_hit.wav', level_complete='sound_effects/level_complete.wav', ) # In breakout.py class Breakout(Game): def __init__(self): ... self.sound_effects = { name: pygame.mixer.Sound(sound) for name, sound in c.sounds_effects.items()} ...
Now, we can play the sound effects when something interesting happens. For example, when the ball hits a brick:
# Hit brick for brick in self.bricks: edge = intersect(brick, self.ball) if not edge: continue self.sound_effects['brick_hit'].play()
The sound effect plays asynchronously, which means the game doesn't freeze while the sound is playing. Multiple sound effects can be played simultaneously.
Record Your Own Sound Effects and Messages
Recording your sound effects is both easy and rewarding. Unlike visual asset design, it doesn't take much talent. Anybody can say "Kaboom!" or "Boing" or shout "You're dead. Better luck next time!"
I often ask my kids to record sound effects as well as voice messages that accompany text messages like 'YOU WIN!' or 'GAME OVER!' Your imagination is the only limitation.
Playing Background Music
Background music should play constantly. In theory, you can have a very loooooooong sound effect, but a more common approach is simply to play the background music in a loop. Music files can be .wav, .mp3, or .midi format. Here is how it's done:
music = pygame.mixer.music.load('background_music.mp3') pygame.mixer.music.play(-1, 0.0)
You can have only one piece of background music playing at a time. But multiple sound effects can play over the background music. That's what mixing is all about.
Adding Advanced Features
Let's get fancy. Breaking bricks with a ball is cool, but it gets old pretty fast. How about a generic special effects system? We'll develop an extensible system of special effects that are associated with certain bricks and activate when the ball hits the brick.
Here is the plan. Effects have a lifetime. The effect starts when the brick breaks and ends when the duration of the effect elapses. What happens if the ball hits another special effect brick? In theory, you could have compounding effects, but to simplify things for the initial implementation, the active effect will stop, and the new effect will take its place.
Special Effects System
A special effect can be defined in the most generic way as two functions. The first function activates the effect, and the second function resets it. We want to attach effects to bricks and make it clear to the player which bricks are special, so they can try to hit or avoid them at certain points.
The following dict from the breakout.py module defines our special effects. Each effect has a name (e.g. long_paddle) and a value, which consists of the color its brick will have as well as the two functions. The functions are defined as lambda functions that take a Game instance, which includes everything a special effect in Breakout may want to change.
special_effects = dict( long_paddle=( colors.ORANGE, lambda g: g.paddle.bounds.inflate_ip( c.paddle_width // 2, 0), lambda g: g.paddle.bounds.inflate_ip( -c.paddle_width // 2, 0)), slow_ball=( colors.AQUAMARINE2, lambda g: g.change_ball_speed(-1), lambda g: g.change_ball_speed(1)), tripple_points=( colors.DARKSEAGREEN4, lambda g: g.set_points_per_brick(3), lambda g: g.set_points_per_brick(1)), extra_life=( colors.GOLD1, lambda g: g.add_life(), lambda g: None))
When the bricks are created, they have a change to be assigned one of the special effects. Here is the code:
def create_bricks(self): w = c.brick_width h = c.brick_height brick_count = c.screen_width // (w + 1) offset_x = (c.screen_width - brick_count * (w + 1)) // 2 bricks = [] for row in range(c.row_count): for col in range(brick_count): effect = None brick_color = c.brick_color index = random.randint(0, 10) if index < len(special_effects): x = list(special_effects.values())[index] brick_color = x[0] effect = x[1:] brick = Brick(offset_x + col * (w + 1), c.offset_y + row * (h + 1), w, h, brick_color, effect) bricks.append(brick) self.objects.append(brick) self.bricks = bricks
The Brick class has an effect field that is usually None, but can get (30% chance) one of the special effects defined above. Note that this code is unaware of what effects are available. It simply gets the effect and the brick color and assigns them if needed.
In this version of Breakout, I trigger effects only when a brick is hit, but you can imagine other scenarios that could trigger events. The previous effect is reset (if there was one), and then the new effect is launched. The reset function and the effect start time are stored for later.
if brick.special_effect is not None: # Reset previous effect if any if self.reset_effect is not None: self.reset_effect(self) # Trigger special effect self.effect_start_time = datetime.now() brick.special_effect[0](self) # Set current reset effect function self.reset_effect = brick.special_effect[1]
If no new effect was triggered, we still need to reset the current event when it expires. That happens in the update()
method. In each frame, the reset function of the current effect was assigned to the reset_effect
field. If the time since the current effect started exceeded the effect duration then the reset_effect()
function is called and the reset_effect
field is set to None (meaning there is no active effect right now).
# Reset special effect if needed if self.reset_effect: elapsed = datetime.now() - self.effect_start_time if elapsed >= timedelta(seconds=c.effect_duration): self.reset_effect(self) self.reset_effect = None
Enlarging the Paddle
The long paddle effect works by inflating the paddle by 50%. Its reset function just resizes it back to normal. The brick color is Orange:
long_paddle=( colors.ORANGE, lambda g: g.paddle.bounds.inflate_ip( c.paddle_width // 2, 0), lambda g: g.paddle.bounds.inflate_ip( -c.paddle_width // 2, 0)),
Slowing the Ball
Another effect that helps with chasing the ball is the slow ball effect, which simply slows the ball speed by one unit. The brick color is Aquamarine.
slow_ball=(colors.AQUAMARINE2, lambda g: g.change_ball_speed(-1), lambda g: g.change_ball_speed(1)),
More Points
If you want big numbers, you'll like the triple points effect that gives you three points for each brick you hit instead of the standard one point. The brick color is dark green.
tripple_points=(colors.DARKSEAGREEN4, lambda g: g.set_points_per_brick(3), lambda g: g.set_points_per_brick(1)),
Extra Lives
Finally, a very helpful effect is the extra lives effect. It just gives you an extra life. No reset is needed really. The brick color is gold.
extra_life=(colors.GOLD1, lambda g: g.add_life(), lambda g: None))
Future Features
There are several natural directions to extend Breakout. If you're interested in trying your hand at adding more capabilities and features, here are some ideas.
Take It to the Next Level
To make Breakout a serious game, it needs levels. Playing just one screen is not enough. At the beginning of each level, you will reset the screen, but keep the score and lives as is. To make the game harder, you can slightly increase the ball speed on each level or add another layer of bricks.
Second Ball
Adding a second ball as a temporary effect is bound to create a lot of chaos. The tricky part here is to treat both balls as equal, regardless of which one was the original. When one ball is gone, the game continues with the single ball that was left. No life is lost.
Persistent High Score
When you have levels with increasing difficulty, the high score becomes a coveted prize. You can keep the high score in a file to persist between games. When a player breaks the high score, you can add a little pizazz or let them write their name (traditionally just three characters).
Bombs and Power-Ups
In the current implementation, all special effects are tied to bricks, but you can add effects (good and bad) that drop from the sky and the player has to collect them or avoid them.
Conclusion
Developing Breakout using Python 3 and Pygame was a super rewarding experience. It's a very powerful combination for 2D games (and 3D games too). If you like Python and want to make your own games, you can't go wrong with Pygame.
I definitely plan to make more games with Python and Pygame.
Finally, remember we have plenty of Python content available for sale and for study in the Envato Market.
No comments:
Post a Comment