Lab 11: Event-based programming

CS151: Computational Thinking: Visual Media
Spring 2019

Goals

In this lab, you will explore principles from event-based programming and create a simple video game that roughly approximates the 70s arcade game Asteroids. Specifically, we will use timer events, keyboard button events, and collision detection.

As the gateway into the final project, this lab will review concepts we've learned all semester: lists, objects and object-oriented design, classes, inheritance, top-down design, and more.

Setup

  1. Make a new folder called Project 11. Copy over turtle_interpreter.py, lsystem.py, and shapes.py from Project 10.
  2. Copy over a recent scene file that you made from Project 9 or 10 (e.g. home.py or mosaic.py). This will be the background artwork for your video game!
  3. Copy over any L-system txt files that your scene or shapes depend on.

TurtleInterpreter

  1. We are going object-oriented with turtle in this project! Replace your call to turtle.setup() in the TurtleInterpreter constructor with self.screen = turtle.Screen(). This creates an object of type Screen, which represents the turtle canvas. Call the setup() method on the Screen object, passing in your desired canvas size (e.g. dx, dy).

    Make sure you do this step AFTER checking whether the TurtleInterpreter has been initialized!
  2. Create a get method getScreen for the screen instance variable.

Shape

  1. Move the creation of your TurtleInterpreter object from draw to the constructor. Make it an instance variable of Shape. Update your function calls in draw to refer to your TurtleInterpreter instance variable.
  2. Create a get method getScreen that calls the getScreen method from your TurtleInterpreter instance variable. This method will pass the Screen object from the TurtleInterpreter to your game.

Game: Background artwork

  1. Open the file with the scene that you copied from a prior project (e.g. home.py).
  2. Make sure that the scene that you want to be the background artwork for your game is fully encapsulated in a function (if it's not, fix it). There should not be any top-level code in this file.
  3. Remove all direct references to TurtleInterpreter/terp in your file (this includes removing calls to hold()!). You should only work with Shape objects in your function.
  4. After creating your first Shape object, call its getScreen method and assign the screen object that it returns to a variable. For example:
sky = shapes.Square(distance=1000, fill=True)
screen = sky.getScreen()
  1. At the end of the scene function, return the screen object.
  2. Create a new file game.py.
  3. Within the new file, create a:
  1. Add the following run method to your Game class:
def run(self):
    '''
    Turns the tracer animations on (but speeds up animations) and starts the main game loop.
    '''
    # Make sure animations are on but fast
    turtle.tracer(True)
    turtle.speed(0)

    # Let's go!
    self.screen.mainloop()
  1. Run game.py. If everything is working so far, you should simply see your background artwork scene drawn to the screen (no game elements yet).

Game: Player Turtle

  1. Create the makePlayer method in Game. It should
turtle.register_shape('rocketship.gif')  # Make the rocketship image available to turtle
turt.shape('rocketship.gif')  # Set the turtle icon to the rocketship image
  1. Call the makePlayer method from your constructor (after you setup the background artwork), assigning the output to an instance variable (e.g. self.player).
  2. Run your game. You should now see your rocketship in the scene.

Game: Interactive player control

We're going to make the player turtle (rocketship) move under keyboard control!

  1. Add instance variables for player speed (units: pixels) and turn rate (units: degrees) in the Game constructor. You might wish to create optional parameters for these, giving them default values of 20 and 10, respectively.
  2. Create the following methods in Game:
  1. Create the setupEvents method that creates your game events. Add keyboard callback events to the four movement methods you just created. Here, callback means that we will instruct our game to move our player by calling one of movement methods when we press the appropriate keyboard button. To add a callback, we call the onkey method of the Screen object. It has the following syntax: self.screen.onkey(<Method name>, <String of keyboard button>). For example self.screen.onkey(self.up, "Up") for moving the player up. To assign to the four arrow keys, use the strings: "Up", "Down", "Left", "Right".
  2. Add a callback to call the quit function when the user presses the "q" key.
  3. Call the listen method on your screen object so that it will start calling the movement methods when you press the arrow keys.
  4. Call setupEvents at the end of the constructor.
  5. Run your game. You should now be able to move your player around the scene!

Game: Enemies!

In this task, we will create a number of enemy turtle objects that move randomly.

  1. In your constructor, create four hard-coded (x, y) bounds instance variables, defining the rectangular region in which the game enemies will occupy. For example, this could roughly correspond to the top third of your screen.
  2. Create a method with the signature def placeEnemyRandomly(self, turt):, which will move the turt object to random coordinates within the enemy bounds you defined in (1).
  3. Create a method with the signature def makeEnemies(self, n): It should
  1. Call makeEnemies from your Game constructor, assign the return value to an instance variable (e.g. self.enemies). Either hard code n (number of enemies) or make it an (optional) parameter in your constructor.
  2. Run your game. You should see stationary enemy square objects in their designated zone.

Game: Enemies on the move

In this task, we will create a timer event to randomly move the enemies over time.

  1. Create the method def moveEnemiesRandomly(self):. It should set each enemy's position to the current position plus a small random (x, y) offset.
  2. In setupEvents, create a timer that will automatically call moveEnemiesRandomly 50 msec after the game starts. The syntax is: self.screen.ontimer(<method name to call>, <int: wait time in msec>).
  3. The ontimer only will call the moveEnemiesRandomly method ONCE 50 msec in the future, but we want to continue moving them every 50 msec indefinitely. To do this, add an identical timer at the end of moveEnemiesRandomly. Make sure that you understand why we're doing this before continuing.
  4. Run your game. You should see your enemies moving on the screen!

Game: Collision detection

We will create another timer event to have the enemy take an action when the player collides with it.

  1. Add an instance variable to the constructor that defines how close the player needs to get to an enemy for the game to register that a collision happened (collision radius). You may wish to make an (optional) parameter of the constructor. A reasonable default value is 30 pixels.
  2. Create the method: def checkForCollisions(self):. It should
  1. Add a timer callback in setupEvents to check for collisions.
  2. Add an identical timer callback at the end of checkForCollisions.
  3. Run your game. Test it by colliding with an enemy. Something should happen!

When your interactive elements are working, you’re ready for Project 11: Video Game Design!

© Caitrin Eaton & Oliver W. Layton