This is the fourth in a five-part tutorial series on making games with Python 3 and Pygame. In Part Three, we delved into the core of Breakout, learned how to handle events, looked at the main Breakout classes, and saw how to move different game objects.
In this part, we'll learn how to detect collisions and what happens when the ball hits various objects such as paddles, bricks, walls, ceilings, and floors. Finally, we'll review important topics of game UI, specifically how to create menus with our own custom buttons.
In the game, things collide with each other. Breakthrough is no exception. Most of the time it's the ball hitting something. The handle_ball_collisions()
method has a nested function called intersect()
that tests whether the ball hits an object and where it hits the object. If the ball misses an object, returns "Left", "Right", "Up", "Down", or "None".
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'
When the ball hits the racket, it bounces away. If it hits the top of the paddle, it will bounce back but maintain the same horizontal velocity component.
However, if it hits one side of the paddle, it will bounce to the other side (left or right) and continue its downward motion until it hits the floor. This 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])
When the racket misses the ball on its way down (or the ball hits the side of the racket), the ball will continue to fall 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 ends when the player runs 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()
When the ball hits a wall or ceiling, it just 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])
When the ball hits a brick, this is a major event in Breakout - the brick disappears, the player gets a point, the ball bounces back, and a few other things happen (sound effects, and possibly special effects ), which I will discuss later.
To determine if a brick was hit, the code checks to see if any bricks intersect 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])
Most games have some kind of user interface. Breakout has a simple menu with two buttons, "PLAY" and "QUIT." This menu appears when the game starts and disappears when the player clicks "Start". Let's see how buttons and menus are implemented and how they integrate with the game.
Pygame does not have a built-in UI library. There are third party extensions, but I decided to build my own button for the menu. Buttons are game objects with three states: normal, hover, and pressed. The normal state is when the mouse is not over the button, and the hover state is when the mouse is over the button but the left mouse button is not pressed. The pressed state is when the mouse is over the button and the player presses the left mouse button.
The button is implemented as a rectangle with a background color and text displayed on it. The button also receives an on_click function (defaults to a noop lambda function), which is 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)
Buttons handle their own mouse events and change their internal state based on those events. When the button is pressed, the MOUSEBUTTONUP
event is received, indicating that the player clicked the button, and the on_click()
function is called.
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 a color that matches the button's current state, so the player can clearly 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]
create_menu()
Function creates a menu with two buttons containing the text "PLAY" and "QUIT". It has two nested functions named on_play()
and on_quit()
which are fed to the corresponding buttons. Each button is added to the objects
list (to be drawn) and 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 called, which removes the button from the objects
list so that they are no longer drawn. Additionally, the boolean fields is_game_running
and start_level
that trigger the start of the game are set to True.
When the exit button is clicked, is_game_running
is set to False
(effectively pausing the game) and game_over
is set to True, triggering the final 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 menus is implicit. When the buttons are in the objects
list, the menu is visible; when they are removed, it is hidden. It's that easy.
It's possible to create a nested menu with its own surface that renders subcomponents like buttons and then just add/remove that menu component, but this simple menu doesn't need it.
In this part, we cover collision detection and what happens when the ball hits various objects such as paddles, bricks, walls, ceilings, and floors. Additionally, we created our own menu with custom buttons that hide and show based on commands.
In this final part of the series, we'll look at the final game, paying close attention to scores and lives, sound effects, and music.
We will then develop a complex special effects system to add interest to the game. Finally, we discuss future directions and potential improvements.
The above is the detailed content of Creating interactive games with Python 3 and Pygame: Part 4. For more information, please follow other related articles on the PHP Chinese website!