Creating Games in Python 3 and Pygame: Part 4

Original author: Gigi Sayfan
  • Transfer
  • Tutorial
image

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

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

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

Collision Recognition


In games, objects collide with each other, and Breakout is no exception. Mostly the ball collides with objects. The method handle_ball_collisions()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 does not collide 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'

Collision of a ball with a racket


When the ball hits the racket, it bounces. If it hits the top of the racket, it bounces back up, but retains the same component of horizontal speed.

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 collides with the floor. The code uses a function intersect().

# Удар об ракетку
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 passes the ball on its way down (or the ball hits the racket from the side), the ball continues to fall and then hits the floor. At this moment, the player loses his life and the ball is recreated so that the game can continue. The game ends when the player runs out of life.

# Удар об пол
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 a ball hits a wall or ceiling, it just bounces off 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])

Collision with bricks


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

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

# Удар об кирпич
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 the buttons and menus are implemented, as well as how they are integrated into the game.

Button Creation


Pygame has no 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 above the button and the player 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 (which by default is an empty 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)

The button processes its own mouse events and changes its internal state based on these events. When the button is in the pressed state and receives the event MOUSEBUTTONUP, this means that the player has pressed the button and the function is called 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 property back_colorused to draw the background rectangle always returns the color corresponding to the current state of the button, so that it is clear to the player 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 function create_menu()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 objects(for rendering), as well as in the field menu_buttons.

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 on_play(), a function is called that removes the buttons from the list objectsso that they are no longer rendered. In addition, the values ​​of the Boolean fields that trigger the start of the game - is_game_runningand start_level- become True.

When the button is pressed, QUIT is_game_runningtakes on a value False(in fact, pausing the game), and is set game_overto True, which triggers the sequence of completion 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 performed implicitly. When the buttons are in the list 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, which renders its subcomponents (buttons and other objects), and then simply add / remove these menu components, but this is not required for such a simple menu.

To summarize


In this part, we examined collision recognition 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 on command.

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

Then we will develop a complex system of special effects that add a few spices to the game. Finally, we will discuss further development and possible improvements.

Also popular now: