Overview
This is part four of a five-part series of tutorials about making games with Python 3 and Pygame. In part three, we dove into the heart of Breakout and learned how to handle events, met the main Breakout class, and saw how to move the different game objects.
In this part, we will see how to detect collisions and what happens when the ball hits various objects like the paddle, the bricks, the walls, the ceiling, and the floor. Finally, we will review the important topic of game UI and in particular how to create a menu with our own custom buttons.
Collision Detection
In games, things bump into each other. Breakout is no different. Mostly it's the ball that bumps into stuff. The handle_ball_collisions()
method has a nested function called intersect()
, which is used to test if the ball hit an object and where it hit the object. It returns 'left', 'right', 'top', 'bottom', or None if the ball didn't hit the 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'
Hitting the Ball With the Paddle
When the ball hits the paddle, it bounces off. If it hits the top of the paddle, it will bounce back up but keep the same horizontal speed component.
But if it hits the side of the paddle, it will bounce to the opposite side (left or right) and continue its motion downward until it hits the floor. The code uses the intersect function().
# Hit paddle 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])
Hitting the Floor
When the paddle misses the ball on its way down (or if the ball hits the paddle on its side), the ball will keep falling and eventually hit the floor. At this point, the player loses a life, and the ball is recreated so the game can continue. The game is over when the player has run out of lives.
# Hit floor if self.ball.top > c.screen_height: self.lives -= 1 if self.lives == 0: self.game_over = True else: self.create_ball()
Hitting the Ceiling and Walls
When the ball hits a wall or the ceiling, it simply bounces back.
# Hit ceiling if self.ball.top < 0: self.ball.speed = (s[0], -s[1]) # Hit wall if self.ball.left < 0 or self.ball.right > c.screen_width: self.ball.speed = (-s[0], s[1])
Hitting Bricks
When a ball hits a brick, it's a major event in Breakout—the brick disappears, the player gets a point, the ball bounces back, and a few other things happen (sound effect and possibly a special effect too) that I'll discuss later.
To determine if a brick was hit, the code checks to see if any of the bricks intersects with the ball:
# Hit brick 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])
Programming the Game Menu
Most games have some UI. Breakout has a simple menu that has two buttons that say 'PLAY' and 'QUIT'. The menu shows up at the beginning of the game and disappears when the player clicks 'PLAY'. Let's see how the buttons and menu are implemented and how they integrate with the game.
Making Buttons
Pygame doesn't have a built-in UI library. There are third-party extensions, but I decided to build my own buttons for the menu. A button is a game object that has three states: normal, hover, and pressed. The normal state is when the mouse isn't over the button, and the hover state is when the mouse is over the button but the left mouse button isn't 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 background color and text displayed over it. The button also receives an on_click function (defaults to a noop lambda function) that gets called when the button is clicked.
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 handles its own mouse events and changes its internal state based on these events. When the button is in pressed state and receives a MOUSEBUTTONUP
event, it means the player clicked the button, and the on_click()
function is invoked.
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 that is used to draw the background rectangle always returns the color that matches the current state of the button, so it's clear to the player 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]
Creating the Menu
The create_menu()
function creates a menu with two buttons with the text 'PLAY' and 'QUIT'. It has two nested functions called on_play()
and on_quit()
that it provides to the corresponding button. Each button is added to the objects
list (to be drawn) and also to 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 clicked, on_play() is invoked, which removes the buttons from the objects
list so they are not drawn anymore. Also, the boolean fields that trigger the start of the game—is_game_running
and start_level
—are set to True.
When the QUIT button is clicked, is_game_running
is set to False
(effectively pausing the game) and game_over
is set to True, triggering the end game sequence.
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
Showing and Hiding the Game Menu
Showing and hiding the menu is implicit. When the buttons are in the objects
list, the menu is visible; when they are removed, it is hidden. As simple as that.
It is possible to create a nested menu with its own surface that renders sub-components like buttons and more, and then just add/remove that menu component, but it's not needed for this simple menu.
Conclusion
In this part, we covered collision detection and what happens when the ball hits various objects like the paddle, the bricks, the walls, the ceiling, and the floor. Also, we created our own menu with custom buttons that we hide and show on command.
In the last part of the series, we will look into the end game, keeping tabs on score and lives, sound effects, and music.
Then, we will develop a sophisticated system of special effects that will spice up the game. Finally, we will discuss the future direction and potential improvements.
No comments:
Post a Comment