Old teachers never die, they just lose their class.
—Anonymous
Now that you’ve created cool graphics using functions and other code in Processing, you can supercharge your creativity using classes. A class is a structure that lets you create new types of objects. The object types (usually just called objects) can have properties, which are variables, and methods, which are functions. There are times you want to draw multiple objects using Python, but drawing lots of them would be way too much work. Classes make drawing several objects with the same properties easy, but they require a specific syntax you’ll need to learn.
The following example from the official Python website shows how to create a “Dog” object using a class. To code along, open a new file in IDLE, name it dog.py and enter the following code.
dog.py
class Dog:
def __init__(self,name):
self.name = name
This creates a new object called Dog using class Dog. It’s customary in Python and many other languages to capitalize the name of a class, but it’ll still work if you don’t. To instantiate, or create, the class, we have to use Python’s __init__ method, which has two underscores before and two after init, meaning it’s a special method to create (or construct) an object. The __init__ line makes it possible to create instances of the class (in this case, dogs). In the __init__ method, we can create any properties of the class we want. Since it’s a dog, it can have a name, and because every dog has its own name, we use the self syntax. We don’t need to use it when we call the objects, only when we’re defining them.
We can then create a dog with a name using the following line of code:
d = Dog('Fido')
Now d is a Dog and its name is Fido. You can confirm this by running the file and entering the following in the shell:
>>> d.name
'Fido'
Now when we call d.name, we get Fido because that is the name property we just gave to it. We can create another Dog and give it the name Bettisa, like so:
>>> b = Dog('Bettisa')
>>> b.name
'Bettisa'
You can see one dog’s name can be different from another’s, but the program remembers them perfectly! This will be crucial when we give locations and other properties to the objects we create.
Finally, we can give the dog something to do by putting a function in the class. But don’t call it a function! A function inside a class is called a method. Dogs bark, so we’ll add that method to the code in Listing 9-1.
dog.py
class Dog:
def __init__(self,name):
self.name = name
def bark(self):
print("Woof!")
d = Dog('Fido')
Listing 9-1: Creating a dog that barks!
When we call the bark() method of the d dog, it barks:
>>> d.bark()
Woof!
It might not be clear why you’d need a Dog class from this simple example, but it’s good to know you can do literally anything you want with classes and be as creative as you want. In this chapter, we use classes to make lots of useful objects like bouncing balls and grazing sheep. Let’s start with the Bouncing Ball example to see how using classes lets us do something really cool while saving us a lot of work.
Start a Processing sketch and save it as BouncingBall.pyde. We’ll draw a single circle on the screen, which we’ll make into a bouncing ball. Listing 9-2 shows the code for drawing one circle.
BouncingBall.pyde
def setup():
size(600,600)
def draw():
background(0) #black
ellipse(300,300,20,20)
Listing 9-2: Drawing a circle
First, we set the size of the window to be 600 pixels wide and 600 pixels tall. Then we set the background to black and drew a circle using the ellipse() function. The first two numbers in the function describe how far the center of the circle is from the top-left corner of the window, and the last two numbers describe the width and height of the ellipse. In this case, ellipse(300,300, 20,20) creates a circle that is 20 pixels wide and 20 pixels high, located in the center of the display window, as shown in Figure 9-1.
Now that we have successfully created a circle located in the center of the display window, let’s try to make it move.
We’ll make the ball move by changing its position. To do this, let’s first create a variable for the x-value and a variable for the y-value and set them to 300, which is the middle of the screen. Go back to Listing 9-2 and insert the following two lines at the beginning of the code, like in Listing 9-3.
BouncingBall.pyde
xcor = 300
ycor = 300
def setup():
size(600,600)
Listing 9-3: Setting variables for the x- and y-values
Here, we use the xcor variable to represent the x-value and the ycor variable to represent the y-value. Then we set both variables to 300.
Now let’s change the x-value and y-value by a certain number in order to change the location of the ellipse. Make sure to use the variables to draw the ellipse, as shown in Listing 9-4.
BouncingBall.pyde
xcor = 300
ycor = 300
def setup():
size(600,600)
def draw():
➊ global xcor, ycor
background(0) #black
xcor += 1
ycor += 1
ellipse(xcor,ycor,20,20)
Listing 9-4: Incrementing xcor and ycor to change the location of the ellipse
The important thing to notice in this example is global xcor, ycor ➊, which tells Python to use the variables we’ve already created and not to create new ones just for the draw() function. If you don’t include this line, you’ll get an error message, something like “local variable ‘xcor’ referenced before assignment.” Once Processing knows what value to assign to xcor and ycor, we increment them both by 1 and draw the ellipse with its center at the location specified using the global variables: (xcor, ycor).
When you save and run Listing 9-4, you should see the ball move, like in Figure 9-2.
Now the ball moves down and to the right, because its x- and y-values are both increasing, but then it moves off the screen and we never see it again! The program keeps incrementing our variables obediently. It doesn’t know it’s drawing a ball or that we want the ball to bounce off the walls. Let’s explore how to keep the ball from disappearing.
When we change the x-value and the y-value by adding 1, we’re changing the position of an object. In math, this change in position over time is called velocity. A positive change in x over time (positive x-velocity) will look like movement to the right (since x is getting bigger), whereas negative x-velocity will look like movement to the left. We can use this “positive-right, negative-left” concept to make the ball bounce off the wall. First, let’s create the x-velocity and y-velocity variables by adding the following lines to our existing code, as shown in Listing 9-5.
BouncingBall.pyde
xcor = 300
ycor = 300
xvel = 1
yvel = 2
def setup():
size(600,600)
def draw():
global xcor,ycor,xvel,yvel
background(0) #black
xcor += xvel
ycor += yvel
#if the ball reaches a wall, switch direction.
if xcor > width or xcor < 0:
xvel = -xvel
if ycor > height or ycor < 0:
yvel = -yvel
ellipse(xcor,ycor,20,20)
Listing 9-5: Adding code to make the ball bounce off the wall
First, we set xvel = 1 and yvel = 2 to specify how the ball will move. You can use other values and see how they change the movement. Then in the draw() function, we tell Python that xvel and yvel are global variables, and we change the x- and y-coordinates by incrementing using these variables. For example, when we set xcor += xvel, we’re updating the position by the velocity (the change in position).
The two if statements tell the program that if the ball’s position goes outside the boundaries of the screen, it should change the ball’s velocity to its negative value. When we change the ball’s velocity to its negative value, we tell the program to move the ball in the opposite direction it was moving in, making it seem like the ball is bouncing off the wall.
We need to be precise in telling at what point the ball should move in the opposite direction in terms of its coordinates. For example, xcor > width represents cases where xcor is larger than the width of the display window, which is when the ball touches the right edge of the screen. And xcor < 0 represents instances where the xcor is less than 0 or when the ball touches the left edge of the screen. Similarly, ycor > height checks for instances where ycor is larger than the height of the window or when the ball reaches the bottom of the screen. Finally, ycor < 0 checks for instances where the ball reaches the upper edge of the screen. Since moving to the right is positive x-velocity (positive change in x), the opposite direction is negative x-velocity. If the velocity is already negative (it’s moving to the left), then the negative of a negative is a positive, which means the ball will move to the right, just like we want it to.
When you run Listing 9-5, you should see something like what’s shown in Figure 9-3.
The ball looks like it’s bouncing off the walls and therefore stays in view.
Now suppose we want to make another bouncing ball, or many other bouncing balls. How would we do that? We could make a new variable for the second ball’s x-value, another variable for the second ball’s y-value, a third variable for its x-velocity, and a fourth for its y-velocity. Then we’d have to increment its position by its velocity, check if it needs to bounce off a wall, and finally draw it. However, we’d end up with double the amount of code! Adding a third ball would triple our code! Twenty balls would simply be out of the question. You don’t want to have to keep track of all these variables for position and velocity. Listing 9-6 show what this would look like.
#ball1:
ball1x = random(width)
ball1y = random(height)
ball1xvel = random(-2,2)
ball1tvel = random(-2,2)
#ball2:
ball2x = random(width)
ball2y = random(height)
ball2xvel = random(-2,2)
ball2tvel = random(-2,2)
#ball3:
ball3x = random(width)
ball3y = random(height)
ball3xvel = random(-2,2)
ball3tvel = random(-2,2)
#update ball1:
ball1x += ball1xvel
ball1y += ball1yvel
ellipse(ball1x,ball1y,20,20)
#update ball2:
ball2x += ball2xvel
ball2y += ball2yvel
ellipse(ball2x,ball2y,20,20)
#update ball3:
ball3x += ball3xvel
ball3y += ball3yvel
ellipse(ball3x,ball3y,20,20)
Listing 9-6: Creating multiple balls without classes. Way too much code!
This is the code for creating only three balls. As you can see, it’s very long, and this doesn’t even include the bouncing part! Let’s see how we can use classes to make this task easier.
In programming, a class works like a recipe that details a way to create an object with its own specific properties. Using classes, we tell Python how to make a ball once. Then all we have to do is create a bunch of balls using a for loop and put them in a list. Lists are great for saving numerous things—strings, numbers, and objects!
Follow these three steps when using classes to create objects:
Let’s use these steps to put the code we’ve already written into a class.
The first step in creating objects using classes is to write a class that tells the program how to make a ball. Let’s add the code in Listing 9-7 at the very beginning of our existing program.
BouncingBall.pyde
ballList=[] #empty list to put the balls in
class Ball:
def __init__(self,x,y):
'''How to initialize a Ball'''
self.xcor = x
self.ycor = y
self.xvel = random(-2,2)
self.yvel = random(-2,2)
Listing 9-7: Defining a class called Ball
Note that because we’re putting the position and velocity variables into the Ball class as properties, you can delete the following lines from your existing code:
xcor = 300
ycor = 300
xvel = 1
yvel = 2
In Listing 9-7, we create an empty list we’ll use to save the balls in; then we start defining the recipe. The name of a class object, which is Ball in this case, is always capitalized. The __init__ method is a requirement to create a class in Python that contains all the properties the object gets when it’s initialized. Otherwise, the class won’t work.
The self syntax simply means every object has its own methods and properties, which are functions and variables that can’t be used except by a Ball object. This means that each Ball has its own xcor, its own ycor, and so on. Because we might have to create a Ball at a specific location at some point, we made x and y parameters of the __init__ method. Adding these parameters allows us to tell Python the location of a Ball when we create it, like this:
Ball(100,200)
In this case, the ball will be located at the coordinate (100,200).
The last lines in Listing 9-7 tell Processing to assign a random number between –2 and 2 to be the x- and y-velocity of the new ball.
Now that we’ve created a class called Ball, we need to tell Processing how to update the ball every time the draw() function loops. We’ll call that the update method and nest it inside the Ball class, just like we did with __init__. You can simply cut and paste all the ball code into the update() method and then add self. to each of the object’s properties, as shown in Listing 9-8.
BouncingBall.pyde
ballList=[] #empty list to put the balls in
class Ball:
def __init__(self,x,y):
'''How to initialize a Ball'''
self.xcor = x
self.ycor = y
self.xvel = random(-2,2)
self.yvel = random(-2,2)
def update(self):
self.xcor += self.xvel
self.ycor += self.yvel
#if the ball reaches a wall, switch direction
if self.xcor > width or self.xcor < 0:
self.xvel = -self.xvel
if self.ycor > height or self.ycor < 0:
self.yvel = -self.yvel
ellipse(self.xcor,self.ycor,20,20)
Listing 9-8: Creating the update() method
Here, we placed all the code for moving and bouncing a ball into the update() method of the Ball class. The only new code is self in the velocity variables, making them velocity properties of the Ball object. Although it might seem like there are many instances of self, that’s how we tell Python that the x-coordinate, for example, belongs to that specific ball and not another. Very soon, Python is going to be updating a hundred balls, so we need self to keep track of each one’s location and velocity.
Now that the program knows how to create and update a ball, let’s update the setup() function to create three balls and put them into the ball list (ballList), as shown in Listing 9-9.
def setup():
size(600,600)
for i in range(3):
ballList.append(Ball(random(width),
random(height)))
Listing 9-9: Creating three balls in the setup() function
We created ballList in Listing 9-7 already, and here we’re appending to the list a Ball at a random location. When the program creates (instantiates) a new ball, it will now choose a random number between 0 and the width of the screen to be the x-coordinate and another random number between 0 and the height of the screen to be the y-coordinate. Then it’ll put that new ball into the list. Because we used the loop for i in range(3), the program will add three balls to the ball list.
Now let’s tell the program to go through ballList and update all the balls in the list (which means drawing them) every loop using the following draw() function:
BouncingBall.pyde
def draw():
background(0) #black
for ball in ballList:
ball.update()
Note that we still want the background to be black, and then we loop over the ball list and for every ball in the list we run its update() method. All the previous code in draw() went into the Ball class!
When you run this sketch, you should see three balls moving around the screen and bouncing off the walls! The great thing about using classes is that it’s super easy to change the number of balls. All you have to do is change the number in for i in range(number): in the setup() function to create even more bouncing balls. When you change this to 20, for example, you’ll see something like Figure 9-4.
What’s cool about using classes is that you can give an object any properties or methods you want. For example, we don’t have to make our balls all the same color. Add the three lines of code shown in Listing 9-10 to your existing Ball class.
BouncingBall.pyde
class Ball:
def __init__(self,x,y):
'''How to initialize a Ball'''
self.xcor = x
self.ycor = y
self.xvel = random(-2,2)
self.yvel = random(-2,2)
self.col = color(random(255),
random(255),
random(255))
Listing 9-10: Updating the Ball class
This code gives every ball its own color when it’s created. Processing’s color() function needs three numbers that represent red, green, and blue, respectively. RGB values go from 0 to 255. Using random(255) lets the program choose the numbers randomly, resulting in a randomly chosen color. However, because the __init__ method runs only one time, once the ball has a color, it keeps it.
Next, in the update() method, add the following line so the ellipse gets filled with its own randomly chosen color:
fill(self.col)
ellipse(self.xcor,self.ycor,20,20)
Before a shape or line gets drawn, you can declare its color using fill for shapes or stroke for lines. Here, we tell Processing to use the ball’s own color (using self) to fill in the following shape.
Now when you run the program, each ball should have a random color, as shown in Figure 9-5!
Now that you can create classes, let’s make something useful. We’ll code a Processing sketch of an ecosystem that simulates sheep walking around eating grass. In this sketch, the sheep have a certain level of energy that gets depleted as they walk around, and their energy gets replenished when they eat grass. If they get enough energy, they reproduce. If they don’t get enough energy, they die. We could potentially learn a lot about biology, ecology, and evolution by creating and tweaking this model.
In this program, the Sheep objects are kind of like the Ball objects you created earlier in this chapter; each has its own x- and y-position and size, and is represented by a circle.
Start a new Processing sketch and save it as SheepAndGrass.pyde. First, let’s create a class that makes a Sheep object with its own x- and y-position and its own size. Then we’ll create an update method that draws an ellipse representing the sheep’s size at the sheep’s location.
The class code is nearly identical to the Ball class, as you can see in Listing 9-11.
SheepAndGrass.pyde
class Sheep:
def __init__(self,x,y):
self.x = x #x-position
self.y = y #y-position
self.sz = 10 #size
def update(self):
ellipse(self.x,self.y,self.sz,self.sz)
Listing 9-11: Creating a class for one sheep
Because we know we’ll be making a bunch of sheep, we start off creating a Sheep class. In the required __init__ method, we set the x- and y-coordinates of the sheep to the parameters we’ll declare when creating a sheep instance. I’ve set the size of the sheep (the diameter of the ellipse) to 10 pixels, but you can have bigger or smaller sheep if you like. The update() method simply draws an ellipse of the sheep’s size at the sheep’s location.
Here’s the setup() and draw() code for a Processing sketch containing one Sheep, which I’ve named shawn. Add the code shown in Listing 9-12 right below the update() method you just wrote in Listing 9-11.
def setup():
global shawn
size(600,600)
#create a Sheep object called shawn at (300,200)
shawn = Sheep(300,200)
def draw():
background(255)
shawn.update()
Listing 9-12: Creating a Sheep object named shawn
We first create shawn, an instance of a Sheep object, in the setup() function. Then we update it in the draw() function—but Python doesn’t know we mean the same shawn unless we tell it that shawn is a global variable.
When you run this code, you should see something like what’s shown in Figure 9-6.
You get a white screen with a little circular sheep at the coordinate (300,200), which is 300 pixels to the right of the starting point and 200 pixels down.
Now let’s teach a Sheep how to move around. We’ll start by programming the Sheep to move around randomly. (You can always program it to move differently in the future if you want to.) Listing 9-13 changes the x- and y-coordinates of a Sheep by a random number between –10 and 10. Return to your existing code and add the following lines above the ellipse() function within the update() method:
SheepAndGrass.pyde
def update(self):
#make sheep walk randomly
move = 10 #the maximum it can move in any direction
self.x += random(-move, move)
self.y += random(-move, move)
fill(255) #white
ellipse(self.x,self.y,self.sz,self.sz)
Listing 9-13: Making the sheep move randomly
This code creates a variable called move to specify the maximum value or distance the sheep will be able to move on the screen. Then we set move to 10 and use it to update the sheep’s x- and y-coordinates by a random number between -move (–10) and move (10). Finally, we use fill(255) to set the sheep’s color to white for now.
When you run this code, you should see the sheep wandering around randomly—and it might wander off the screen.
Let’s give the sheep some company. If we want to create and update more than one object, it’s a good idea to put them in a list. Then in the draw() function, we’ll go through the list and update each Sheep. Update your existing code to look like Listing 9-14.
SheepAndGrass.pyde
class Sheep:
def __init__(self,x,y):
self.x = x #x-position
self.y = y #y-position
self.sz = 10 #size
def update(self):
#make sheep walk randomly
move = 10 #the maximum it can move in any direction
self.x += random(-move, move)
self.y += random(-move, move)
fill(255) #white
ellipse(self.x,self.y,self.sz,self.sz)
sheepList = [] #list to store sheep
def setup():
size(600,600)
for i in range(3):
sheepList.append(Sheep(random(width),
random(height)))
def draw():
background(255)
for sheep in sheepList:
sheep.update()
Listing 9-14: Creating more sheep using a for loop
This code is similar to the code we wrote to put the bouncing balls in a list. First, we create a list to store the sheep. Then we create a for loop and put a Sheep in the sheep list. Then in the draw() function, we write another for loop to go through the sheep list and update each one according to the update() method we already defined. When you run this code, you should get three Sheep walking around randomly. Change the number 3 in for i in range(3): to a larger number to add even more sheep.
Walking takes up energy! Let’s give the sheep a certain level of energy when they’re created and take away their energy when they walk. Use the code in Listing 9-15 to update your existing __init__ and update() methods in the SheepAndGrass.pyde.
class Sheep:
def __init__(self,x,y):
self.x = x #x-position
self.y = y #y-position
self.sz = 10 #size
self.energy = 20 #energy level
def update(self):
#make sheep walk randomly
move = 1
self.energy -= 1 #walking costs energy
if sheep.energy <= 0:
sheepList.remove(self)
self.x += random(-move, move)
self.y += random(-move, move)
fill(255) #white
ellipse(self.x,self.y,self.sz,self.sz)
Listing 9-15: Updating __init__ and update() with the energy property
We do this by creating an energy property in the __init__ method and set it to 20, the energy level every sheep starts with. Then self.energy -= 1 in the update() method lowers the sheep’s energy level by 1 when it walks around.
Then we check whether the sheep is out of energy, and if it is, we remove it from the sheepList. Here, we use a conditional statement to check whether if sheep.energy <= 0 returns True. If so, we remove that sheep from the sheepList using the remove() function. Once that Sheep instance is gone from the list, it doesn’t exist anymore.
When you run the program, you should see the Sheep move around for a second and then disappear—walking around is costing the sheep energy, and once that energy is gone, the sheep dies. What we need to do is to give the sheep grass to eat. We’ll call each patch of grass Grass and make a new class for it. Grass will have its own x- and y-value, size, and energy content. We’ll also make it change color when it’s eaten.
In fact, we’ll be using a bunch of different colors in this sketch for our sheep and our grass, so let’s add the code in Listing 9-16 to the very beginning of the program so we can just refer to the colors by their names. Feel free to add other colors too.
WHITE = color(255)
BROWN = color(102,51,0)
RED = color(255,0,0)
GREEN = color(0,102,0)
YELLOW = color(255,255,0)
PURPLE = color(102,0,204)
Listing 9-16: Setting colors as constants
Using all-caps for the color names indicates that they’re constants and won’t change in value, but that’s just for the programmer. There’s nothing inherently magical about the constants, and you can change these values if you want. Setting constants lets you just type the names of the colors instead of having to write the RGB values every time. We’ll do this when we make the grass green. Update your existing code by adding the code in Listing 9-17 right after the Sheep class in SheepAndGrass.pyde:
class Grass:
def __init__(self,x,y,sz):
self.x = x
self.y = y
self.energy = 5 #energy from eating this patch
self.eaten = False #hasn't been eaten yet
self.sz = sz
def update(self):
fill(GREEN)
rect(self.x,self.y,self.sz,self.sz)
Listing 9-17: Writing the Grass class
You’re probably starting to get used to the structure of the class notation. It conventionally starts with the __init__ method, where you create its properties. In this case, you tell the program that Grass will have an x- and y-location, an energy level, a Boolean (True/False) variable that keeps track of whether the grass has been eaten or not, and a size. To update a patch of grass, we just create a green rectangle at the Grass object’s location.
Now we have to initialize and update our grass, the same way we did for our sheep. Because there will be a lot of grass, let’s create a list for it. Before the setup() function, add the following code.
sheepList = [] #list to store sheep
grassList = [] #list to store grass
patchSize = 10 #size of each patch of grass
We might want to vary the size of the patch of grass in the future, so let’s create a variable called patchSize so we’ll only have to change it in one place. In the setup() function, after creating the sheep, create the grass by adding the new code in Listing 9-18.
def setup():
global patchSize
size(600,600)
#create the sheep
for i in range(3):
sheepList.append(Sheep(random(width),
random(height)))
#create the grass:
for x in range(0,width,patchSize):
for y in range(0,height,patchSize):
grassList.append(Grass(x,y,patchSize))
Listing 9-18: Updating the Grass object using patchSize variable
In this example, global patchSize tells Python that we’re using the same patchSize variable everywhere. Then we write two for loops (one for x and the other for y) to append Grass to the grass list so we can create a square grid of grass.
Then we update everything in the draw() function, just like we did for the sheep. Whatever is drawn first will be drawn covered up by what’s drawn after, so we’ll update the grass first by changing the draw() function to the code in Listing 9-19.
SheepAndGrass.pyde
def draw():
background(255)
#update the grass first
for grass in grassList:
grass.update()
#then the sheep
for sheep in sheepList:
sheep.update()
Listing 9-19: Updating the grass before the sheep
When you run this code, you should see a grid of green squares, like in Figure 9-7.
Let’s shut off the black outline so it’ll look like a smooth field of grass. Add noStroke() to the setup() function to remove the outline of the green squares:
def setup():
global patchSize
size(600,600)
noStroke()
Now we have our grass!
How do we make it so that when a sheep is on a patch of grass, the sheep gets the grass’s energy and the patch of grass turns brown to show that the sheep has eaten it? Change the update() method for Grass by adding the following lines of code:
def update(self):
if self.eaten:
fill(BROWN)
else:
fill(GREEN)
rect(self.x,self.y,self.sz,self.sz)
This code tells Processing that if the patch of grass is “eaten,” the rectangle should be filled with a brown color. Otherwise, the grass should be colored green. There’s more than one way for a sheep to “eat” grass. One way is to make each patch of grass check the entire sheepList for a sheep on its location, which could mean tens of thousands of patches are checking thousands of sheep. Those numbers could get big. However, because each patch of grass is in the grassList, an alternate way is that when a sheep changes its location, it could simply change the patch at that location to “eaten” (if it isn’t already) and get energy from eating it. That would mean a lot less checking.
The problem is that the x- and y-coordinates of the sheep don’t exactly match up to where the patches of grass are in the grassList. For example, our patchSize is 10, meaning that if a sheep is at (92,35), it’ll be on the 10th patch to the right and the 4th patch down (because the “first” patch is from x = 0 to x = 9). We’re dividing by the patchSize to get the “scaled” x- and y-values, 9 and 3.
However, the grassList doesn’t have rows and columns. We do know that the x-value, 9, means it’s the 10th row (don’t forget row 0), so we’ll just have to add in nine rows of 60 (the height divided by the patchSize) and then add the y-value to get the index of the patch of grass the sheep is on. Therefore, we need a variable to tell us how many patches of grass there are in a row, which we’ll call rows_of_grass. Add global rows_of_grass to the beginning of the setup() function and then add this line to setup() after declaring the size:
rows_of_grass = height/patchSize
This takes the width of the display window and divides it by the size of the patches of grass to tell us how many columns of grass there are. The code to add to the Sheep class is in Listing 9-20.
SheepAndGrass.pyde
self.x += random(-move, move)
self.y += random(-move, move)
#"wrap" the world Asteroids-style
➊ if self.x > width:
self.x %= width
if self.y > height:
self.y %= height
if self.x < 0:
self.x += width
if self.y < 0:
self.y += height
#find the patch of grass you're on in the grassList:
➋ xscl = int(self.x / patchSize)
yscl = int(self.y / patchSize)
➌ grass = grassList[xscl * rows_of_grass + yscl]
if not grass.eaten:
self.energy += grass.energy
grass.eaten = True
Listing 9-20: Updating the sheep’s energy level and turning the grass brown
After updating the sheep’s location, we “wrap” the coordinates ➊ so if the sheep walks off the screen in one direction, it shows up on the other side of the screen, like in the video game Asteroids. We calculate which patch the sheep is on according to the patchSize ➋. Then we use code to go from x- and y-values to the index of that patch in the grassList ➌. We now know the exact index of the patch of grass the sheep is on. If this patch of grass is not already eaten, the sheep eats it! It gets the energy from the grass, and the grass’s eaten property is set to True.
Run this code, and you’ll see the three sheep running around eating grass, which turns brown once it’s eaten. Slow the sheep down by changing the move variable to a lesser value, such as 5. You can also scale down the patches by changing one number, the patchSize variable, to 5. Try other values if you like.
Now we can create more Sheep. Let’s change the number in the for i in range line to 20, like so:
#create the sheep
for i in range(20):
sheepList.append(Sheep(random(width),
random(height)))
When you run this code, you should see something like Figure 9-8.
Now there are 20 sheep walking around, leaving patches of brown grass.
Let’s have the sheep choose a color when they’re “born.” After the code defining the color constants, let’s put some colors into a color list, like this:
YELLOW = color(255,255,0)
PURPLE = color(102,0,204)
colorList = [WHITE,RED,YELLOW,PURPLE]
Make the following changes to the Sheep class to use different colors. First, you need to give Sheep a color property. Because color is already a keyword in Processing, col is used in Listing 9-21.
class Sheep:
def __init__(self,x,y,col):
self.x = x #x-position
self.y = y #y-position
self.sz = 10 #size
self.energy = 20
self.col = col
Listing 9-21: Adding a color property to the Sheep class
Then in the update() method, replace the fill line with this:
fill(self.col) #its own color
ellipse(self.x,self.y,self.sz,self.sz)
Before the ellipse is drawn, fill(self.col) tells Processing to fill the ellipse with the Sheep’s own randomly chosen color.
When all the Sheep are instantiated in the setup() function, you need to give them a random color. That means at the top of the program you have to import the choice() function from the random module, like this:
from random import choice
Python’s choice() function allows you to have one item chosen at random from a list and then returned. We should be able to tell the program to do this as follows:
choice(colorList)
Now the program will return a single value from the color list. Finally, when you’re creating the Sheep, add the random choice of color from the color list as one of the arguments you pass to the Sheep constructor, as shown here:
def setup():
size(600,600)
noStroke()
#create the sheep
for i in range(20):
sheepList.append(Sheep(random(width),
random(height),
choice(colorList)))
Now when you run this code, you should see a bunch of randomly colored sheep walking around the screen, as shown in Figure 9-9.
Each new sheep gets assigned one of the four colors we defined in colorList: white, red, yellow, or purple.
Unfortunately, in our current program the sheep eat the grass until they wander too far away from the grass, run out of energy, and die. To prevent this, let’s tell the sheep to use some of that energy to reproduce.
Let’s use the code in Listing 9-22 to tell the sheep to reproduce if their energy level reaches 50.
if self.energy <= 0:
sheepList.remove(self)
if self.energy >= 50:
self.energy -= 30 #giving birth takes energy
#add another sheep to the list
sheepList.append(Sheep(self.x,self.y,self.col))
Listing 9-22: Adding a conditional for sheep to reproduce
The conditional if self.energy >= 50: checks whether that sheep’s energy is greater than or equal to 50. If it is, we decrement the energy level by 30 for birthing and add another sheep to the sheep list. Notice that the new sheep is at the same location and is the same color as its parent. Run this code, and you should see the sheep reproduce, like in Figure 9-10.
Soon you should see what looks like tribes of similarly colored sheep.
Unfortunately, the sheep soon eat up all the grass in their area and die (probably a lesson in there somewhere). We need to allow our grass to regrow. To do this, change the Grass’s update() method to this:
def update(self):
if self.eaten:
if random(100) < 5:
self.eaten = False
else:
fill(BROWN)
else:
fill(GREEN)
rect(self.x,self.y,self.sz,self.sz)
The Processing code random(100) generates a random number between 0 and 100. If the number is less than 5, we regrow a patch of grass by setting its eaten property to False. We use the number 5 because this gives us a probability of 5/100 that eaten grass will regrow during each frame. Otherwise, it stays brown.
Run the code, and you should see something like Figure 9-11.
Now you might get so many sheep that the program starts to slow down! This could be because the sheep have too much energy. If so, try reducing the amount of energy each patch of grass contains from 5 to 2:
class Grass:
def __init__(self,x,y,sz):
self.x = x
self.y = y
self.energy = 2 #energy from eating this patch
self.eaten = False #hasn't been eaten yet
self.sz = sz
That seems to be a good balance that lets the sheep population grow at a reasonable pace. Play around with the numbers all you want—it’s your world!
Let’s give one of the sheep groups an advantage. You can choose any advantage you can think of (getting more energy from grass or producing more offspring at a time, for instance). For this example, we’re going to let the purple sheep walk a little further than the others. Will that make any difference? To find out, make the Sheep’s update() method match the following code:
def update(self):
#make sheep walk randomly
move = 5 #the maximum it can move in any direction
if self.col == PURPLE:
move = 7
self.energy -= 1
This conditional checks whether the Sheep’s color is purple. If so, it sets the Sheep’s move value to 7. Otherwise, it leaves the value at 5. This allows the purple sheep to travel further, and therefore more likely to find green patches, than the other sheep. Let’s run the code and check the outcome, which should look like Figure 9-12.
After a little while it sure looks like that tiny advantage paid off for the purple sheep. They’re dominating the environment and pushing out all the other sheep just by competing for grass. This simulation could spark interesting discussions about ecology, invasive species, biodiversity, and evolution.
In this chapter, you learned how to make objects using classes, which involved defining the class using properties and then instantiating (“creating”) and updating the object. This let you create multiple similar-but-independent objects with the same properties more efficiently. The more you use classes, the more creative you can get by making autonomous objects walk, fly, or bounce around without your having to code every step!
Knowing how to use classes supercharges your coding abilities. Now you can create models of complicated situations easily, and once you tell the program what to do with one particle, or planet, or sheep, it’ll be able to make a dozen, a hundred, or even a million of them very easily!
You also got a taste of setting up models to explore physical, biological, chemical, or environmental situations with very few equations! A physicist once told me that’s often the most efficient method for solving problems involving many factors, or “agents.” You set up a computer model, let it run, and look at the results.
In the next chapter, you’ll learn how to create fractals using an almost-magical phenomenon called recursion.