If you read about variables, repetition and commands you already know a great deal about how to keep your code tidy and organized. Variables can be used to store data, commands to manipulate data, and for-loops to do things (e.g. traverse lists) multiple times. As you move on to bigger projects you'll probably want to learn about classes as well. A class is a programming convention that defines a thing (or object) together with all of the thing's properties (e.g. variables) and behavior methods (e.g. commands). If you take a look at the source code of NodeBox libraries you'll notice that they are full of classes.
For example, a Creature class would consist of all the traits shared by different creatures, such as size, position, speed and direction (properties), and avoid_obstacle(), eat_food() (methods).
We can then use the Creature class to define many different instances of a creature. For example, an ant is a small and speedy creature that wanders around randomly looking for food, avoiding obstacles by going around them. A dinosaur is also a creature, but bigger and slower. It eats anything crossing its path and avoids obstacles by crashing through them.
- class: the abstract characteristics (properties and methods) of a thing.
- object: one particular instance of a class.
- properties: variables tied to an instance of a class.
- methods: commands that manipulate an instance's properties.
Class definition and initialization
Each class has an __init__() method which is executed once when an instance of the class is created. The __init__() method sets all the starting values for an object's properties.
For example, here's what a simple definition of a Creature class would look like:
class Creature: def __init__(self, x, y, speed=1.0, size=4): self.x = x self.y = y self.speed = speed self.size = size
As you can see a creature has x and y properties (the location of the creature) and speed and size properties (which we'll use to move the creature around). All of them can be set when a new creature is "instantiated". The speed and size properties have default values so they are optional.
ant = Creature(100, 100, speed=2.0) dinosaur = Creature(200, 250, speed=0.25, size=45)
To change a property value for a creature later on we can simply do:
ant.speed = 2.5
- Class names use CamelCase notation.
- Properties that are only for private use inside the class start with _
Methods and the self parameter
Let's add a roam() method to the Creature class to move creatures around randomly. A class method looks exactly like a command definition but it always takes self as the first parameter. The self parameter is the current instance of the class. It allows you to access all of an object's current property values and methods from inside the method.
class Creature: def __init__(self, x, y, speed=1.0, size=4): self.x = x self.y = y self.speed = speed self.size = size self._vx = 0 self._vy = 0 def roam(self): """ Creature changes heading aimlessly. """ v = self.speed self._vx += random(-v, v) self._vy += random(-v, v) self._vx = max(-v, min(self._vx, v)) self._vy = max(-v, min(self._vy, v)) self.x += self._vx self.y += self._vy
So now we have added two new properties to the class, _vx and _vy, which store a creature's current heading. Both property names start with an underscore because they are for private use inside the class methods (e.g. no one should directly manipulate ant._dx from the outside).
Each time the roam() method is called we add or subtract something of the creature's speed to the heading, like a compass twirling around randomly. We also make sure the horizontal and vertical heading do not exceed the creature's speed. Otherwise the creature would start going faster and faster which isn't very realistic. Finally, we update the creature's position by moving it to the new heading.
Now we can create two ants (a small fast one and a big slow one). Because their size and speed differ they will roam in different ways.
ant1 = Creature(300, 300, speed=2.0) ant2 = Creature(300, 300, speed=0.5, size=8) speed(30) def draw(): ant1.roam() ant2.roam() oval(ant1.x, ant1.y, ant1.size, ant1.size) oval(ant2.x, ant2.y, ant2.size, ant2.size)
Taking it further: obstacle avoidance
Now that we know the basics about classes, properties and methods we can take things a lot further. We can define different classes for different things and have them interact with each other.
If we want to have our little critters avoid obstacles while wandering around, they will need to have some sense of the world around them. Know where the obstacles are located. So we'll need to keep track of obstacle instances. Then we'll introduce the concept of a feeler. A feeler could be the creature's sight, hearing or antennae. Basically it is a point in front of where the creature is heading. If this point falls inside an obstacle, it's time for the creature to change its. direction.
The creature is heading into an obstacle - it can touch the obstacle with its forward feeler.
It needs to adjust its bearing clockwise to avoid the obstacle.
We'll start out by creating a World class which we can use to store a list of obstacles. Later on we can add all sorts of other stuff to the world (e.g. weather methods, colony location properties, etc...).
class World: def __init__(self): self.obstacles = 
Next we'll make an Obstacle class.
An obstacle in the world is a basic object that has a position and a radius.
class Obstacle: def __init__(self, x, y, radius): self.x = x self.y = y self.radius = radius
To know if a creature is going to run into an obstacle we need to know if the tip of its feeler intersects with an obstacle. Obviously we're going to need some math to calculate the coordinates of the tip of the feeler. Luckily everything we need is already described in the tutorial on math geometry. We can copy-paste the commands and add them to our script:
from math import degrees, atan2 from math import sqrt, pow from math import radians, sin, cos def angle(x0, y0, x1, y1): """ Returns the angle between two points. """ return degrees( atan2(y1-y0, x1-x0) ) def distance(x0, y0, x1, y1): """ Returns the distance between two points. """ return sqrt(pow(x1-x0, 2) + pow(y1-y0, 2)) def coordinates(x0, y0, distance, angle): """ Returns the coordinates of given distance and angle from a point. """ return (x0 + cos(radians(angle)) * distance, y0 + sin(radians(angle)) * distance)
With the above coordinates() command we can:
- calculate the location of a point (tip of the feeler)
- at a given distance (feeler length)
- under a given angle (creature heading)
- from a starting point (creature location).
So first of all we'll need to add a new feeler_length property to the Creature class, and a new heading() method that calculates the angle between a creature's current position and its next position. We also add a new world property to the Creature class. That way each creature has access to all the obstacles in the world. We loop through the list of the world's obstacles in the avoid_obstacles() method.
The avoid_obstacles() method will do the following:
- calculate the direction angle of a creature,
- traverse all obstacles in the world,
- for each obstacle, check if it is closer than the feeler length,
- if it is, check if the coordinates of the tip of the feeler are inside the obstacle,
- and if so, steer away in the most logical direction.
class Creature: def __init__(self, world, x, y, speed=1.0, size=4): self.x = x self.y = y self.speed = speed self.size = size self.world = world self.feeler_length = 25 self._vx = 0 self._vy = 0 def heading(self): """ Returns the creature's heading as angle in degrees. """ return angle(self.x, self.y, self.x+self._vx, self.y+self._vy) def avoid_obstacles(self, m=0.4, perimeter=4): # Find out where the creature is going. a = self.heading() for obstacle in self.world.obstacles: # Calculate the distance between the creature and the obstacle. d = distance(self.x, self.y, obstacle.x, obstacle.y) # Respond faster if the creature is very close to an obstacle. if d - obstacle.radius < perimeter: m *= 10 # Check if the tip of the feeler falls inside the obstacle. # This is never true if the feeler length # is smaller than the distance to the obstable. if d - obstacle.radius <= self.feeler_length: tip_x, tip_y = coordinates(self.x, self.y, d, a) if distance(obstacle.x, obstacle.y, tip_x, tip_y) < obstacle.radius: # Nudge the creature away from the obstacle. m *= self.speed if tip_x < obstacle.x: self._vx -= random(m) if tip_y < obstacle.y: self._vy -= random(m) if tip_x > obstacle.x: self._vx += random(m) if tip_y > obstacle.y: self._vy += random(m) if d - obstacle.radius < perimeter: return def roam(self): """ Creature changes heading aimlessly. With its feeler it will scan for obstacles and steer away. """ self.avoid_obstacles() v = self.speed self._vx += random(-v, v) self._vy += random(-v, v) self._vx = max(-v, min(self._vx, v)) self._vy = max(-v, min(self._vy, v)) self.x += self._vx self.y += self._vy
Another easy example of classes is the Tendrils item in the gallery.