CS 152: Lab 7

Title image Project 7
Spring 2020

Lab Exercise 7: Working with Objects

This project is intended to introduce you to working with existing classes. The domain for this project and all of the remaining projects will be physics simulations: modeling position, velocity, acceleration, rotation, and collisions. Each week our simulations will get slightly more complex. This week we start with a ball and some blocks, focusing on modeling gravity and detecting ball-block collisions.


Tasks

If you have not already done so, mount your personal space, create a new project7 folder and start your text editor and a Terminal. Then download the Zelle graphics package file graphicsPlus.py.

You may also want to grab the Zelle documentation, which is useful to have open for quick access.

Objects

The Zelle graphics package follows what is called object-oriented design [OOD]. The package allows us to create a window and then draw shapes into the window and move them around. The window and each shape (e.g. Point, Circle, Rectangle) are all encapsulated as objects.

An object is a data structure in memory that contains both variables (called fields) and references to functions (called methods). Using our vocabulary for representing memory in Python, an object is a symbol table where each name or method is an entry in the table.

+ (more detail on objects)

To create an object, we use the object's constructor function from the graphics package. For example, to create a window we need to create a GraphWin object and assign it to a variable. The GraphWin constructor function is part of the graphics package, so we have to use gr. as a prefix to specify where the GraphWin function is defined as in the example below.

win = gr.GraphWin( 'title', 500, 500, False )

Objects contain methods (functions associated with an object) that tell the object to do something. For example, we can tell the GraphWin object to wait for a mouse click by using the getMouse() method. To call an object's method, use the variable holding the object followed by a . and the name of the method. Another way to think about it is that the object is a symbol table, and the method is defined in the object's symbol table.

win.getMouse()

Video 1: Working with Objects (also in Courses/CS152/Course_Materials/Lab_Videos)

  1. Explore the Zelle Graphics package

    The Zelle graphics package lets us create a window, create graphical shapes in the window, draw text, move the shapes or 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, create a window, draw a Circle in the middle of it, and wait for a mouse click to close it.

    1. Create a new file circles.py

      After your header, import graphicsPlus as gr and import random.

    2. Define a function makeCircle()

      This function should create a window, create a Circle, draw the circle, then wait for a mouse click, then close the window.

      + (more detail)

      1. Create a GraphWin object to make a window.

        To create an object we use the name of the object's class as a function (e.g. GraphWin()) and assign the result to a variable (e.g. win). The function that creates an object is called the constructor. The GraphWin constructor 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.

        win = gr.GraphWin( 'test window', 500, 500, False )

      2. Create a Circle object.

        Create a Circle object and assign it to a local variable like circle. The Circle constructor takes two arguments: a Point specify the circle's center, and a radius. To make a Circle at 100, 200, the first argument needs to be a Point object at location 100, 200. To create a Point object use the syntax gr.Point(100, 200).

        circle = gr.Circle( gr.Point( 100, 200 ), 10 )

      3. Draw the Circle into the window.

        Creating the Circle object does not actually draw it into the window. To draw a graphics object, use the object's draw method with the window as the argument.

        circle.draw( win )

      4. Update the window.

        Because we specified the last argument to GraphWin as False, we need to call the update method of the window in order to see the results of any drawing commands.

        win.update()

      5. Wait for a mouse click.

        After drawing the Circle, use the window variable to execute the method getMouse(). The GraphWin getMouse method waits for the user to click the mouse in the window and returns the location of the click.

        win.getMouse()

      6. Close the window.

        To close the window, use the window variable to call the close method.

        win.close()

    3. Test yor program.

      Put a call to makeCircle() at the bottom of your file within the standard conditional

      if __name__ == "__main__":
          makeCircle()

      The program should open a window, draw a Circle, wait for a mouse click, then quit.

    4. Specify the color of the ball.

      Afer creating the circle, but before drawing it into the window, use the Circle object's setFill and setOutline methods to specify the color of these features of the circle.

      To set the fill color to green, for example, you could use the following.

      circle.setFill( 'green' )

      You can also specify the red, green, and blue values if you use the color_rgb function from the graphics package.

      circle.setFill( gr.color_rgb( 40, 200, 50 ) )

      To set the outline color, use the setOutline method instead of setFill. Add code to change the color of your Circle and then test your program.

    5. Make the circle move over time.

      After the update and before the getMouse, start a definite for loop that executes 125 times. Inside the loop, use the circle's move method to make it move 1 in x and 2 in y (down and somewhat right). Then call time.sleep(0.01) to keep the loop from running too fast and call win.update() to make the circle re-draw.

      for t in range(250):
          circle.move( 0.5, 1 )
          time.sleep( 0.01 )
          win.update()

      Save and test your program.

    6. Have some fun and create lots of circles of different colors.

      At the start of your makeCircle function, assign to the variable circle_list the empty list. We're going to use this list to hold a bunch of circle objects.

      circle_list = []

      Put the part of your code that creates a Circle, sets its color, and draws it into the window inside of a for loop that executes 50 times.

      Instead of placing each Circle at 100, 200, use the random.randint() function to place the circle in the range 0, 500 in both x and y.

      circle = gr.Circle( gr.Point(random.randint(0,500), random.randint(0,500)), 10 )

      Instead of making all the circles the same color, use random.randint(0, 255) to make random red, green, and blue values for the gr.color_rgb function.

      circle.setFill( gr.color_rgb(random.randint(0, 255), 
                                   random.randint(0, 255), 
                                   random.randint(0, 255)))

      After setting the circle's color, append circle to circle_list. At the end of the loop, circle_list should contain 50 Circle objects.

      Run your code again and see if you get 50 circles of different colors at random locations. One of the circles should still move.

    7. Make all of your circles move

      You already have a loop that executes 125 times. Rather than move only one Circle, however, we want to move all 50 circles in circle_list. Start by deleting the line circle.move( 1, 2 ).

      Replace that line with a for loop that loops over circle_list. Use a good name for your loop variable, like circle. Since circle_list is a list of Circle objects, that means your loop variable will be a Circle object. Inside the loop, use the loop variable to move that Circle object randomly downward.

      for circle in circle_list:
          circle.move(random.randint(-5, 5), random.randint(0, 8))

      Save and test your code.

    Review all the things we have learned to do so far.

    1. How to create a window using the gr.GraphWin function and assign it to a local variable.
    2. How to create Circle and Point objects using gr.Circle and gr.Point.
    3. How to call a method of a graphics object like setFill, draw, or move.
    4. How to tell a graphics object to draw into a window using the object's draw method.
    5. How to store graphics objects in a list.
    6. How to use a loop over a list of graphics objects to execute an action on each object.
    7. How to wait for a mouse click using the getMouse method.
    8. How to force the window to update all of the drawing commands using the update method.

  2. A Simple Physics Simulation

    Let's create a simple physics simulation that sends a ball on a parabolic trajectory. In order to do this, we need to keep track of the ball's position, velocity, and acceleration.

    To visualize the ball we also need to keep track of any graphics objects we use, the window into which they are drawn, and whether they have been drawn. We might also want to keep track of the ball's radius and color.

    Just like we did with each elephant, we can use a list to hold all of the information for each ball in our simulation, with a set of index variables to specify the location of each piece of information in the list.

    We can also create functions that set and get the information in the list as one more level of abstraction that can make it easier to program the behavior we want. These functions are called accessors, and let us get and set information about the ball. In addition, we'll create functions that update the ball's appearance and position/velocity over time according to the standard equations of motion.

    Video 2: Representing a Ball

    1. Create a new file shapes.py

      Import the packages graphicsPlus as gr, random, and time.

    2. Create index variables for the ball.

      At the top of your shapes file, define and assign the following index variables: Ipos, Ivel, Iacc, Ishape, Iwin, Idrawn, Isize, and Icolor. Assign them values from 0 to 7, respectively.

    3. Write a constructor function for a ball.
      def ball_construct( radius, pos, vel, acc, win, color="red" ):

      This function is similar to newElephant in that it should create a list to represent a ball, initialize the elements of the list, call the ball_render function, and then return the list.

      The arguments to ball_construct are the radius (number), position (2 element list), velocity (2 element list), acceleration (2 element list), window (GraphWin object), and color (optional, either a string or a Zelle color created by the color_rgb functions).

      The list elements will be the following.

      IndexIndex variableTypeMeaning
      0Iposlist with 2 valuesThe x and y position of the center of the ball
      1Ivellist with 2 valuesThe x and y velocities of the ball
      2Iacclist with 2 valuesThe x and y accelerations of the ball
      3Ishapelist of graphics objectsThe graphics objects that comprise the ball's visualization
      4IwinGraphWin objectThe window into which the ball should be drawn
      5IdrawnTrue or FalseWhether the ball is currently drawn into the window.
      6IsizeNumberThe radius of the ball
      7IcolorString or Zelle colorThe color of the ball

      The function should first create a ball list with each of the above elements, where the shape list (index Ishape) is the empty list and drawn (index Idrawn) is False. Make sure to make copies of the pos, vel, and acc parameters. For example the following code assigns to a local variable lcl_pos a copy of the two-element list pos.

      lcl_pos = [ pos[0], pos[1] ]

      After creating the ball list, the function should call ball_render with the ball list as the argument. Then it should return the ball list.

    4. Write the function ball_render that makes the visualization.
      def ball_render( ball ):

      The argument to ball_render is a ball list as created by ball_construct. The function should undraw the existing representation if it is already drawn, define the Zelle graphics shapes that make up the ball, set the ball's color, put the Zelle graphics shapes into a list that gets assigned to the Ishape position in the ball list, and then redraw the ball if it was already drawn.

      When creating the visualization for a ball, you have to think about the difference between simulation coordinates and visualization coordinates.

      Video 3: Simulation versus Visualization Coordinates

      + (more detail)

      The ball_render function should follow the outline below. The purpose is to create the list of graphics objects to represent the ball. If you wish, you can make a more complicated visualization that uses more shapes than just a single Circle.

      def ball_render( ball_list ):
          # assign to drawn the Idrawn field of ball_list
      
          # if drawn 
              # call the undraw() function with ball_list as the argument
      
          # assign to rad the Isize field of ball_list
          # assign to pos the Ipos field of ball_list
          # assign to win the Iwin field of ball_list
      
          # assign to circle a new Circle object at (position pos[0], win.getHeight() - pos[1]) with the ball's radius
          # use the circle's setFill method to set the color to the ball_list Icolor field
          
          # (optional) create and color more graphics Shapes, so long as they stay within the ball's radius
      
          # assign to the ball_list's Ishape field a list containing the circle and any other shapes you created
      
          # if drawn
              # call the draw function with the ball_list as the argument
    5. Write a function that draws all of the graphics elements in a ball into its window.
      def draw( object_list ):

      The object_list is a ball list like what you created in ball_construct. If the ball is not already drawn, then loop over the shapes in the shapes list entry of the object_list and draw them into the window. The window is also stored in the object_list.

      At the end of this function, assign to the Idrawn position of the object_list the value True.

    6. Write a function that undraws all of the graphics elements in a ball.
      def undraw( object_list ):

      If the object is drawn, this function should undraw each element in the shapes list of the object.

      At the end of the function, assign to the Idrawn position of object_list the value False.

      Test your code with this test program. It should create a 5x5 grid of balls with different colors and radii.

      Video 4: The test functions

    7. Write get functions for position, velocity and acceleration.
      def getPosition( object_list ):
          # code here, returns a copy of the object's position list
      
      def getVelocity( object_list ):
          # code here, returns a copy of the object's velocity list
      
      def getAcceleration( object_list ):
          # code here, returns a copy of the object's acceleration list

      Each of these functions should return a copy of the appropriate 2-element list in object_list. For example, the position of the object is stored in object_list[Ipos]. Therefore, a copy of the position list is the expression object_list[Ipos][:].

    8. Write set functions for velocity and acceleration.
      def setVelocity( object_list, vel ):
          # code here, updates the velocity information in object list with the values from vel
      
      def setAcceleration( object_list, acc ):
          # code here, updates the acceleration information in object list with the values from vel

      These functions are the opposites of the get functions. They should update the values in the corresponding velocity or acceleration sub-lists in object_list. Be sure to copy the information from the vel or acc parameters.

    9. Write a setPosition function.
      def setPosition( object_list, pos ):
          # save the current x and y positions to local variables
          # update the object_list position to be the new x and y values in pos
          # calculate the difference between the new and old positions to get dx and dy
          # loop over the graphics objects and move them by dx and dy

      The setPosition function is slightly more complex because it has to move the visual representation of the ball to the new position. The process is the following. (1) Store the existing position, (2) update the position stored in object_list to the new position, (3) compute the difference between the new and old position, and (4) loop over the graphics elements and move them by the difference.

      Use this test program to test your getPosition and setPosition functions.

    10. Write functions to get and set the radius.
      def ball_getRadius( object_list ):
          # code here: return the ball's radius, which is in the Isize field of objectList
      
      def ball_setRadius( object_list, rad ):
          # code here: update the ball's radius information then call ball_render

      These functions are similar to the get and set functions above. The ball_getRadius should return the value from the Isize field of object_list. The ball_setRadius function should assign the new value rad to the Isize field of object_list and then call ball_render, passing in the object_list, to update the visual representation.

      Use this test program to test your getRadius and setRadius functions. The balls should get bigger and then get smaller.

      If the ball's disappear, check two things. First, are you saving the drawn state to a local variable at the start of ball_render? Second, are you updating the Idrawn field of the ball list to be True/False in the draw/undraw functions, respectively?

    11. Write an update function.
      def update( object_list, dt ):
          # compute how much the object moves in (dx and dy) using the laws of physics
          # update the object's position by dx and dy
          # update the object's velocity using the laws of physics
          # loop over the graphics objects and move them by dx and -dy

      This method should use the equations of motion under uniform acceleration to calculate the changes to position and velocity. The parameter dt specifies the time step (delta-T) for the update. The method also needs to move the graphics shapes that provide the visualization for the object.

      + (more detail)

      The first step is to use the equations of motion under uniform acceleration to figure out how much the object moved in the given time step. In the object_list, the position (x, y), the velocity (vx, vy), and the acceleration (ax, ay), are stored at the Ipos, Ivel, and Iacc indexes. For both x and y, the amount of change is given by:

      delta_pos = vel * dt + 0.5 * acc * dt * dt

      The second step is to add the delta_x and delta_y to the position of the object, stored in the Ipos position of the object_list. Make sure you are adding to their current value, not replacing the current value.

      The third step is to update the velocity x and y values. The change in velocity is the acceleration times the time step.

      delta_vel = acc * dt

      The final step is to loop over the list of graphics objects, position Ishape of the object_list, and move each element in the list by the delta_x and -delta_y values you calulated in the first step. Use the graphic element's move method. We have to use -delta_y because we want up to be positive y motion.

      Use this test program to test your update function. The balls should follow a parabolic trajectory as they move.


When you are done with the lab exercises, you may begin the project.