Starting the game
Finally, we have built the functionality needed to run the game loop—the logic required to update the ball's position according to the rebounds, and restart the game if the player loses one life.
Now we can add the following methods to our Game
class to complete the development of our game:
def start_game(self): self.canvas.unbind('<space>') self.canvas.delete(self.text) self.paddle.ball = None self.game_loop() def game_loop(self): self.check_collisions() num_bricks = len(self.canvas.find_withtag('brick')) if num_bricks == 0: self.ball.speed = None self.draw_text(300, 200, 'You win!') elif self.ball.get_position()[3] >= self.height: self.ball.speed = None self.lives -= 1 if self.lives < 0: self.draw_text(300, 200, 'Game Over') else: self.after(1000, self.setup_game) else: self.ball.update() self.after(50, self.game_loop)
The start_game
method, which we left unimplemented in a previous section, is responsible for unbinding the Spacebar input key so that the player cannot start the game twice, detaching the ball from the paddle, and starting the game loop.
Step by step, the game_loop
method does the following:
- It calls
self.check_collisions()
to process the ball's collisions. We will see its implementation in the next code snippet. - If the number of bricks left is zero, it means that the player has won, and a congratulations text is displayed.
- Suppose the ball has reached the bottom of the canvas:
- Then, the player loses one life. If the number of lives left is zero, it means that the player has lost, and the Game Over text is shown. Otherwise, the game is reset
- Otherwise, this is what happens:
- The position of the ball is updated according to its speed and direction, and the game loop is called again. The
.after(delay, callback)
method on a Tkinter widget sets a timeout to invoke a function after a delay in milliseconds. Since this statement will be executed when the game is not over yet, this creates the loop necessary to execute this logic continuously:def check_collisions(self): ball_coords = self.ball.get_position() items = self.canvas.find_overlapping(*ball_coords) objects = [self.items[x] for x in items \ if x in self.items] self.ball.collide(objects)
- The position of the ball is updated according to its speed and direction, and the game loop is called again. The
The check_collisions
method links the game loop with the ball collision method. Since Ball.collide
receives a list of game objects and canvas.find_overlapping
returns a list of colliding items with a given position, we use the dictionary of items to transform each canvas item into its corresponding game object.
Remember that the items
attribute of the Game
class contains only those canvas items that can collide with the ball. Therefore, we need to pass only the items contained in this dictionary. Once we have filtered the canvas items that cannot collide with the ball, such as the text displayed in the top-left corner, we retrieve each game object by its key.
With list comprehensions, we can create the required list in one simple statement:
objects = [self.items[x] for x in items if x in self.items]
The basic syntax of list comprehensions is the following:
new_list = [expr(elem) for elem in collection]
This means that the new_list
variable will be a list whose elements are the result of applying the expr
function to each elem
in the list collection.
We can filter the elements to which the expression will be applied by adding an if
clause:
new_list = [expr(elem) for elem in collection if elem is not None]
This syntax is equivalent to the following loop:
new_list = [] for elem in collection: if elem is not None: new_list.append(elem)
In our case, the initial list is the list of colliding items, the if
clause filters the items that are not contained in the dictionary, and the expression applied to each element retrieves the game object associated with the canvas item. The collide
method is called with this list as a parameter, and the logic for the game loop is completed.