๐Ÿ“œ โฌ†๏ธ โฌ‡๏ธ

Making games in Python 3 and Pygame: Part 4

image

This is the fourth of the five parts of the tutorial on creating games using Python 3 and Pygame. In the third part, we plunged into the heart of Breakout and learned how to handle events, met the main Breakout class and saw how to move different game objects.

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

In this section, we will learn how to recognize collisions and what happens when the ball hits different objects: racket, bricks, walls, ceiling and floor. Finally, we look at the important topic of the user interface and, in particular, how to create a menu of your own buttons.

Collision detection


In games, objects collide with each other, and Breakout is no exception. Basically with objects the ball collides. The handle_ball_collisions() method has a built-in function called intersect() , which is used to check whether the ball hit the object and where it collided with the object. It returns 'left', 'right', 'top', 'bottom' or None if the ball has not collided with an object.
')
 def handle_ball_collisions(self): def intersect(obj, ball): edges = dict( left=Rect(obj.left, obj.top, 1, obj.height), right=Rect(obj.right, obj.top, 1, obj.height), top=Rect(obj.left, obj.top, obj.width, 1), bottom=Rect(obj.left, obj.bottom, obj.width, 1)) collisions = set(edge for edge, rect in edges.items() if ball.bounds.colliderect(rect)) if not collisions: return None if len(collisions) == 1: return list(collisions)[0] if 'top' in collisions: if ball.centery >= obj.top: return 'top' if ball.centerx < obj.left: return 'left' else: return 'right' if 'bottom' in collisions: if ball.centery >= obj.bottom: return 'bottom' if ball.centerx < obj.left: return 'left' else: return 'right' 

Ball crash with racket


When the ball hits the racket, it bounces off. If it hits the top of the racket, it is reflected back up, but retains the same component of the horizontal velocity.

But if he hits the side of the racket, it bounces in the opposite direction (left or right) and continues to move down until it hits the floor. The code uses the intersect() function.

 #    s = self.ball.speed edge = intersect(self.paddle, self.ball) if edge is not None: self.sound_effects['paddle_hit'].play() if edge == 'top': speed_x = s[0] speed_y = -s[1] if self.paddle.moving_left: speed_x -= 1 elif self.paddle.moving_left: speed_x += 1 self.ball.speed = speed_x, speed_y elif edge in ('left', 'right'): self.ball.speed = (-s[0], s[1]) 

Collision with the floor


When a racket misses the ball on the way down (or the ball hits the side of the racket), the ball continues to fall and then hits the floor. At this point, the player loses his life and the ball is created anew so that the game can continue. The game ends when the player ends up living.

 #    if self.ball.top > c.screen_height: self.lives -= 1 if self.lives == 0: self.game_over = True else: self.create_ball() 

Collision with ceiling and walls


When the ball hits a wall or ceiling, it just bounces off of them.

 #    if self.ball.top < 0: self.ball.speed = (s[0], -s[1]) #    if self.ball.left < 0 or self.ball.right > c.screen_width: self.ball.speed = (-s[0], s[1]) 

Facing bricks


When the ball hits a brick, this is the main event of the Breakout game - the brick disappears, the player gets a point, the ball is reflected back and several more events occur (sound effect, and sometimes a special effect), which I will consider later.

To determine if the ball hit a brick, the code will check if any of the bricks with the ball intersect:

 #    for brick in self.bricks: edge = intersect(brick, self.ball) if not edge: continue self.bricks.remove(brick) self.objects.remove(brick) self.score += self.points_per_brick if edge in ('top', 'bottom'): self.ball.speed = (s[0], -s[1]) else: self.ball.speed = (-s[0], s[1]) 

Game menu programming


Most games have some kind of UI. Breakout has a simple menu with two buttons, 'PLAY' and 'QUIT'. The menu is displayed at the beginning of the game and disappears when the player clicks on 'PLAY'. Let's see how buttons and menus are implemented, as well as how they integrate into the game.

Creating buttons


Pygame does not have a built-in UI library. There are third-party extensions, but for the menu I decided to create my own buttons. A button is a game object that has three states: normal, highlighted and pressed. The normal state is when the mouse is not above the button, and the highlighted state is when the mouse is above the button, but the left mouse button is not yet pressed. The pressed state is when the mouse is over the button and the player has pressed the left mouse button.

The button is implemented as a rectangle with a background color and text displayed on top of it. The button also receives the on_click function (by default, which is an empty lambda function), which is called when the button is pressed.

 import pygame from game_object import GameObject from text_object import TextObject import config as c class Button(GameObject): def __init__(self, x, y, w, h, text, on_click=lambda x: None, padding=0): super().__init__(x, y, w, h) self.state = 'normal' self.on_click = on_click self.text = TextObject(x + padding, y + padding, lambda: text, c.button_text_color, c.font_name, c.font_size) def draw(self, surface): pygame.draw.rect(surface, self.back_color, self.bounds) self.text.draw(surface) 

The button processes its own mouse events and changes its internal state based on these events. When the button is pressed down and receives the MOUSEBUTTONUP event, this means that the player has pressed the button and the on_click() function is on_click() .

 def handle_mouse_event(self, type, pos): if type == pygame.MOUSEMOTION: self.handle_mouse_move(pos) elif type == pygame.MOUSEBUTTONDOWN: self.handle_mouse_down(pos) elif type == pygame.MOUSEBUTTONUP: self.handle_mouse_up(pos) def handle_mouse_move(self, pos): if self.bounds.collidepoint(pos): if self.state != 'pressed': self.state = 'hover' else: self.state = 'normal' def handle_mouse_down(self, pos): if self.bounds.collidepoint(pos): self.state = 'pressed' def handle_mouse_up(self, pos): if self.state == 'pressed': self.on_click(self) self.state = 'hover' 

The back_color property used to draw the background rectangle always returns the color corresponding to the current state of the button, so that the player can see that the button is active:

 @property def back_color(self): return dict(normal=c.button_normal_back_color, hover=c.button_hover_back_color, pressed=c.button_pressed_back_color)[self.state] 

Menu creation


The create_menu() function creates a menu with two buttons with the text 'PLAY' and 'QUIT'. It has two built-in functions, on_play() and on_quit() , which it passes to the corresponding button. Each button is added to the list of objects (for drawing), as well as in the menu_buttons field.

 def create_menu(self): for i, (text, handler) in enumerate((('PLAY', on_play), ('QUIT', on_quit))): b = Button(c.menu_offset_x, c.menu_offset_y + (c.menu_button_h + 5) * i, c.menu_button_w, c.menu_button_h, text, handler, padding=5) self.objects.append(b) self.menu_buttons.append(b) self.mouse_handlers.append(b.handle_mouse_event) 

When the PLAY button is pressed, the on_play() function is on_play() , which removes the buttons from the objects list so that they are no longer drawn. In addition, the values โ€‹โ€‹of the Boolean fields that trigger the start of the game โ€” is_game_running and start_level โ€” become equal to True.

When you press the QUIT button, is_game_running takes the value False (actually putting the game on pause), and game_over to True, which triggers the completion sequence of the game.

 def on_play(button): for b in self.menu_buttons: self.objects.remove(b) self.is_game_running = True self.start_level = True def on_quit(button): self.game_over = True self.is_game_running = False 

Display and hide the game menu


The display and hiding of the menu are implicitly performed. When the buttons are in the list of objects , the menu is visible; when they are removed, it is hidden. Everything is very simple.

You can create a built-in menu with its own surface that renders its subcomponents (buttons and other objects), and then just add / remove these menu components, but for such a simple menu this is not required.

Summarize


In this part, we looked at collision detection and what happens when the ball collides with different objects: a racket, bricks, walls, floor and ceiling. We also created a menu with our own buttons, which can be hidden and displayed by a command.

In the last part of the series, we will cover the completion of the game, the tracking of points and lives, sound effects and music.

Then we will develop a complex system of special effects that add a little spice to the game. Finally, we will discuss further developments and possible improvements.

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


All Articles