Step 4: Show a running score¶
This is a small step in concept: keep count of how many bricks have been knocked out and show that score underneath the game window.
But it involves two changes:
- Keeping track of the state of the game – in this case, the score.
- Putting the gameplay inside a window and other information outside it
First we’ll push the gameplay inside a window, in two steps. Then we’ll add a status line and keep track of the score.
Step 4a: Create a gameplay window¶
Note
At the end of this step, the game will still be showing across the entire window but the new gameplay window will show where it will be showing at the end of the next step.
The Code¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | WIDTH = 640 HEIGHT = 480 # # Create a game window which can be smaller than the # screen with its own background & frame colours. # GAME_WINDOW = ZRect(0, 0, WIDTH, HEIGHT) GAME_WINDOW.inflate_ip(-50, -50) GAME_WINDOW.background_colour = "darkblue" GAME_WINDOW.frame_colour = "white" class Ball(ZRect): pass # # The ball is a red square halfway across the game screen # ball = Ball(0, 0, 30, 30) ball.center = WIDTH / 2, HEIGHT / 2 ball.colour = "red" # # The ball moves one step right and one step down each tick # ball.direction = 1, 1 # # The ball moves at a speed of 3 steps each tick # ball.speed = 3 class Bat(ZRect): pass # # The bat is a green oblong which starts just along the bottom # of the screen and halfway across. # BAT_W = 150 BAT_H = 15 bat = Bat(WIDTH / 2, HEIGHT - BAT_H, BAT_W, BAT_H) bat.colour = "green" class Brick(ZRect): pass # # The brick is a rectangle one eight the width of the game screen # and one quarter high as it is wide. # N_BRICKS = 8 BRICK_W = WIDTH / N_BRICKS BRICK_H = BRICK_W / 4 BRICK_COLOURS = ["purple", "lightgreen", "lightblue", "orange"] # # Create <N_BRICKS> blocks, filling the full width of the screen. # Each brick is as high as a quarter of its width, so they remain # proportional as the number of blocks or the screen size changes. # # The brick colours cycle through <BRICK_COLOURS> # bricks = [] for n_brick in range(N_BRICKS): brick = Brick(n_brick * BRICK_W, 0, BRICK_W, BRICK_H) brick.colour = BRICK_COLOURS[n_brick % len(BRICK_COLOURS)] bricks.append(brick) def draw(): # # Clear the screen and place the ball at its current position # screen.clear() # # Draw the game window and a frame around it # screen.draw.filled_rect(GAME_WINDOW, GAME_WINDOW.background_colour) screen.draw.rect(GAME_WINDOW.inflate(+2, +2), GAME_WINDOW.frame_colour) screen.draw.filled_rect(ball, ball.colour) screen.draw.filled_rect(bat, bat.colour) for brick in bricks: screen.draw.filled_rect(brick, brick.colour) def on_mouse_move(pos): # # Make the bat follow the horizontal movement of the mouse. # x, y = pos bat.centerx = x def update(): # # Move the ball along its current direction at its current speed # dx, dy = ball.direction ball.move_ip(ball.speed * dx, ball.speed * dy) # # Bounce the ball off the bat # if ball.colliderect(bat): ball.direction = dx, -dy # # If the ball hits a brick, kill that brick and # bounce the ball. # to_kill = ball.collidelist(bricks) if to_kill >= 0: bricks.pop(to_kill) ball.direction = dx, -dy # # Bounce the ball off the left or right walls # if ball.right >= WIDTH or ball.left <= 0: ball.direction = -dx, dy # # If the ball hits the bottom of the screen, you lose # if ball.bottom >= HEIGHT: exit() # # Bounce the ball off the top wall # if ball.top <= 0: ball.direction = dx, -dy # # If there are no bricks left, you win # if not bricks: exit() |
What’s happening?¶
- We’re creating a rectangle to hold the gameplay window which is is 50 pixels smaller than the screen which holds the entire game.
- To create a blue filling and a white border, we draw a filled-in rectangle in blue and then a slightly larger, unfilled rectangle in white which fits around it.
Change it around¶
Change the colours of the gameplay window
Make the gameplay window not be centred on the screen.
Hint: The line which “inflates” the gameplay rectangle by 50 pixels in each direction is just a shortcut for playing with its width and height and x and y attributes. You can change them directly.
Step 4b: Move the gameplay inside the gameplay window¶
The Code¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | WIDTH = 640 HEIGHT = 480 # # Create a game window which can be smaller than the # screen with its own background & frame colours. # GAME_WINDOW = ZRect(0, 0, WIDTH, HEIGHT) GAME_WINDOW.inflate_ip(-50, -50) GAME_WINDOW.background_colour = "darkblue" GAME_WINDOW.frame_colour = "white" class Ball(ZRect): pass # # The ball is a red square halfway across the game window # ## DELETE --> ball = Ball(0, 0, 30, 30) ball = Ball((0, 0), (30, 30)) ## DELETE --> ball.center = WIDTH / 2, HEIGHT / 2 ball.center = GAME_WINDOW.center ball.colour = "red" # # The ball moves one step right and one step down each tick # ball.direction = 1, 1 # # The ball moves at a speed of 3 steps each tick # ball.speed = 3 class Bat(ZRect): pass # # The bat is a green oblong which starts just along the bottom # of the game window and halfway across. # BAT_W = 150 BAT_H = 15 ## DELETE --> bat = Bat(WIDTH / 2, HEIGHT - BAT_H, BAT_W, BAT_H) bat = Bat(GAME_WINDOW.centerx, GAME_WINDOW.bottom - BAT_H, BAT_W, BAT_H) bat.colour = "green" class Brick(ZRect): pass # # The brick is a rectangle one eight the width of the game window # and one quarter high as it is wide. # N_BRICKS = 8 ## DELETE --> BRICK_W = WIDTH / N_BRICKS BRICK_W = GAME_WINDOW.width / N_BRICKS BRICK_H = BRICK_W / 4 BRICK_COLOURS = ["purple", "lightgreen", "lightblue", "orange"] # # Create <N_BRICKS> blocks, filling the full width of the game window. # Each brick is as high as a quarter of its width, so they remain # proportional as the number of blocks or the screen size changes. # # The brick colours cycle through <BRICK_COLOURS> # bricks = [] for n_brick in range(N_BRICKS): ## DELETE --> brick = Brick(n_brick * BRICK_W, 0, BRICK_W, BRICK_H) brick = Brick( GAME_WINDOW.left + (n_brick * BRICK_W), GAME_WINDOW.top, BRICK_W, BRICK_H ) brick.colour = BRICK_COLOURS[n_brick % len(BRICK_COLOURS)] bricks.append(brick) def draw(): # # Clear the screen, draw the game window and place the ball at its current position # screen.clear() # # Draw the game window and a frame around it # screen.draw.filled_rect(GAME_WINDOW, GAME_WINDOW.background_colour) screen.draw.rect(GAME_WINDOW.inflate(+2, +2), GAME_WINDOW.frame_colour) screen.draw.filled_rect(ball, ball.colour) screen.draw.filled_rect(bat, bat.colour) for brick in bricks: screen.draw.filled_rect(brick, brick.colour) def on_mouse_move(pos): # # Make the bat follow the horizontal movement of the mouse. # Ensure that the bat does not move outside the game window. # x, y = pos bat.centerx = x bat.clamp_ip(GAME_WINDOW) def update(): # # Move the ball along its current direction at its current speed # dx, dy = ball.direction ball.move_ip(ball.speed * dx, ball.speed * dy) # # Bounce the ball off the bat # if ball.colliderect(bat): ball.direction = dx, -dy # # If the ball hits a brick, kill that brick and # bounce the ball. # to_kill = ball.collidelist(bricks) if to_kill >= 0: bricks.pop(to_kill) ball.direction = dx, -dy # # Bounce the ball off the left or right walls # ## DELETE --> if ball.right >= WIDTH or ball.left <= 0: if ball.right >= GAME_WINDOW.right or ball.left <= GAME_WINDOW.left: ball.direction = -dx, dy # # If the ball hits the bottom wall, you lose # ## DELETE --> if ball.bottom >= HEIGHT: if ball.bottom >= GAME_WINDOW.bottom: exit() # # Bounce the ball off the top wall # ## DELETE --> if ball.top <= 0: if ball.top <= GAME_WINDOW.top: ball.direction = dx, -dy # # If there are no bricks left, you win # if not bricks: exit() |
What’s happening?¶
This is quite a busy step, but a lot of the changes are mechanical substitutions.
- In short, wherever we were previously assuming that we were playing within
the whole width of the game screen, we now have to assume that we are
playing only within the gameplay window. So, for example, checking whether
the ball has hit the left-hand edge of the screen (
ball.left <= 0
) now has to check instead whether we’ve hit the left-hand edge of the gameplay window (ball.left <= GAME_WINDOW.left
). - One particular change is to keep the bat within the gameplay
window. By default, the mouse will stay within the game screen so we
didn’t have to do anything to stop the bat going too far to the left or right.
Now, though, if we don’t “clamp” the bat within the gameplay window, it would
start to move outside its borders if the mouse is moved too far. You can
see this change in the
on_mouse_move
code.
Change it around¶
- Allow the bat to move outside the gameplay window to see what happens
- Have the bricks occupy a smaller width than the entire width of the gameplay window.
Step 4c: Add a running score count¶
The Code¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | WIDTH = 640 HEIGHT = 480 class Game(object): pass game = Game() game.score = 0 # # Create a status display, as wide as the screen and 60 pixels high. # It's placed at the bottom of the screen # ## DELETE --> GAME_WINDOW = ZRect(0, 0, WIDTH, HEIGHT) ## DELETE --> GAME_WINDOW.inflate_ip(-50, -50) STATUS_DISPLAY = ZRect(0, HEIGHT - 60, WIDTH, 60) # # Create a game window which is as wide as the screen but allows # a status display underneath # GAME_WINDOW = ZRect(0, 0, WIDTH, HEIGHT - STATUS_DISPLAY.height - 1) GAME_WINDOW.background_colour = "darkblue" GAME_WINDOW.frame_colour = "white" class Ball(ZRect): pass # # The ball is a red square halfway across the game window # ball = Ball((0, 0), (30, 30)) ball.center = GAME_WINDOW.center ball.colour = "red" # # The ball moves one step right and one step down each tick # ball.direction = 1, 1 # # The ball moves at a speed of 3 steps each tick # ball.speed = 3 class Bat(ZRect): pass # # The bat is a green oblong which starts just along the bottom # of the game window and halfway across. # BAT_W = 150 BAT_H = 15 bat = Bat(GAME_WINDOW.centerx, GAME_WINDOW.bottom - BAT_H, BAT_W, BAT_H) bat.colour = "green" class Brick(ZRect): pass # # The brick is a rectangle one eight the width of the game window # and one quarter high as it is wide. # N_BRICKS = 8 BRICK_W = GAME_WINDOW.width / N_BRICKS BRICK_H = BRICK_W / 4 BRICK_COLOURS = ["purple", "lightgreen", "lightblue", "orange"] # # Create <N_BRICKS> blocks, filling the full width of the game window. # Each brick is as high as a quarter of its width, so they remain # proportional as the number of blocks or the screen size changes. # # The brick colours cycle through <BRICK_COLOURS> # bricks = [] for n_brick in range(N_BRICKS): brick = Brick( GAME_WINDOW.left + (n_brick * BRICK_W), GAME_WINDOW.top, BRICK_W, BRICK_H ) brick.colour = BRICK_COLOURS[n_brick % len(BRICK_COLOURS)] bricks.append(brick) def draw(): # # Clear the screen, draw the game window and place the ball at its current position # screen.clear() # # Draw the game window and a frame around it # screen.draw.filled_rect(GAME_WINDOW, GAME_WINDOW.background_colour) screen.draw.rect(GAME_WINDOW.inflate(+2, +2), GAME_WINDOW.frame_colour) # # Show the current status, centred inside the status area # screen.draw.text("Score: %d" % game.score, center=STATUS_DISPLAY.center) screen.draw.filled_rect(ball, ball.colour) screen.draw.filled_rect(bat, bat.colour) for brick in bricks: screen.draw.filled_rect(brick, brick.colour) def on_mouse_move(pos): # # Make the bat follow the horizontal movement of the mouse. # Ensure that the bat does not move outside the game window. # x, y = pos bat.centerx = x bat.clamp_ip(GAME_WINDOW) def update(): # # Move the ball along its current direction at its current speed # dx, dy = ball.direction ball.move_ip(ball.speed * dx, ball.speed * dy) # # Bounce the ball off the bat # if ball.colliderect(bat): ball.direction = dx, -dy # # If the ball hits a brick, kill that brick and # bounce the ball. # to_kill = ball.collidelist(bricks) if to_kill >= 0: bricks.pop(to_kill) game.score += 1 ball.direction = dx, -dy # # Bounce the ball off the left or right walls # if ball.right >= GAME_WINDOW.right or ball.left <= GAME_WINDOW.left: ball.direction = -dx, dy # # If the ball hits the bottom wall, you lose # if ball.bottom >= GAME_WINDOW.bottom: exit() # # Bounce the ball off the top wall # if ball.top <= GAME_WINDOW.top: ball.direction = dx, -dy # # If there are no bricks left, you win # if not bricks: exit() |
What’s happening?¶
We’re introducing the concept of game state: a programming object which keeps track of various aspects of the game. In this step, we’re just tracking the score; in the next step, we’ll use this to introduce the idea of different screens (a “welcome” screen, a “scoreboard” screen etc.).
- We create a global game object whose only attribute is a score, which we initialise to zero.
- We add a status window: another on-screen rectangle at the bottom of the display. We adjust the size of the gameplay window to fit alongside it.
- To keep track of the score, we simply add one to the score every time a brick is knocked out.
- The screen.draw.text function is a very flexible way of getting text onto the screen.
Change it around¶
Have each brick colour give a different score
Hint: bricks.pop returns the brick you’ve just popped. You can get a brick’s colour from its .colour attribute.
Play around with the text: make it left-aligned, a different colour, a different font, &c.