Lab Exercise 8: Designing Classes
This project continues our work with classes, but we're moving into the domain of physics simulations instead of biological simulations.
This is the first part of a multi-part project where we will look at increasingly complex physical simulations. This week we start with a ball and some blocks, focusing on gravity and ball-block collisions.
If you have not already done so, mount your personal space, create a new project8 folder and bring up TextWrangler and a Terminal. Then download the Zelle graphics package file graphicsPlus.py.
You may also want to grab the Zelle documentation.
- Explore the Zelle Graphics package
The Zelle graphics package lets us create a window, create graphical shapes in the window, draw text, and obtain user input like key presses and mouse clicks. Each of the Zelle graphics elements is an object, and the methods available for each object are described in the Zelle documentation.
First, let's create a window and wait for a mouse click to close it.
- Create a new file, import graphicsPlus (as gr), import time, import random, and save it as explore.py.
- Define a function test1 that will execute if the file is executed.
- Inside the main function create a GraphWin object and assign it to a variable (e.g. win. The GraphWin function takes four arguments: a title, the width of the window, the height of the window, and whether to automatically update the window. Use the values "test window", 500, 500, False.
- After creating the window, use the window variable to execute the method getMouse(). Assign the return value of getMouse to a variable (e.g. pos). The GraphWin getMouse function waits for the user to click the mouse in the window and returns the location of the click.
- Print the value (i.e. pos) returned by getMouse. The type of the value returned is a graphics Point object.
- Print the X and Y values of the Point object individually. To access the X value of a Point object use getX(). To access the Y value of a Point object, use getY().
- Close the window by using the window variable to call the close method.
Test your program. It should open a window, wait for a mouse click, print the location of the mouse click and quit.
Second, let's create a window and draw a Circle wherever the user clicks their mouse.
- Define a second function test2(). Edit your code so that test2 will execute when the file is executed.
- Create a window just like in test1.
- Create an empty list and assign it to a variable (e.g. shapes).
- Start a while loop that runs forever
- Inside the while loop, assign to a variable (e.g. pos) the result of using the window variable to call the method checkMouse(). The checkMouse function returns a position if the user has clicked the mouse recently or None if there is no mouse click to report.
- If the return value from checkMouse is not equal to None, then do the following.
- Create a graphics Circle object and assign it to a variable (e.g. c). Use the mouse click Point object as the first argument and a radius (e.g. 10) as the second.
- Add the circle Object to the shapes list.
- Set the fill color of the circle using the objects setFill method.
- Use the circle Object variable to call the draw() method with the window as the argument.
- Outside the if statement, but inside the while loop, assign to a variable (e.g. key) the result of using the window variable to call the method checkKey(). The checkKey function returns a string with a character or string if someone has typed something.
- If the key is equal to 'q', then break (exit the loop).
- Outside the if statement, but inside the loop, use the window variable to call the update method.
- Still inside the loop, call time.sleep(0.033)
- After the loop, close the window.
Run your program. It should create a circle whenever the user clicks in the window, and it should quit if the user types 'q'.
How could you update the test2 function so the balls move randomly after being created?
- Define a Ball class and its __init__ method
Create a file named physics_objects.py. Your first task in the lab is to create a class Ball that will represent a ball in your simulation. Since this is a physics simulation, you will want to represent all of its relevant attributes such as mass, radius, position, velocity, and acceleration.
The __init__ method should have self and a GraphWin object (win) as its only non-optional argument. You are free to add optional arguments such as position and radius, but none are required. Inside the __init__ method, create fields to hold each of the following pieces of information.
- self.mass - give it an initial value of 1.
- self.radius - give it an initial value of 1.
- self.position - this will be a two-element list, representing the x and y location values. Give the x and y positions initial values of 0.
- self.velocity - a two-element list, with initial values of 0.
- self.acceleration - a two-element list, with initial values of 0.
- self.win - a GraphWin object (the win parameter).
In addition to the physical parameters, each Ball object needs to know how to draw itself to a visualization window. We want to be able to use different units for the physical parameters versus the visualization parameters, so add a scale field to self and give it an initial value of 10. The scale will transform a position in simulation coordinates to screen coordinates.
We also need to add the graphics object that will represent the ball on the screen. Create a field vis of self and assign it a list with one element, which is a Graphics Circle object.
A Circle object, needs an anchor point for its center and a radius. For the anchor point x and y values use the Ball's position x value multipled by the scale field.
The y position needs adjustment because the screen coordinates are different than the physics coordinates. In the physics coordinates a move in the positive y direction is up, but on the screen coordinates a move in the positive y direction is down (the upper left corner of the screen is at position (0,0)). This means, that whenever we translate from physics coordinates to screen coordinate, we need to subtract the physics y position from the window's height.
Therefore, the proper y coordinate for the Graphics Circle is win.getHeight() minus the product of the Ball's position y value and the scale field.
The final parameter for the Circle-creation function is the radius, which should be the Ball's radius value multiplied by the scale field. The entire statement might look like the following, depending on what you named your fields and how you imported the graphics module.
self.vis = [ gr.Circle( gr.Point(self.pos*self.scale, win.getHeight()-self.pos*self.scale), self.radius * self.scale ) ]
- Write a draw method
The method should loop over the self.vis list and have each item call its draw method, passing in the window (self.win) field.
- Write get and set methods for the Ball class
Create get and set methods for each of the physical attributes of the Ball. For example, the getMass method should return the value of the mass field of the object. The setMass method should take in a new value as one of the parameters and assign it to the mass field of the object.
When writing get methods, you want to avoid returning the internal list. For example, if the following getPosition method returns a reference to the list being used by the object to store its position.
# a bad example def getPosition(self): return self.position
By returning a reference to the list, another program can now change the values stored in self.position. Instead, return a copy of the list.
# a good example def getPosition(self): return self.position[:]
By returning a copy, another program can't unexpectedly edit the Ball's internal list.
Use the following definitions for your get/set methods. You must follow these specifications or the test files will not work without modification.
- def getPosition(self): # returns a 2-element tuple with the x, y position.
- def setPosition(self, px, py): # px and py are the new x,y values
- def getVelocity(self): # returns a 2-element tuple with the x and y velocities.
- def setVelocity(self, vx, vy): # vx and vy are the new x and y velocities
- def getAcceleration(self): # returns a 2-element tuple with the x and y acceleration values.
- def setAcceleration(self, ax, ay): # ax and ay are new x and y accelerations.
- def getMass(self): # Returns the mass of the object as a scalar value
- def setMass(self, m): # m is the new mass of the object
- def getRadius(self): # Returns the radius of the Ball as a scalar value
- def setRadius(self, r): # (**Optional**) r is the new radius of the Ball object. Note, this function will need to undraw the circle, create a new circle with the new radius, and draw it back into the window.
The only tricky methods are setPosition and setRadius, which require not only updating the appropriate field, but also moving or changing the visualization.
+ (more detail)
If a Circle is in screen space location A and you want it to be in screen space location B, then moving the object by the amount (B - A) does what you want to do. To calculate B-A, calculate the difference in simulation space and then multiply by the scale factor for x and by the negative scale factor for y.
def setPosition(self, px, py): # assign to x_old the current x position # assign to y_old the current y position # assign to the x coordinate in self.pos the new x coordinate # assign to the y coordinate in self.pos the new y coordinate # assign to dx the change in the x position times self.scale # assign to dy the change in the y positino times -self.scale # for each item in the vis field of self # call the move method of the item, passing in dx and dy
Once you have completed this step, download the file testBall_1.py. The test function has code to test some of the get/set functions, but it is not complete. As you write get/set functions, add test code to the function to make sure your functions work properly.
- Write an update method that implements Newtonian physics
Write a method, update that adjusts the internal position and velocity values based on current accelerations and forces. The method should use the equations of motion under uniform acceleration to update the velocity and position. The method also needs to move the visualization. The function will take in a time step, dt that indicates how much time to model.
+ (more detail)
The following is a step-by-step algorithm for updating the ball.
def update(self, dt): # assign to x_old the current x position # assign to y_old the current y position # update the x position to be x_old + x_vel*dt + 0.5*x_acc * dt*dt # update the y position to be y_old + y_vel*dt + 0.5*y_acc * dt*dt # assign to dx the change in the x position times the scale factor (self.scale) # assign to dy the negative of the change in the y position times the scale factor (self.scale) # for each item in self.vis # call the move method of the graphics object with dx and dy as arguments.. # update the x velocity by adding the acceleration times dt to its old value # update the y velocity by adding the acceleration times dt to its old value
To test the update method, uncomment the last section of the testBall main function. It should implement Brownian motion of the ball.
- Write a test file for the Ball class
Write a test file that will create a Ball in the center of the screen, give it a random initial velocity and an acceleration of (0, -20), and then loop until the ball leaves the screen, calling the ball's update function inside the loop. If the ball goes out of bounds, the program should reposition the ball to the center of the screen and give it a new random velocity.
If you wish, you can use fall.py as a template.
Test your code and make sure you have a ball falling down, then re-spawning.
When you are done with the lab exercises, you may begin the project.