Project 2: Breakout

Due: Monday, 21 October, 11:59 PM

Github Classroom Link: https://classroom.github.com/a/4dhFPQYJ

Prof. Jed Rembold's alternate instructions


A. Introduction [Show]

A. Introduction [Hide]

This project is composed of 5 milestones, each roughly equivalent to a problem or part of a problem on a problem set. Distinct from problem sets, each of the problems works together to create one, single, more expansive assignment. Before getting the milestones, there are a few introductory remarks.

Additionally, this project will involve working with a "library" not unlike karel.py called pgl.py. This project specification will specify how to interact with this file.


Files:

  • Update (only) this file:
    • Breakout.py
  • You will also need:
    • pgl.py

You should be able to run Breakout.py as soon as you download the code. It will create an empty graphics window. The structure of Breakout.py was covered extensively in the "Factoring" lecture here with the only difference being how many more variables are set beyond just the size of the graphics window.


Resources on PGL (Portable Graphics Library):


B. What is Breakout [Show]

B. What is Breakout [Hide]

Breakout is a unique program that is driven by two events: a timing event, which simulates the motion of a moving ball, a mouse-move event, which simulate the motion of a "paddle" that controls the ball. Working with bothing timing and mouse events at the same time is very exciting, and forms the field of "real-time computing" which I worked in during my graduate program! In practice, it is based around two event listeners. My version also uses a third, very simple listener that only listens for clicks and unpauses the game on click, is discussed briefly in milestone 2.

There is an Web Demo of each milestone.

The initial configuration is rows of 'bricks', a centered 'ball', and a centered paddle, as in the leftmost image. The paddle is in a fixed position in the vertical direction, but moves back and forth across the screen along with the mouse until it reaches the edges of the window.

A complete game consists of three turns. On each turn, a ball is launched from the center of the window toward the bottom of the screen at a random angle. That ball bounces off the paddle and the walls of the world. Thus, after two bounces–one off the paddle and one off the left wall–the ball might have the trajectory shown in the second image. (Note that the dotted line is only there to show the ball’s path and does not actually appear on the screen.)

As you can see in the second image, the ball is soon to collide with one of the bricks on the bottom row. When that happens, the ball bounces just at it does on any other collision, but the brick disappears. The third image shows what the game looks like after that collision and after the player has moved the paddle to put it in line with the oncoming ball. The fourth image shows this process happening repeatedly.


C. Starting Template [Show]

C. Starting Template [Hide]

The template file "Breakout.py" is initially as follows, composed almost exclusively of many numerical variable declarations:


from pgl import GWindow, GOval, GRect
import random

# Constants
GWINDOW_WIDTH = 360            # Width of the graphics window
GWINDOW_HEIGHT = 600           # Height of the graphics window
N_ROWS = 10                    # Number of brick rows
N_COLS = 10                    # Number of brick columns
BRICK_ASPECT_RATIO = 4 / 1     # Width to height ratio of a brick
BRICK_TO_BALL_RATIO = 3 / 1    # Ratio of brick width to ball size
BRICK_TO_PADDLE_RATIO = 2 / 3  # Ratio of brick to paddle width
BRICK_SEP = 2                  # Separation between bricks (in pixels)
TOP_FRACTION = 0.1             # Fraction of window above bricks
BOTTOM_FRACTION = 0.05         # Fraction of window below paddle
N_BALLS = 3                    # Number of balls (lives) in a game
TIME_STEP = 10                 # Time step in milliseconds
INITIAL_Y_VELOCITY = 3.0       # Starting y velocity downwards
MIN_X_VELOCITY = 1.0           # Minimum random x velocity
MAX_X_VELOCITY = 3.0           # Maximum random x velocity

# Derived Constants
BRICK_WIDTH = (GWINDOW_WIDTH - (N_COLS + 1) * BRICK_SEP) / N_COLS
BRICK_HEIGHT = BRICK_WIDTH / BRICK_ASPECT_RATIO
PADDLE_WIDTH = BRICK_WIDTH / BRICK_TO_PADDLE_RATIO
PADDLE_HEIGHT = BRICK_HEIGHT / BRICK_TO_PADDLE_RATIO
PADDLE_Y = (1 - BOTTOM_FRACTION) * GWINDOW_HEIGHT - PADDLE_HEIGHT
BALL_DIAMETER = BRICK_WIDTH / BRICK_TO_BALL_RATIO


# Function: breakout
def breakout():
    """The main program for the Breakout game."""

    gw = GWindow(GWINDOW_WIDTH, GWINDOW_HEIGHT)

    # You fill in the rest of this function along with any additional
    # helper and callback functions you need





# Startup code
if __name__ == "__main__":
    breakout()

The structure of this file is identical to the structure discussed in the "Factoring" lecture which you may review here.


0. Bricks [Show]

0. Bricks [Hide]

Milestone 0: Best By Tuesday

This task concerns code that is NOT within an event listener.

Bricks is specified using the all-caps variables in the starter file. The only value you need to compute is the horizontal, or 'x', coordinate of the first column to center the bricks. The colors of the bricks remain constant for two rows and run in the following rainbow-like sequence: "Red", "Orange", "Green", "Cyan", and "Blue". The milestone is complete when you have the following bricks placed appropriately:

I used nested for loops to lay bricks. My code was very similar to Problem Set 4: Problem 2.


for i in range(N_ROWS):
    for j in range(N_COLS):
        my_rect(...) # something like this

1. Paddle [Show]

1. Paddle [Hide]

Milestone 1: Best By Wednesday

This task concerns the event listener for mouse events.

Create a single rectangle corresponding to the "paddle", the black rectangle at the bottom of the graphics window of size and shape described by the all-caps variables in Breakout.py. The milestone is complete when the 'paddle' follows the mouse and yet does not go off the graphics window.

Outside of the event listener, I:

  • used the same technique as 'gw.box' from the "Click" lecture here to create a "gw.paddle".

Inside of the event listener for a "mousemove" event I:

  • checked the horizontal, or 'x' value of the mouse event.
  • checked this 'x' value to make sure it was between two values
    • If it was less than some minimum value, I set 'x' equal to that minimum value.
    • If it was greater than some maximum value, I set 'x' to that maximum value.
  • I set the location of the paddle based on the new, computed 'x' value and the provided, all-caps 'y' value.

2. Ball [Show]

2. Ball [Hide]

Milestone 3: Best By Thursday

This task concerns the event listener for timer events.

Create a single oval or many-sided polygon which corresponding to the "ball", of size described by the all-caps variables in Breakout.py. The milestone is complete when, after an inital click begins the game, the 'ball' moves around the graphics window, bouncing off of walls. It may pass through bricks and the paddle during this milestone.

Personally, I added my timer listener after creating the bricks, but after setting a "pause" variable to 'True'. A separate, very simple, event listener monitored clicks and set pausing to 'False' when a click occurred. My timer event would check for pauses before updating the ball location.

Be conscientious of how pgl often places shapes based on their top-left location rather than their center, and we addressed this with "Boxy Click" here.

Outside of the event listener, I:

  • used the same technique as 'gw.box' from the "Click" lecture here to create a "gw.ball".
  • additionally created "gw.vx" hold a number - the horizontal 'velocity' of the ball. Basically, this is how many pixels the ball moves each time a timer event happens.
  • additionally created "gw.vy" which is initially equal to an all-caps variable from "Breakout.py" - 'INITIAL_Y_VELOCITY'.

This is one way to set up the velocity.


gw.vx = random.uniform(MIN_X_VELOCITY, MAX_X_VELOCITY)
if random.uniform(0,1) < 0.5:
    gw.vx = -gw.vx

I created a timer event implemented bouncing much the same way implemented by "Bouncing Pacman" in the section here. Basically there is a listener which:

  • triggers every TIME_STEP
  • computes a new location vx pixels in the x direction and vy pixels in the y direction from the current location
  • determines if this new location is within a wall
    • if so, reverses the velocity in the direction of the wall (bounces).
      • if the wall is the left or right side, change the 'gw.vx' to equal '-gw.vx'.
      • if the wall is the top or bottom, change the 'gw.vy' to equal '-gw.vy'.

3. Collisions [Show]

3. Collisions [Hide]

Milestone 3: Best By Friday

This task concerns the event listener for timer events.

Check whether the ball is colliding with another object in the window, and update the graphics window appropriately if so. This milestone is complete when the ball may bounce off of the paddle and off of bricks, and when bricks are removed from the graphics window when the ball bounces off of them.

You will likely need to use gw.get_element_at(x,y), which either returns a shape or None, to determine if a brick or paddle is at certain location, and gw.remove(...) to remove bricks.

We recommend checking the corner points of the square that encloses the ball. For a GOval the upper left corner of the ball is at the point (x,y), the other corners will be at the locations:

I updated my timer event so that after checking for walls::

  • check for shapes at the four corner locations.
    • If there is a shape, vertical bounce by changing 'gw.vy' to equal '-gw.vy'.
    • If the shape is not the paddle, remove the shape.

You may wish to write a function get_colliding_object, which checks all four (if necessary) corners of the ball and returns the either a shape or None, which will make writing an 'if' statement to deal with collisions much easier.


4. Finalize [Show]

4. Finalize [Hide]

Milestone 4: Best By Saturday

This task concerns the event listener for timer events.

For this task add some finishing touches:

  • If the ball reaches the bottom, it should not bounce, but be deleted and trigger the next 'turn'.
  • The next 'turn' should be begin with the game in a paused state, the ball in the initial starting position, and one fewer turn remaining.
  • If all bricks are removed, the game should end, likely by pausing - anything more sophisticated would be an extension.

Once you have completed Milestone 4, submit your code via Github classroom. You will probably want to test a bit more, and also consider doing extensions, which are outline by Prof. Jed Rembold here