Chapter 6: Fun and Games with OOPs: Object-Oriented Programs

I paint objects as I think them, not as I see them.

—Pablo Picasso

6.1 Introduction

In this chapter we’ll develop our own “killer app”: A 3D video game called “Robot versus Zombies.” By the end of this chapter you’ll have the tools to develop all kinds of interactive 3D programs for games, scientific simulations, or whatever else you can imagine.

We’re sneaky! The true objective of this chapter is to demonstrate a beautiful and fundamental concept called object-oriented programming. Object-oriented programming is not only the secret sauce in 3D graphics and video games, it’s widely used in most modern large-scale software projects. In this chapter, you’ll learn about some of the fundamental ideas in object-oriented programming. And, yes, we’ll write that video game too!

6.2 Thinking Objectively

Before we get to the Robot versus Zombies video game, let’s imagine the following scenario. You’re a summer intern at Lunatix Games, a major video game developer. One of their popular games, Lunatix Lander, has the player try to land a spaceship on the surface of a planet. This requires the player to fire thrusters to align the spaceship with the landing site and slow it down to a reasonable landing speed. The game shows the player how much fuel remains and how much fuel is required for certain maneuvers.

As you test the game, you notice that it often reports that there is not enough fuel to perform a key maneuver when, in fact, you are certain there should be just the right amount of fuel. Your task is to figure out what’s wrong and find a way to fix it.

Each of the rocket’s two fuel tanks has a capacity of 1000 units; the fuel gauge for each tank reports a value between 0 and 1.0, indicating the fraction of the capacity remaining in that tank. Here is an example of one of your tests, where fuelNeeded represents the fraction of a 1000 unit tank required to perform the maneuver, and tank1 and tank2 indicate the fraction of the capacity of each of the two tanks. The last statement is checking to see if the total amount of fuel in the two tanks equals or exceeds the fuel needed for the maneuver.

>>> fuelNeeded = 42.0/1000
>>> tank1 = 36.0/1000
>>> tank2 = 6.0/1000
>>> tank1 + tank2 >= fuelNeeded
False
../_images/Alien8.PNG

Bummer!

Notice that \(\frac{36}{1000} + \frac{6}{1000} = \frac{42}{1000}\) and the fuel needed is exactly \(\frac{42}{1000}\). Strangely though, the code reports that there is not enough fuel to perform the maneuver, dooming the ship to crash.

When you print the values of fuelNeeded, tank1, tank2, and tank1 + tank2 you see the problem:

../_images/Alien8.PNG

To be precise, imprecision occurs because computers use only a fixed number of bits of data to represent data. Therefore, only a finite number of different quantities can be stored. In particular, the fractional part of a floating-point number, the mantissa, must be rounded to the nearest one of the finite number of values that the computer can store, resulting in the kinds of unexpected behavior that we see here.

>>> fuelNeeded
0.042000000000000003
>>> tank1
0.035999999999999997
>>> tank2
0.0060000000000000001
>>> tank1 + tank2
0.041999999999999996
../_images/Alien8.PNG

A rational thing to have!

This example of numerical imprecision is the result of the inherent error that arises when computers try to convert fractions into floating-point numbers (numbers with a decimal-point representation). However, assuming that all of the quantities that you measure on your rocket are always rational numbers—that is fractions with integer numerators and denominators—this imprecision problem can be avoided! How? Integers don’t suffer from imprecision. So, for each rational number, we can store its integer numerator and denominator and then do all of our arithmetic with integers.

../_images/Alien8.PNG

Or even a fraction of them all.

For example, the rational number \(\frac{36}{1000}\) can be stored as the pair of integers \(36\) and \(1000\) rather than converting it into a floating-point number. To compute \(\frac{36}{1000} + \frac{6}{1000}\) we can compute \(36+6 = 42\) as the numerator and \(1000\) as the denominator. Comparing this to the fuelNeeded value of \(\frac{42}{1000}\) involves separately comparing the numerators and denominators, which involves comparing integers and is thus free from numerical imprecision.

So, it would be great if Python had a way of dealing with rational numbers as pairs of integers. That is, we would like to have a “rational numbers” type of data (or data type, as computer scientists like to call it) just as Python has an integer data type, a string data type, and a list data type (among others). Moreover, it would be great if we could do arithmetic and comparisons of these rational numbers just as easily as we can do with integers.

The designers of Python couldn’t possibly predict all of the different data types that one might want. Instead, Python (like many other languages) has a nice way to let you, the programmer, define your own new types and then use them nearly as easily as you use the built-in types such as integers, strings, and lists.

This facility to define new types of data is called object-oriented programming or OOP and is the topic of this chapter.

6.3 The Rational Solution

Let’s get started by defining a rational number type. To do this, we build a Python “factory” for constructing rational numbers. This factory is called a class and it looks like this:

class Rational:
    def __init__(self, num, denom):
        self.numerator = num
        self.denominator = denom

Don’t worry about the weird syntax; we’ll come back to that in a moment when we take a closer look at the details. For now, the big idea is that once we’ve written this Rational class (and saved it in a file with the same name but with the suffix .py at the end, in this case Rational.py) we can “manufacture” – or more technically instantiate – new rational numbers to our heart’s content. Here’s an example of calling this “factory” to instantiate two rational numbers \(\frac{36}{1000}\) and \(\frac{6}{1000}\):

r1 = Rational(36, 1000)
r2 = Rational(6, 1000)

What’s going on here? When Python sees the instruction

r1 = Rational(36, 1000)
../_images/Alien8.PNG

Perhaps this is self -ish of Python.

it does two things. First, it instantiates an empty object which we’ll call self. Actually, self is a reference to this empty object as shown in Figure 6.1.

Figure 6.1: self refers to a new empty object. It’s only empty for a moment!

Next, Python looks through the Rational class definition for a function named __init__ (notice that there are two underscore characters before and after the word “init”). That’s a funny name, but it’s a convention in Python. Notice that in the definition of __init__ above, that function seems to take three arguments, but the line r1 = Rational(36, 1000) has only supplied two. That’s weird, but—as you may have guessed—the first argument is passed in automatically by Python and is a reference to the new empty self object that we’ve just had instantiated for us.

The __init__ function takes the reference to our new empty object called self, and it’s going to add some data to that object. The values \(36\) and \(1000\) are passed in to __init__ as num and denom, respectively. Now, when the __init__ function executes the line self.numerator = num, it says, “go into the object referenced by self , give it a variable called numerator, and give that variable the value that was passed in as num.” Similarly, the line self.denominator = denom says “go into the object referenced by self, give it a variable called denominator, and give that variable the value that was passed in as denom.” Note that the the names num, denom, numerator, and denominator are not special—they are just the names that we chose.

../_images/Alien8.PNG

Python uses the name “attributes” for the variables that belong to a class. Some other languages use names like “data members”, “properties”, “fields”, and “instance variables” for the same idea.

The variables numerator and denominator in the __init__ function are called attributes of the Rational class. A class can have as many attributes as you wish to define for it. It’s pretty clear that a rational number class would have to have at least these two attributes!

The last thing that happens in the line

r1 = Rational(36, 1000)

is that the variable r1 is now assigned to be a reference to the object that Python just created and we initialized. We can see the contents of the rational numbers as follows:

>>> r1.numerator
36

We used the “dot” in the function __init__ as well, when we said self.numerator = num. The “dot” was doing the same thing there. It said, “go into the self object and look at the attribute named numerator.” Figure 6.2 shows the situation now.

Figure 6.2: r1 refers to the Rational object with its numerator and denominator.

We note that in our figures in this chapter, we’re representing memory in a somewhat different (and simpler) way than we used in Chapter 5. For example, Figure 6.2 shows the values of numerator and denominator as if they were stored inside the variables. In reality, as we saw in Chapter 5, the integer values would be somewhere else in memory, and the variables would store references to those values.

In our earlier example, we “called” the Rational “factory” twice to instantiate two different rational numbers in the example below:

r1 = Rational(36, 1000)
r2 = Rational(6, 1000)

The first call, Rational(36, 1000) instantiated a rational number, self, with numerator \(36\) and denominator \(1000\). This was called self, but then we assigned r1 to refer to this object. Similarly, the line r2.numerator is saying, “go to the object called r2 and look at its attribute named numerator.” It’s important to keep in mind that since r1 and r2 are referring to two different objects, each one has its “personal” numerator and denominator. This is shown in Figure 6.3.

Figure 6.3: Two Rational numbers with references r1 and r2.

../_images/Alien8.PNG

This would be a short chapter if that was the whole story!

Let’s take stock of what we’ve just seen. First, we defined a factory—technically known as a class–called Rational. This Rational class describes a template for manufacturing a new type of data. That factory can now be used to instantiate a multitude of items—technically known as objects–of that type. Each object will have its own variables – or “attributes” – in this case numerator and denominator, each with their own values.

That’s cute, but is that it? Remember that our motivation for defining the Rational numbers was to have a way to manipulate (add, compare, etc.) rational numbers without having to convert them to the floating-point world where numerical imprecision can cause headaches (and rocket failures, and worse).

Python’s built-in data types (such as integers, floating-point numbers, and strings) have the ability to be added, compared for equality, etc. We’d like our Rational numbers to have these abilities too! We’ll begin by adding a function to the Rational class that will allow us to add one Rational to another and return the sum, which will be a Rational as well. A function defined inside a class has a special fancy name—it’s called a method of that class. The __init__ method is known as the constructor method.

Our add method in the Rational class will be used like this:

>>> r1.add(r2)

This should return a Rational number that is the result of adding r1 and r2. So, we should be able to write:

>>> r3 = r1.add(r2)

Now, r3 will refer to the new Rational number returned by the add method. The syntax here may struck you as funny at first, but humor us; we’ll see in a moment why this syntax is sensible.

Let’s write this add method! If r1 \(= \frac{a}{b}\) and r2 \(= \frac{c}{d}\) then r1+r2 \(= \frac{ad+bc}{bd}\). Note that the resulting fraction might be simplified by dividing out by terms that are common to the numerator and the denominator, but let’s not worry about that for now. Here is the Rational class with its shiny new add method:

class Rational:
    def __init__(self, num, denom):
        self.numerator = num
        self.denominator = denom

    def add(self, other):
        newNumerator = self.numerator * other.denominator +
                       self.denominator * other.numerator
        newDenominator = self.denominator*other.denominator
        return Rational(newNumerator, newDenominator)

What’s going on here!? Notice that the add method takes in two arguments, self and other, while our examples above showed this method taking in a single argument. (Stop here and think about this. This is analogous to what we saw earlier with the __init__ method.)

To sort this all out, let’s consider the following sequence:

>>> r1 = Rational(1, 2)
>>> r2 = Rational(1, 3)
>>> r3 = r1.add(r2)

The instruction r1.add(r2) does something funky: It calls the Rational class’s add method. It seems to pass in just r2 to the add method but that’s an optical illusion! In fact, it passes in two values: first it automatically passes a reference to r1 and then it passes in r2. This is great, because our code for the add method is expecting two arguments: self and other. So, r1 goes into the self “slot” and r2 goes into the other slot. Now, the add method can operate on those two Rational numbers, add them, construct a new Rational number representing their sum, and then return that new object.

../_images/Alien8.PNG

“Snazzy” is a technical term.

Here’s the key: Consider some arbitrary class Blah. If we have an object myBlah of type Blah, then myBlah can invoke a method foo with the notation myBlah.foo(arg1, arg2, ..., argN). The method foo will receive first a reference to the object myBlah followed by all of the N arguments that are passed in explicitly. Python just knows that the first argument is always the automatically-passed-in reference to the object before the “dot”. The beauty of this seemingly weird system is that the method is invoked by an object and the method “knows” which object invoked it. Snazzy!

After performing the above sequence of instructions, we could type:

>>> r3.numerator
>>> r3.denominator

What we would we see? We’d see the numerator and denominator of the Rational number r3. In this case, the numerator would be \(5\) and the denominator would be \(6\).

Notice that instead of typing r3 = r1.add(r2) above, we could have instead have typed r3 = r2.add(r1). What would have happened here? Now, r2 would have called the add method, passing r2 in for self and r1 in for other. We would have gotten the same result as before because addition of rationals is commutative.

6.4 Overloading

../_images/Alien8.PNG

“Spiffy” is yet another technical term.

So far, we have built a basic class for representing rational numbers. It’s neat and useful, but now we’re about to make it even spiffier.

You probably noticed that the syntax for adding two Rational numbers is a bit awkward. When we add two integers, like \(42\) and \(47\), we certainly don’t type 42.add(47), we type 42+47 instead.

It turns out that we can use the operator “+” to add Rational numbers too! Here’s how: We simply change the name of our add method to __add__. Those are two underscore characters before and two underscore characters after the word add. Python has a feature that says “if a function is named __add__ then when the user types r1 + r2, I will translate that into r1.__add__(r2).” How does Python know that the addition here is addition of Rational numbers rather than addition of integers (which is built-in)? It simply sees that r1 is a Rational, so the “+” symbol must represent the __add__ method in the Rational class. We could similarly define __add__ methods for other classes and Python will figure out which one applies based on the type of data in front of the “+” symbol.

../_images/Alien8.PNG

This is “good” overloading. “Bad” overloading involves taking more than 18 credits in a term.

This feature is called overloading.

We have overloaded the “+” symbol to give it a meaning that depends on the context in which it is used. Many, though not all, object-oriented programming languages support overloading. In Python, overloading addition is just the tip of the iceberg. Python allows us to overload all of the normal arithmetic operators and all of the comparison operators such as “==”, “!=”, “<”, among others.

Let’s think for a moment about comparing rational numbers for equality. Consider the following scenario, in which we have two different Rational objects and we compare them for equality:

>>> r1 = Rational(1, 2)
>>> r2 = Rational(1, 2)
>>> r1 == r2
False
../_images/Alien8.PNG

“Blob” really is a technical term!

Why did Python say “False”? The reason is that even though r1 and r2 look the same to us, each one is a reference to a different object. The two objects have identical contents, but they are different nonetheless, just as two identical twins are two different people. Another way of seeing this is that r1 and r2 refer to different blobs of memory, and when Python sees them ask if r1 == r2 it says “Nope! Those two references are not to the same memory location.” Since we haven’t told Python how to compare Rationals in any other way, it simply compares r1 and r2 to see if they are referring to the very same object.

So let’s “overload” the “==” symbol to correspond to a function that will do the comparison as we intend. We’d like for two rational numbers to be considered equal if their ratios are the same, even if their numerators and denominators are not the same. For example \(\frac{1}{2} = \frac{42}{84}\). One way to test for equality is to use the “cross-multiplying” method that we learned in grade school: Multiply the numerator of one of the fractions by the denominator of the other and check if this is equal to the other numerator-denominator product. Let’s first write a method called __eq__ to include in our Rational number class to test for equality.

def __eq__(self, other):
    return self.numerator * other.denominator ==
           self.denominator * other.numerator

Now, if we have two rational numbers such as r1 and r2, we could invoke this method with r1.__eq__(r2) or with r2.__eq__(r1). But because we’ve used the special name __eq__ for this method, Python will know that when we write r1 == r2 it should be translated into r1.__eq__(r2). There are many other symbols that can be overloaded. (To see a complete list of the methods that Python is happy to have you overload, go to http://docs.python.org/2/reference/datamodel.html#special-method-names.)

../_images/Alien8.PNG

This example serves to doubly underscore the beauty of overloading!

For example, we can overload the “>=” symbol by defining a method called __ge__ (which stands for greater than or equal). Just like __eq__, this method takes two arguments: A reference to the calling object that is passed in automatically (self) and a reference to another object to which we are making the comparison. So, we could write our __ge__ method as follows:

def __ge__(self, other):
    return self.numerator * other.denominator >=
           self.denominator * other.numerator

Notice that there is only a tiny difference between how we implemented our __eq__ and __ge__ methods. Take a moment to make sure you understand why __ge__ works.

../_images/Alien8.PNG

We hope that you aren’t feeling overloaded at this point. We’d feel bad if you “object”ed to what we’ve done here.

Finally, let’s revisit the original fuel problem with which we started the chapter. Recall that due to numerical imprecision with floating-point numbers, we had experienced mission failure:

>>> fuelNeeded = 42.0/1000
>>> tank1 = 36.0/1000
>>> tank2 = 6.0/1000
>>> tank1 + tank2 >= fuelNeeded
False

In contrast, we can now use our slick new Rational class to save the mission!

>>>  fuelNeeded = Rational(42, 1000)
>>>  tank1 = Rational(36, 1000)
>>>  tank2 = Rational(6, 1000)
>>>  tank1 + tank2 >= fuelNeeded
True

Mission accomplished!

6.5 Printing an Object

Our Rational class is quite useful now. But check this out:

../_images/Alien8.PNG

0x6b918!? What the heck is that?!

>>> r1 = Rational(1, 2)
>>> r2 = Rational(1, 3)
>>> r3 = r1 + r2
>>> r3
<Rational.Rational instance at 0x6b918>
>>> print(r3)
<Rational.Rational instance at 0x6b918>

Notice the strange output when we asked for r3 or when we tried to print(r3). In both cases, Python is telling us, “r3 is a Rational object and I’ve given it a special internal name called 0x blah, blah, blah.”

What we’d really like, at least when we print(r3), is for Python to display the number in some nice way so that we can see it! You may recall that Python has a way to “convert” integers and floating-point numbers into strings using the built-in function str. For example:

>>> str(1)
'1'
>>> str(3.142)
'3.142'

So, since the print function wants to print strings, we can print numbers this way:

>>> print(str(1))
1
>>> print("My favorite number is " + str(42))
My favorite number is 42

In fact, other Python types such as lists and dictionaries also have str functions:

>>> myList = [1, 2, 3]
>>> print("Here is a very nice list: " + str(myList))
Here is a very nice list: [1, 2, 3]

Python lets us define a str function for our own classes by overloading a special method called __str__. For example, for the Rational class, we might write the following __str__ method:

def __str__(self):
    return str(self.numerator) + "/" + str(self.denominator)

What is this function returning? It’s a string that contains the numerator followed by a forward slash followed by the denominator. When we type print(str(r3)), Python will invoke this __str__ method. That function first calls the str function on self.numerator. Is the call str(self.numerator) recursive? It’s not! Since self.numerator is an integer, Python knows to call the str method for integers here to get the string representation of that integer. Then, it concatenates to that string a another string containing the forward slash, /, indicating the fraction line. Finally, to that string it concatenates the string representation of the denominator. Now, this string is returned. So, in our running example from above where r3 is the rational number \(\frac{5}{6}\), we could use our str method as follows:

>>> r3
<Rational.Rational instance at 0x6b918>
>>> print("Here is r3: "+str(r3))
Here is r3: 5/6

Notice that in the first line when we ask for r3, Python just tells us that it is a reference to Rational object. In the third line, we ask to convert r3 into a string for use in the print function. By the way, the __str__ method has a closely related method named __repr__ that you can read about on the Web.

6.6 A Few More Words on the Subject of Objects

Let’s say that we wanted (for some reason) to change the numerator of r1 from its current value to 42. We could simply type

r1.numerator = 42

In other words, the internals of a Rational object can be changed. Said another way, the Rational class is mutable. (Recall our discussion of mutability in Chapter 5.) In Python, classes that we define ourselves are mutable (unless we add fancy special features to make them immutable). To fully appreciate the significance of mutability of objects, consider the following pair of functions:

def foo():
    r = Rational(1, 3)
    bar(r)
    print r

def bar(number):
    number.numerator += 1

What happens when we invoke function foo? Notice that function bar is not returning anything. However, the variable number that it receives is presumably a Rational number, and foo increments the value of this variable’s numerator. Since user-defined classes such as Rational are mutable, this means that the Rational object that was passed in will have its numerator changed!

How does this actually work? Notice that in the function foo, the variable r is a reference to the Rational number \(\frac{1}{3}\). In other words, this Rational object is somewhere in the computer’s memory and r is the address where this blob of memory resides. When foo calls bar(r) it is passing the reference (the memory location) r to foo. Now, the variable number is referring to that memory location. When Python sees number.numerator += 1 it first goes to the memory address referred to by number, then uses the “dot” to look at the numerator part of that object, and increments that value by 1. When bar eventually returns control to the calling function, foo, the variable r in foo is still referring to that same memory location, but now the numerator in that memory location has the new value that bar set.

../_images/Alien8.PNG

Our legal team objected to us using the word “everything” here, but it’s close enough to the truth that we’ll go with it.

This brings us to a surprising fact: Everything in Python is an object! For example, Python’s list datatype is an object. “Wait a second!” we hear you exclaim. “The syntax for using lists doesn’t look anything like the syntax that we used for using Rationals!” You have a good point, but let’s take a closer look.

For the case of Rationals, we had to make a new object this way:

r = Rational(1, 3)

On the other hand, we can make a new list more simply:

myList = [42, 1, 3]

In fact, though, this list notation that you’ve grown to know and love is just a convenience that the designers of Python have provided for us. It’s actually a shorthand for this:

myList = list()
myList.append(42)
myList.append(1)
myList.append(3)

Now, if we ask Python to show us myList it will show us that it is the list [42, 1, 3]. Notice that the line myList = list() is analogous to r = Rational(1, 3) except that we do not provide any initial values for the list. Then, the append method of the list class is used to append items onto the end of our list. Lists are mutable, so each of these appends changes the list!

Indeed, the list class has many other methods that you can learn about online. For example, the reverse method reverses a list. Here’s an example, based on the myList list object that we created above:

>>> myList
[42, 1, 3]
>>> myList.reverse()
>>> myList
[3, 1, 42]

Notice that this reverse method is not returning a new list but rather mutating the list on which it is being invoked.

Before moving on, let’s reflect for a moment on the notation that we’ve seen for combining two lists:

>>> [42, 1, 3] + [4, 5]
[42, 1, 3, 4, 5]

How do you think that the “+” symbol works here? You got it–it’s an overloaded method in the list class! That is, it’s the __add__ method in that class!

Strings, dictionaries, and even integers and floats are all objects in Python! However, a few of these built-in types - such as strings, integers, and floats - were designed to be immutable. Recall from the previous chapter that this means that their internals cannot be changed. You can define your own objects to be immutable as well, but it requires some effort and it’s rarely necessary, so we won’t go there.

6.7 Getting Graphical with OOPs

../_images/Alien8.PNG

Warning! This section contains graphical language!

We’ve seen that object-oriented programming is elegant and, hopefully, you now believe that it’s useful. But what about 3D graphics and our video game? That’s where we’re headed next!

To get started, you’ll need to get the “Jupyter VPython” 3D graphics system for Python 3. You can install it by running pip install vpython at the command prompt. You should also make sure you have the most recent version of Anaconda installed.

Once you have VPython installed, run jupyter notebook from the command prompt. A window should pop up in your browser. In that window, select the New button on the upper right, then select VPython under Notebooks.

In the new window that pops up, you’ll need to import ``vpython as follows:

>>> from vpython import *

Next, in the same cell, type:

>>> b = box()

Then hit the Run button. A white box should pop up in a small window below the cell (the box you typed into). If you change your code and want to re-run your code, you will need to click the Restart the Kernel button (near the Run button), then click the red Restart button in the box that pops up. Then wait for a blue Kernel Ready message to flash in the upper right hand corner, and then press Run.

../_images/Alien8.PNG

In the display window (where the box is displayed), click and drag with the right mouse button (hold down the command key on a Macintosh). Drag left or right, and you rotate around the scene. To rotate around a horizontal axis, drag up or down. Click and drag up or down with the middle mouse button to move closer to the scene or farther away (on a 2-button mouse, hold down the left and right buttons; on a 1-button mouse, hold down the Option key).

What you’ll see now on the screen is a white box. It might look more like a white square, so rotate it around to see that it’s actually a 3D object.

As you may have surmised, box is a class defined in the vpython module. The command

>>> b = box()

invoked the constructor to create a new box object and we made b be the name, or more precisely “a reference”, to that box.

Just like our Rational number class had numerator and denominator attributes, the box class has a number of attributes as well. Among these are the box’s length, height, and width; its position, its color, and even its material properties. Try changing these attributes at the command line as follows:

>>> b.length = 0.5  # the box's length just changed
>>> b.width = 2.0 # the box's width just changed
>>> b.height = 1.5  # the box's height just changed
>>> b.color = vector(1.0, 0.0, 0.0)  # the box turned red
>>> b.texture = textures.wood  # it's wood-grained!
>>> b.opacity = 0.42 # it's translucent!

When we initially made our b = box() it had “default” values for all of these attributes. The length, width, and height attributes were all 1.0, the color attribute was white, and the textures attribute was a boring basic one. Notice that some of the attributes of a box are fairly obvious: length, width, and height are all numbers. However, the color attribute is weird. Similarly, the texture property was set in a strange way. Let’s take a closer look at just one of these attributes and you can read more about others later on the VPython documentation website. (You will want to look up ‘Glowscript documentation’. If you only search for ‘vpython documentation’, you will only find the documentation for Classic VPython, the predecessor of Jupyter VPython.)

Clear the cell you are in (except for the import statement). Let’s make a new box and ask Python for its color attribute by running the following code:

>>> c = box()
>>> c.color
(1.0, 1.0, 1.0)

VPython represents color using a tuple with three values, each between \(0.0\) and \(1.0\). The three elements in this tuple indicate how much red (from \(0.0\), which is none, to \(1.0\), which is maximum), green, and blue, respectively, is in the color of the object.

../_images/Alien8.PNG

If your roommate sees you staring at your fingers, just explain that you are doing something very technical.

So, \((1.0, 1.0, 1.0)\) means that we are at maximum of each color, which amounts to bright white. The tuple \((1.0, 0.0, 0.0)\) is bright red and the tuple \((0.7, 0.0, 0.4)\) is a mixture of quite a bit of red and somewhat less blue.

The box class has another attribute called pos that stores the position of the center of the box. The coordinate system used by VPython is what’s called a “right-handed” coordinate system: If you take your right hand and stick out your thumb, index finger, and middle finger so that they are perpendicular to one another with your palm facing you, the positive \(x\) axis is your thumb, the positive \(y\) axis is your index finger, and the positive \(z\) axis is your middle finger.

../_images/Alien8.PNG

When you used your mouse to rotate the scene, that actually rotated the entire coordinate system.

Said another way, before you start rotating in the display window with your mouse, the horizontal axis is the \(x\) axis, the vertical axis is the \(y\) axis, and the \(z\) axis points out of the screen.

Take a look at the position of the box by adding a call to c.pos. When you re-run the code, you’ll see this:

>>> c.pos
<0.000000, 0.000000, 0.000000>
../_images/Alien8.PNG

Is vector class also called linear algebra?

VPython has a class called vector, and pos is an object of this type, as we can tell by the vector notation. Nice! The box class is defined using the vector class. Using a vector class inside the box class is, well, very classy! “OK,” we hear you concede grudgingly, “but what’s the point of a vector? Why couldn’t we just use a tuple or a list instead?” Here’s the thing: The vector class has some methods defined in it for performing vector operations. For example, the vector class has an overloaded addition operator for adding two vectors:

>>> v = vector(1, 2, 3)
>>> w = vector(10, 20, 30)
>>> v + w
<11.000000, 22.000000, 33.000000>

This class has many other vector operations as well. For example, the norm() method returns a vector that points in the same direction but has magnitude (length) 1:

>>> u = vector(1, 1, 0)
>>> u.norm()
<0.707107, 0.707107, 0.000000>
../_images/Alien8.PNG

Take a look at the rich set of other vector operations on the VPython web site in order to dot your i’s and cross your t’s or, more precisely, to dot your scalars and cross your vectors!

So, while we could have represented vectors using lists, we wouldn’t have a nice way of adding them, normalizing them, and doing all kinds of other things that vectors like to do.

But, our objective for now is to change our box’s pos vector in order to move it. We can do this, for example, as follows:

>>> c.pos = vector(0, 1, 2)

While we can always create a box and change its attributes afterwards, sometimes it’s convenient to just set the attributes of the box at the time that the box is first instantiated. The box class constructor allows us to set the values of attributes at the time of construction like this:

>>> d = box(length = 0.5, width = 2.0, height = 1.5, color = vector(1.0, 0.0, 0.0))

Whatever attributes we don’t specify will get their default values. For example, since we didn’t specify a vector value for pos, the box’s initial position will be the origin.

VPython has lots of other shape classes beyond boxes including spheres, cones, cylinders, among others. While these objects have their own particular attributes (for example a sphere has a radius), all VPython objects share some useful methods. One of these methods is called rotate. Not surprisingly, this method rotates its object. Let’s take rotate out for a spin!

Try this with the box b that we defined above:

>>> d.rotate(angle=pi/4)

We are asking VPython to rotate the box b by \(\frac{\pi}{4}\) radians. The rotation is, by default, specified in radians about the \(x\) axis.

Now, let’s put this all together to write a few short VPython programs just to flex our 3D graphics muscles. First, let’s write a very short program that rotates a red box forever (clean out your cell before copying this code in):

from vpython import *

def spinBox():
    myBox = box(color = vector(1.0, 0.0, 0.0))
    while True:
        # Slow down the animation to 60 frames per second.
        # Change the value to see the effect!
        rate(60)
        myBox.rotate(angle=pi/100)

Second, take a look at the program below. Try to figure out what it’s doing before you run it.

from vpython import *
import random

def spinboxes():
    boxList = []
    for boxNumber in range(0,10):
        x = random.randint(-5,5)
        y = random.randint(-5,5)
        z = random.randint(-5,5)
        red = random.random()       # random number between 0 and 1
        green = random.random()     # random number between 0 and 1
        blue = random.random()      # random number between 0 and 1
        newBox = box()
        newBox.pos = vector(x, y, z)
        newBox.color = vector(red, green, blue)
        newBox.axis =
        random.choice([vector(1,0,0),vector(0,1,0),vector(0,0,1)]) # makes boxes rotate in random directions
        boxList.append(newBox)
    # the physics loop, which updates the world
    while True:
        rate(60)
        for myBox in boxList:
            myBox.rotate(angle=pi/100)
spinboxes()

This is very cool! We now have a list of objects and we can go through that list and rotate each of them.

6.8 Robot and Zombies, Finally!

It’s time to make our video game! The premise of our game is that we will control a robot that moves on the surface of a disk (a large flat cylinder) populated by zombies. The player will control the direction of the robot with a GUI (Graphical User Interface) that YOU will make! Our GUI should have the following: Two buttons to speed up and slow down the robot, two buttons to turn the robot left and right, and one button to quit the program. (We will give you the code for the GUI so that we can focus on making our robots and alien.) Meanwhile, the zombies will each move and turn independently by random amounts.

The program will be in an infinite loop. At each step, the player’s robot will take a small step forward. The buttons will simultaneously control the robot’s turn amounts. Similarly, each zombie will turn a random amount and then take a small step forward. Our game will have no particular objective, but you can add one later if you like. Perhaps, the objective is to run into as many zombies as possible – or perhaps avoid them.

To get started, we wish to define a player robot class that we’ll use to manufacture (“instantiate”) the player’s robot and another class that allows to instantiate zombies. It particularly makes sense to have a zombie class because a class allows us to instantiate many objects – and we indeed plan to have many zombies!

In fact, the player’s robot and zombies have a lot in common. They are 3D entities that should be able to move forward and turn. Because of this commonality, we would be replicating a lot of effort if we were to define a robot class and a zombie class entirely separately. On the other hand, the two classes are not going to be identical because the player’s robot looks different from zombies (we hope) and because the robot will be controlled by the player while the zombies move on their own.

So, here’s the key idea: We’ll define a class – let’s call it GenericBot – that has all of the attributes that any entity in our game – a player robot or a zombie – should have. Then, we’ll define a PlayerBot class and a ZombieBot class both of which “inherit” all of the attributes and methods of the GenericBot and add the special extras (e.g., how their bodies look) that differentiate them.

Our GenericBot class will have a constructor, an __init__ method, that takes as input the initial position of the bot, its initial heading (the direction in which it is pointing), and its speed (the size of each step that it makes when we ask it to move forward). Here’s the code; we’ll dissect it below.

from vpython import *
import math
import random

class GenericBot:
    def __init__(self, position = vector(0, 0, 0),
                 heading = vector(0, 0, 1), speed = 1):
        self.position = position
        self.heading = heading.norm()
        self.speed = speed
        self.parts = []

    def update(self):
        self.turn(0)
        self.forward()

    def turn(self, angle):
        # convert angle from degrees to radians (VPython
        # assumes all angles are in radians)
        theta = math.radians(angle)
        self.heading = rotate(self.heading, angle = theta, axis = vector(0, 1, 0))
        for part in self.parts:
            part.rotate(angle = theta, axis = vector(0, 1, 0),
                        origin = self.position)

    def forward(self):
        self.position += self.heading * self.speed
        for part in self.parts:
            part.pos += self.heading * self.speed

Let’s start with the __init__ method – the so-called “constructor” method. It takes three input arguments: A position (a VPython vector object indicating the bot’s initial position), a heading (a vector indicating the direction that the bot is initially pointing), and speed (a number indicating how far the bot moves at each update step). Notice that the line:

def __init__(self, position = vector(0, 0, 0),
             heading = vector(0, 0, 1), speed = 1):

provides default values for these inputs. This means that if the user doesn’t provide values for these input arguments, the inputs will be set to these values. (You may recall that the box class had default arguments as well. We could either define a new box with b = box() in which case we got the default values or we could specify our own values for these arguments.) If the user provides only some of the input arguments, Python will assume that they are the arguments from left-to-right. For example, if we type

>>> mybot = GenericBot(vector(1, 2, 3))

then Python assumes that vector(1, 2, 3) should go into the position variable and it uses the default values for heading and speed. If we type

>>> mybot = GenericBot(vector(1, 2, 3), vector(0, 0, 1))

then the first vector goes into the position argument and the second goes into the heading argument. If we want to provide values in violation of the left-to-right order, we can always tell Python which value we are referring to like this:

>>> mybot = GenericBot(heading=vector(0, 1, 0))
../_images/Alien8.PNG

It was de-fault of de-authors for this de-gression!

Now, Python sets the heading to the given value and uses the default values for the other arguments.

OK, so much for defaults! The __init__ method then sets its position (self.position), heading (self.heading), speed (self.speed), and parts (self.parts) attributes. The self.heading is normalized to make the length of the vector a unit vector (a vector of length 1) using the vector class’s norm() method. The self.parts list will be a list of VPython 3D objects – boxes, spheres, and so forth – that make up the body of the bot. Since the player bot and the zombie bots will look different, we haven’t placed any of these body parts into the list just yet. That’s coming soon!

Notice that the GenericBot has three additional methods: update, forward, and turn. In fact, the update method simply calls the turn method to turn the bot 0 radians (we’ll probably change that later!) and then calls the forward method to move one step at the given speed. The turn method changes the bot’s self.heading so that it heads in the new direction induced by the turning angle and then rotates each of the parts in the bot’s self.parts list by that same angle.

Something very lovely and subtle is happening in the for loop of the turn method! Notice that each part is expected to be a VPython object, like a box or sphere or something else. Each of those objects has a rotate method. Python is saying here “hey part, figure out what kind of object you are and then call your rotate method to rotate yourself.” So, if the first part is a box, then this will call the box rotate method. If the next part is a sphere, the sphere‘s rotate method will be called here. This all works great, as long as each element in the self.parts list has a rotate method. Fortunately, all VPython shapes do have a rotate method.

We’ll also just point out quickly that the line

part.rotate(angle = theta, axis = vector(0, 1, 0),
            origin = self.position)

is telling that part to rotate by an angle theta about a vector aligned with vector (0, 1, 0) (the \(y\)-axis) but starting at the vector given by self.position. Under our assumption that the \(y\)-axis is “up”, this effectively rotates the object about a line that runs through the center of its body from “its feet to its head”. That is, it rotates the body parts the way we would like it to rotate, as opposed to the default rotation which is about the \(x\)-axis.

The forward method changes the bot’s self.position vector by adding to it the heading vector scaled by the bot’s self.speed. Note that self.position is just the bot’s own self-concept of where it is located. We also need to physically move all of the parts of the bot’s body, which is done by changing the pos position vector of each VPython objects in the self.parts list, again by the self.heading vector scaled by self.speed.

../_images/Alien8.PNG

Drumroll, please!

Next comes the most amazing part of this whole business! We now define the ZombieBot class that inherits all of the methods and attributes of the GenericBot but adds the components that are specific to a zombie. Here’s the code and we’ll discuss it in a moment.

class ZombieBot(GenericBot):
    def __init__(self, position = vector(0, 0, 0),
                 heading = vector(0, 0, 1)):
        GenericBot.__init__(self, position, heading)
        self.body = cylinder(pos = self.position,
                             axis = vector(0, 4, 0),
                             radius = 1,
                             color = vector(0, 1, 0))
        self.arm1 = cylinder(pos = self.position + vector(0.6, 3, 0),
                             axis = vector(0, 0, 2),
                             radius = .3,
                             color = vector(1, 1, 0))
        self.arm2 = cylinder(pos = self.position + vector(-0.6, 3, 0),
                             axis = vector(0, 0, 2),
                             radius = .3,
                             color = vector(1, 1, 0))
        self.halo = ring(pos = self.position + vector(0, 5, 0),
                             axis = vector(0, 1, 0),
                             radius = 1,
                             color = vector(1, 1, 0))
        self.head = sphere(pos = self.position + vector(0, 4.5, 0),
                             radius = 0.5,
                             color = vector(1, 1, 1))
        self.parts = [self.body, self.arm1, self.arm2,
                      self.halo, self.head]

    def update(self):
        # call turn with a random angle between -5 and 5
        # degrees
        self.turn(random.uniform(-5, 5))
        self.forward()

The class definition begins with the line: class ZombieBot(GenericBot). The GenericBot in parentheses is saying to Python “this class inherits from GenericBot.” Said another way, a ZombieBot “is a kind of” GenericBot. Specifically, this means that a ZombieBot has the __init__, update, turn, and forward methods of GenericBot. The class GenericBot is called the superclass of ZombieBot. Similarly, ZombieBot is called a subclass or derived class of GenericBot.

Note that ZombieBot has its own __init__ constructor method. If we had not defined this __init__ then each time we constructed a ZombieBot object, Python would automatically invoke the __init__ from GenericBot, the superclass from which ZombieBot was derived. However, since we’ve defined an __init__ method for ZombieBot, that method will get called when we instantiate a ZombieBot object. It’s not that the GenericBot‘s constructor isn’t useful to us, but we want to do some other things too. Specifically, we want to populate the list of body parts, self.parts, with the VPython shapes that constitute a zombie.

We get two-for-the-price-of-one by first having the ZombieBot‘s __init__ method call the GenericBot‘s __init__ method to do what it can do for us. This is invoked via GenericBot.__init__(self, position, heading). This is saying, “hey, I know that I’m a ZombieBot, but that means I’m a kind of GenericBot and, as such, I can call any GenericBot method for help. In particular, since the GenericBot already has an __init__ method that does some useful things, I’ll call it to set my position and heading attributes.”

After calling the GenericBot constructor, the ZombieBot constructor continues to do some things on its own. In particular, it defines some VPython objects and places them in the parts list of body parts. You might notice that all of those body parts are positioned relative to the ZombieBot‘s position – which is just a vector that we defined that keeps track of where the bot is located in space.

Since ZombieBot inherited from GenericBot, it automatically has the update, turn, and forward methods defined in the GenericBot class. The turn and forward methods are fine, but the update method needs to be replaced to turn the ZombieBot at random. Thus, we provide a new turn method in the ZombieBot class.

Now, imagine that we do the following:

>>> zephyr = ZombieBot()
>>> zephyr.update()

The first line creates a new ZombieBot object. Since we didn’t provide any inputs to the constructor, the default values are used and zephyr the zombie is at position (0, 0, 0) and heading in direction (0, 0, 1). The second line tells zephyr to update itself. Python checks to see if the ZombieBot class contains an update method. It does, so that method is invoked. That method then calls the turn method with a random angle between -5 and 5 degrees. Python checks to see if the ZombieBot class has its own turn method. Since it doesn’t, Python goes to the superclass, GenericBot, and looks for a turn method there. There is one there and that’s what’s used! Next, the update method calls the forward method. Since there’s no forward method defined in ZombieBot, Python again goes to the superclass and uses the forward method there.

The very nice thing here is that ZombieBot inherits many things from the superclass from which it is derived and only changes – or overrides – those methods that it needs to customize for zombies.

Now, we can do something similar for the player’s robot:

class PlayerBot(GenericBot):
    def __init__(self, position = vector(0, 0, 0),
                 heading = vector(0, 0, 1)):
        GenericBot.__init__(self, position, heading)
        self.body = cylinder(pos = self.position + vector(0, 0.5, 0),
                               axis = vector(0, 6, 0),
                               radius = 1,
                               color = vector(1, 0, 0))
        self.head = box(pos = vector(0, 7, 0) + self.position,
                               length = 2,
                               width = 2,
                               height = 2,
                               color = vector(0, 1, 0))
        self.nose = cone(pos = vector(0, 7, 1) + self.position,
                               radius = 0.5,
                               axis = vector(0, 0, 1),
                               color = vector(1, 1, 0))
        self.wheel1 = cylinder(pos = self.position + vector(1, 1, 0),
                               axis = vector(0.5, 0, 0),
                               radius = 1,
                               color = vector(0, 0, 1))
        self.wheel2 = cylinder(pos = self.position + vector(-1, 1, 0),
                               axis = vector(-0.5, 0, 0),
                               radius = 1,
                               color = vector(0, 0, 1))
        self.parts = [self.body, self.head, self.nose,
                      self.wheel1, self.wheel2]

    def update(self):
        self.turn(0) # we'll leave the turn handling up to our buttons...
        self.forward()

The PlayerBot class also inherits from the GenericBot class. It again calls the GenericBot‘s constructor for help initializing some attributes and then defines its own attributes. It uses the turn and forward methods from the superclass, and although the update method isn’t any different, it reminds us that the inputs for the robot come from the button handlers in the GUI section.

Recall that a short awhile ago we noted that all Python shapes have a rotate method. That’s because all of these shapes – boxes, spheres, cylinders, cones, and others – inherit from a shape superclass that defines a rotate method. Therefore, they all “know” how to rotate because they inherited that “knowledge” from their parent class. That parent class has many features – methods and attributes – that its children need, just like our GenericBot class has features that are used by its children – the derived classes ZombieBot and PlayerBot.

Finally, here’s our game! In this file, we import the vpython graphics package; the robot.py file containing the GenericBot, ZombieBot, and PlayerBot classes, and the random and math packages. The main function instantiates a large flat VPython cylinder object, that we name ground, that is the surface on which the bots will move. Its radius is given by a global variable GROUND_RADIUS. We then instantiate a single PlayerBot named player and call a helper function called makeZombies that instantiates many zombies (the number is given by the global variable ZOMBIES) and returns a list of ZombieBot objects at random positions on our cylindrical playing area. Finally, the main function enters an infinite loop. At each iteration, we check to see if the player’s location is beyond the radius of the playing area, and if so turn the robot 180 degrees so that its next step will hopefully be inside the player area. Now, each zombie is updated by calling its update method. Here too, if the zombie has stepped out of the playing area, we rotate it at random (180 degrees plus or minus 30 degrees). We then also include code of our own to read user input (which we won’t detail here). And so, voilá!

from vpython import *
from robot import *
import random
import math
import ipywidgets as widgets

# variable declarations
global userbot
global running
running = True
GROUND_RADIUS = 50
ZOMBIES = 20

# declare our buttons
fastButton = widgets.Button(description = 'F', width = '60px', height = '60px')
slowButton = widgets.Button(description = 'S', width = '60px', height = '60px')
leftButton = widgets.Button(description = 'L', width = '60px', height = '60px')
rightButton = widgets.Button(description = 'R', width = '60px', height = '60px')
fillerButton0 = widgets.Button(description = '', width = '60px', height = '60px')
resetButton = widgets.Button(description = 'Reset', width = '120px', height = '60px')
quitButton = widgets.Button(description = 'Quit', width = '120px', height = '60px')
fillerButton1 = widgets.Button(description = '', width = '120px', height = '60px')
scene.caption = "To use the directional pad, click on a marked direction. F = Faster, S = Slower, L = turn Left and R = turn Right."

# These functions set up our buttons to read in inputs
def fastButton_handler(s):
    global userbot
    userbot.speed += 0.1
fastButton.on_click(fastButton_handler)

def slowButton_handler(s):
    global userbot
    userbot.speed -= 0.1
slowButton.on_click(slowButton_handler)

def leftButton_handler(s):
    global userbot
    userbot.turn(5)
leftButton.on_click(leftButton_handler)

def rightButton_handler(s):
    global userbot
    userbot.turn(-5)
rightButton.on_click(rightButton_handler)

def quitButton_handler(s):
    global running
    running = False
    print("Exiting the main loop. Ending this vPython session...")
quitButton.on_click(quitButton_handler)

# now arrange and display our GUI
container0 = widgets.HBox(children = [fillerButton0, fastButton, fillerButton0, quitButton])
container1 = widgets.HBox(children = [leftButton, fillerButton0, rightButton, fillerButton1])
container2 = widgets.HBox(children = [fillerButton0, slowButton, fillerButton0, fillerButton1])
display(container0)
display(container1)
display(container2)

def main():
    global userbot
    global running
    ground = cylinder(pos = vector(0, -1, 0),
                      axis = vector(0, 1, 0),
                      radius = GROUND_RADIUS)
    userbot = PlayerBot()
    zombies = makeZombies()
    while running:
        rate(30)
        userbot.update()
        if mag(userbot.position) >= GROUND_RADIUS:
            userbot.turn(180)
        for z in zombies:
            z.update()
            if mag(z.position) >= GROUND_RADIUS:
                z.turn(random.uniform(150, 210))

def makeZombies():
    zombies = []
    for z in range(ZOMBIES):
        theta = random.uniform(0, 360)
        r = random.uniform(0, GROUND_RADIUS)
        x = r * cos(math.radians(theta))
        z = r * sin(math.radians(theta))
        zombies.append(ZombieBot(position = vector(x, 0, z)))
    return zombies
main()

6.9 Conclusion

../_images/Alien8.PNG

I suppose that “yucky” is yet another technical term.

This is all really neat, but why is object-oriented programming such a big deal? As we saw in our Rational example, one benefit of object-oriented programming is that it allows us to define new types of data. You might argue, “Sure, but I could have represented a rational number as a list or tuple of two items and then I could have written functions for doing comparisons, addition, and so forth without using any of this class stuff.” You’re absolutely right, but you then have exposed a lot of yucky details to the user that she or he doesn’t want to know about. For example, the user would need to know that rational numbers are represented as a list or a tuple and would need to remember the conventions for using your comparison and addition functions. One of the beautiful things about object-oriented programming is that all of this “yuckiness” (more technically, “implementation details”) is hidden from the user, providing a layer of abstraction between the use and the implementation of rational numbers.

Layer of abstraction?! What does that mean? Imagine that every time you sat in the driver’s seat of a car you had to fully understand various components of the engine, transmission, steering system, and electronics just to operate the car. Fortunately, the designers of cars have presented us with a nice layer of abstraction: the steering wheel, pedals, and dashboard. We can now do interesting things with our car without having to think about the low-level details. As a driver, we don’t need to worry about whether the steering system uses a rack and pinion or something entirely different. This is precisely what classes provide for us. The inner workings of a class are securely “under the hood,” available if needed, but not the center of attention. The user of the class doesn’t need to worry about implementation details; she or he just uses the convenient and intuitive provided methods. By the way, the “user” of your class is most often you! You too don’t want to be bothered with implementation details when you use the class—you’d rather be thinking about bigger and better things at that point in your programming.

Object-oriented design is the computer science version of modular design, an idea that engineers pioneered long ago and have used with great success. Classes are modules. They encapsulate logical functionality and allow us to reason about and use that functionality without having to keep track of every part of the program at all times. Moreover, once we have designed a good module/class we can reuse it in many different applications.

Finally, in our Robot and Zombies game, we saw the important idea of inheritance. Once we construct one class, we can write special versions that inherit all of the methods and attributes of the “parent” or superclass, but also add their own unique features. In large software systems, there can be a large and deep hierarchy of classes: One class has children classes that inherit from it which in turn have their own children classes, and so forth. This design methodology allows for great efficiencies in reusing rather than rewriting code.

Takeaway message: Classes—the building blocks of object-oriented designs and programs—provide us with a way of providing abstraction that allows us to concentrate on using these building blocks without having to worry about the internal details of how they work. Moreover, once we have a good building block we can use it over and over in all different kinds of programs.