📜 ⬆️ ⬇️

Making games in Python 3 and Pygame: Part 5

image

This is the last of the five parts of the tutorial on creating games using Python 3 and PyGame. In the fourth part, we learned to recognize collisions, react to the fact that the ball collides with different game objects and created a game menu with its own buttons.

(The remaining parts of the tutorial: first , second , third , fourth .)

In the last part, we will look at various topics: the end of the game, the management of lives and glasses, sound effects, music, and even a flexible system of special effects. For dessert, we will look at possible improvements and directions for further development.

End of the game


Sooner or later the game must end. In this version of Breakout, the game ends in one of two ways: the player either loses all his lives, or destroys all the bricks. There is no next level in the game (but it can be easily added).
')

Game Over!


The game_over of the Game class is set to False in the __init__() method of the Game class. The main loop continues until the variable game_over changes the value 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) 

All this happens in the Breakout class in the following cases:


 def on_quit(button): self.game_over = True self.is_game_running = False def handle_ball_collisions(self): ... #    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 of the end game message


Usually at the end of the game, we do not want the game window to silently disappear. An exception is the case when we click on the QUIT button in the menu. When a player loses his last life, Breakout shows the traditional message 'GAME OVER!', And when the player wins, it shows the message 'YOU WIN!'

In both cases, the show_message() function is used. It displays text over the current screen (the game pauses) and waits a few seconds before returning. In the next iteration of the game cycle, checking the game_over field game_over determine that it is True, after which the program ends.

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) 

Preservation of records between games


In this version of the game I do not save records, because there is only one level in it, and the results of all players after the destruction of bricks will be the same. In general, saving records can be implemented locally, saving records to a file and displaying another message if the player breaks the record.

Adding sound effects and music


Games are an audio-visual process. Many games have sound effects — short audio sequences that are played when a player kills monsters, finds a treasure, or a horrible death. Some games also have background music that contributes to the atmosphere. Breakout has only sound effects, but I'll show you how to play music in your games.

Sound effects


To play sound effects we need sound files (as is the case with image files). These files can be in .wav, .mp3 or .ogg format. Breakout stores its sound effects in the sound_effects folder:

  ~ / git / pygame-breakout> tree sound_effects /
 sound_effects /
 Brick── brick_hit.wav
 Effect── effect_done.wav
 Level── level_complete.wav
 Pad── paddle_hit.wav 

Let's see how these sound effects load and play at the right time. First, to play sound effects (or background music), we need to initialize the Pygame sound system. This is done in the Game class: pygame.mixer.pre_init(44100, 16, 2, 4096)

Then, in the Breakout class, all sound effects are loaded from the config into the pygame.mixer.Sound object and stored in the dictionary:

 #  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', ) #  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 sound effects when something interesting happens. For example, when the ball hits a brick:

 #    for brick in self.bricks: edge = intersect(brick, self.ball) if not edge: continue self.sound_effects['brick_hit'].play() 

The sound effect is played asynchronously, that is, the game does not pause during its sound. Multiple sound effects can be played simultaneously.

Record your own sound effects and messages


Recording your own sound effects can be a simple and fun experience. Unlike the creation of visual resources, it does not require great talent. Anyone can say “Boom!” Or “Jump”, or shout “You have been killed. Lucky another time! ”

I often ask my children to record sound effects and voice messages accompanying text messages, for example 'YOU WIN!' or 'GAME OVER!' The only limitation here is your own imagination.

Play background music


Background music should be played constantly. Theoretically, you can create a very long sound effect, but most often use looped reproduction of background music. Music files can be in .wav, .mp3 or .midi format. This is how music is realized:

 music = pygame.mixer.music.load('background_music.mp3') pygame.mixer.music.play(-1, 0.0) 

Only one background music can play at a time. However, several sound effects can be played over the background music. This is what is called mixing.

Adding advanced features


Let's do something curious. It is interesting to destroy bricks with a ball, but it bothers pretty quickly. What about the general system of special effects? We will develop an expandable system of special effects associated with some bricks, which is activated when the ball hits the brick.

Here is the plan. Effects have a lifetime. An effect starts when a brick breaks and ends when the effect’s duration ends. What happens if the ball hits another brick with a special effect? In theory, it is possible to create compatible effects, but in order to simplify everything in the original implementation, the active effect will stop, and the new effect will take its place.

Special effects system


The special effect in the most general case can be defined as two functions. The first function activates the effect, and the second resets it. We want to attach effects to bricks and give the player a clear understanding of which of the bricks are special, so that they can try to hit them or avoid them at certain times.

Our special effects are defined by the dictionary from the module breakout.py . Each effect has a name (for example, long_paddle) and a value that consists of a brick color, as well as two functions. Functions are set as lambda functions that take an instance of the Game, which includes everything that can change the special effect in Breakout.

 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 creating bricks, they can 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, which is usually None, but can (with a 30% probability) contain one of the special effects defined above. Note that this code does not know what effects exist. He simply receives the effect and color of the brick and, if necessary, assigns them.

In this version of Breakout, I trigger effects only when I hit a brick, but you can think of other options for triggering events. The previous effect is reset (if it existed), and then a new effect is triggered. The reset function and the start time of the effect are stored for future use.

 if brick.special_effect is not None: #       if self.reset_effect is not None: self.reset_effect(self) #   self.effect_start_time = datetime.now() brick.special_effect[0](self) #      self.reset_effect = brick.special_effect[1] 

If a new effect is not launched, we still need to reset the current effect after its lifetime. This happens in the update() method. In each frame, the reset function of the current effect is assigned to the reset_effect field. If the time after the start of the current effect exceeds the duration of the effect, the reset_effect() function is reset_effect() , and the reset_effect field takes the value None (meaning that there are currently no active effects).

 #     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 

Racket increase


The effect of a long racket is to increase the racket by 50%. Its reset function returns the racket to normal size. The brick is orange in color:

 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 in the pursuit of the ball is slowing the ball, that is, reducing its speed by one unit. The brick is Aquamarine color.

 slow_ball=(colors.AQUAMARINE2, lambda g: g.change_ball_speed(-1), lambda g: g.change_ball_speed(1)), 

More points


If you want great results, then you will enjoy the effect of tripling points, giving you three points for each destroyed brick instead of the standard one point. The brick has a dark green color.

 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 useful effect will be the effect of extra lives. He just gives you another life. It does not need a reset. The brick is gold.

 extra_life=(colors.GOLD1, lambda g: g.add_life(), lambda g: None)) 

Opportunities to add in the future


There are several logical directions for extending Breakout. If you are interested in trying yourself in adding new features and functions, here are a few ideas.

Go to next level


To turn Breakout into a serious game, you need levels, one is not enough. At the beginning of each level, we will reset the screen, but save points and lives. To complicate the game, you can slightly increase the speed of the ball at each level or add another layer of bricks.

Second ball


The effect of temporarily adding a second ball will create a huge chaos. The difficulty here is to treat both goals as equal, regardless of which of them is the source. When one ball disappears, the game continues with the only remaining one. Life is not lost.

Lingering records


When you have levels with increasing difficulty, it is advisable to create a table of records. You can store records in the file so that they are saved after the game. When a player breaks a record, you can add small pizzas or allow him to write a name (traditionally of just three characters).

Bombs and Bonuses


In the current implementation, all special effects are associated with bricks, but you can add effects (good and bad) falling from the sky that the player can collect or avoid.

Summarize


Breakout development using Python 3 and Pygame was a very exciting experience. This is a very powerful combination for creating 2D games (and for 3D games too). If you like Python and want to create your own games, then do not hesitate to choose Pygame.

I will definitely create in Python and Pygame and other games.

Source: https://habr.com/ru/post/347286/


All Articles