Book Image

Python Game Programming By Example

Book Image

Python Game Programming By Example

Overview of this book

Table of Contents (14 chapters)
Python Game Programming By Example
Credits
About the Authors
About the Reviewers
www.PacktPub.com
Preface
Index

Movement and collisions


Now that we have placed all of our game objects, we can define the methods that will be executed in the game loop. This loop runs indefinitely until the game ends, and each iteration updates the position of the ball and checks the collision that occurs.

With the Canvas widget, we can calculate what the items that overlap with the given coordinates are, so for now, we will implement the methods that are responsible for moving the ball and changing its direction.

Let's start with the movement of the ball and the conditions for recreating the bouncing effect when it reaches the canvas borders:

    def update(self):
        coords = self.get_position()
        width = self.canvas.winfo_width()
        if coords[0] <= 0 or coords[2] >= width:
            self.direction[0] *= -1
        if coords[1] <= 0:
            self.direction[1] *= -1
        x = self.direction[0] * self.speed
        y = self.direction[1] * self.speed
        self.move(x, y)

The update method does the following:

  • It gets the current position and the width of the canvas. It stores the values in the coords and width local variables, respectively.

  • If the position collides with the left or right border of the canvas, the horizontal component of the direction vector changes its sign

  • If the position collides with the upper border of the canvas, the vertical component of the direction vector changes its sign

  • We scale the direction vector by the ball's speed

  • The self.move(x, y) moves the ball

For instance, if the ball hits the left border, the coords[0] <= 0 condition evaluates to true, so the x-axis component of the direction changes its sign, as shown in this diagram:

If the ball hits the top-right corner, both coords[2] >= width and coords[1] <= 0 evaluate to true. This changes the sign of both the components of the direction vector, like this:

The logic of the collision with a brick is a bit more complex, since the direction of the rebound depends on the side where the collision occurs.

We will calculate the x-axis component of the ball's center and check whether it is between the lowermost and uppermost x-axis coordinates of the colliding brick. To translate this into a quick implementation, the following snippet shows the possible changes in the direction vector as per the ball and brick coordinates:

        coords = self.get_position()
        x = (coords[0] + coords[2]) * 0.5
        brick_coords = brick.get_position()
        if x > brick_coords[2]:
            self.direction[0] = 1
        elif x < brick_coords[0]:
            self.direction[0] = -1
        else:
            self.direction[1] *= -1

For instance, this collision causes a horizontal rebound, since the brick is being hit from above, as shown here:

On the other hand, a collision from the right-hand side of the brick would be as follows:

This is valid when the ball hits the paddle or a single brick. However, the ball can hit two bricks at the same time. In this situation, we cannot execute the previous statements for each brick; if the y-axis direction is multiplied by -1 twice, the value in the next iteration of the game loop will be the same.

We could check whether the collision occurred from above or behind, but the problem with multiple bricks is that the ball may overlap the lateral of one of the bricks and, therefore, change the x-axis direction as well. This happens because of the ball's speed and the rate at which its position is updated.

We will simplify this by assuming that a collision with multiple bricks at the same time occurs only from above or below. That means that it changes the y-axis component of the direction without calculating the position of the colliding bricks:

        if len(game_objects) > 1:
            self.direction[1] *= -1

With these two conditions, we can define the collide method. As we will see later, another method will be responsible for determining the list of colliding bricks, so this method only handles the outcome of a collision with one or more bricks:

    def collide(self, game_objects):
        coords = self.get_position()
        x = (coords[0] + coords[2]) * 0.5
        if len(game_objects) > 1:
            self.direction[1] *= -1
        elif len(game_objects) == 1:
            game_object = game_objects[0]
            coords = game_object.get_position()
            if x > coords[2]:
                self.direction[0] = 1
            elif x < coords[0]:
                self.direction[0] = -1
            else:
                self.direction[1] *= -1

        for game_object in game_objects:
            if isinstance(game_object, Brick):
                game_object.hit()

Note that this method hits every brick instance that is colliding with the ball, so the hit counters are decreased and the bricks are removed if they reach zero hits.