Basic game objects
Before we start drawing all our game items, let's define a base class with the functionality that they will have in common—storing a reference to the canvas and its underlying canvas item, getting information about its position, and deleting the item from the canvas:
class GameObject(object): def __init__(self, canvas, item): self.canvas = canvas self.item = item def get_position(self): return self.canvas.coords(self.item) def move(self, x, y): self.canvas.move(self.item, x, y) def delete(self): self.canvas.delete(self.item)
Assuming that we have created a Canvas widget as shown in our previous code samples, a basic usage of this class and its attributes would be like this:
item = canvas.create_rectangle(10,10,100,80, fill='green') game_object = GameObject(canvas,item) #create new instance print(game_object.get_position()) # [10, 10, 100, 80] game_object.move(20, -10) print(game_object.get_position()) # [30, 0, 120, 70] game_object.delete()
In this example, we created a green rectangle and a GameObject
instance with the resulting item. Then we retrieved the position of the item within the canvas, moved it, and calculated the position again. Finally, we deleted the underlying item.
The methods that the GameObject
class offers will be reused in the subclasses that we will see later, so this abstraction avoids unnecessary code duplication. Now that you have learned how to work with this basic class, we can define separate child classes for the ball, the paddle, and the bricks.
The Ball class
The Ball
class will store information about the speed, direction, and radius of the ball. We will simplify the ball's movement, since the direction vector will always be one of the following:
- [1, 1] if the ball is moving towards the bottom-right corner
- [-1, -1] if the ball is moving towards the top-left corner
- [1, -1] if the ball is moving towards the top-right corner
- [-1, 1] if the ball is moving towards the bottom-left corner
Therefore, by changing the sign of one of the vector components, we will change the ball's direction by 90 degrees. This will happen when the ball bounces against the canvas border, when it hits a brick, or the player's paddle:
class Ball(GameObject): def __init__(self, canvas, x, y): self.radius = 10 self.direction = [1, -1] self.speed = 10 item = canvas.create_oval(x-self.radius, y-self.radius, x+self.radius, y+self.radius, fill='white') super(Ball, self).__init__(canvas, item)
For now, the object initialization is enough to understand the attributes that the class has. We will cover the ball rebound logic later, when the other game objects have been defined and placed in the game canvas.
The Paddle class
The Paddle
class represents the player's paddle and has two attributes to store the width and height of the paddle. A set_ball
method will be used to store a reference to the ball, which can be moved with the ball before the game starts:
class Paddle(GameObject): def __init__(self, canvas, x, y): self.width = 80 self.height = 10 self.ball = None item = canvas.create_rectangle(x - self.width / 2, y - self.height / 2, x + self.width / 2, y + self.height / 2, fill='blue') super(Paddle, self).__init__(canvas, item) def set_ball(self, ball): self.ball = ball def move(self, offset): coords = self.get_position() width = self.canvas.winfo_width() if coords[0] + offset >= 0 and \ coords[2] + offset <= width: super(Paddle, self).move(offset, 0) if self.ball is not None: self.ball.move(offset, 0)
The move
method is responsible for the horizontal movement of the paddle. Step by step, the following is the logic behind this method:
- The
self.get_position()
calculates the current coordinates of the paddle - The
self.canvas.winfo_width()
retrieves the canvas width - If both the minimum and maximum x-axis coordinates, plus the offset produced by the movement, are inside the boundaries of the canvas, this is what happens:
- The
super(Paddle, self).move(offset, 0)
calls the method with same name in thePaddle
class's parent class, which moves the underlying canvas item - If the paddle still has a reference to the ball (this happens when the game has not been started), the ball is moved as well
- The
This method will be bound to the input keys so that the player can use them to control the paddle's movement. We will see later how we can use Tkinter to process the input key events. For now, let's move on to the implementation of the last one of our game's components.
The Brick class
Each brick in our game will be an instance of the Brick
class. This class contains the logic that is executed when the bricks are hit and destroyed:
class Brick(GameObject): COLORS = {1: '#999999', 2: '#555555', 3: '#222222'} def __init__(self, canvas, x, y, hits): self.width = 75 self.height = 20 self.hits = hits color = Brick.COLORS[hits] item = canvas.create_rectangle(x - self.width / 2, y - self.height / 2, x + self.width / 2, y + self.height / 2, fill=color, tags='brick') super(Brick, self).__init__(canvas, item) def hit(self): self.hits -= 1 if self.hits == 0: self.delete() else: self.canvas.itemconfig(self.item, fill=Brick.COLORS[self.hits])
As you may have noticed, the __init__
method is very similar to the one in the Paddle
class, since it draws a rectangle and stores the width and the height of the shape. In this case, the value of the tags
option passed as a keyword argument is 'brick'
. With this tag, we can check whether the game is over when the number of remaining items with this tag is zero.
Another difference from the Paddle
class is the hit
method and the attributes it uses. The class variable called COLORS
is a dictionary—a data structure that contains key/value pairs with the number of hits that the brick has left, and the corresponding color. When a brick is hit, the method execution occurs as follows:
- The number of hits of the brick instance is decreased by
1
- If the number of hits remaining is
0
,self.delete()
deletes the brick from the canvas - Otherwise,
self.canvas.itemconfig()
changes the color of the brick
For instance, if we call this method for a brick with two hits left, we will decrease the counter by 1
and the new color will be #999999
, which is the value of Brick.COLORS[1]
. If the same brick is hit again, the number of remaining hits will become zero and the item will be deleted.