10. Moving objects
Objects and classes
Sometimes it makes sense for several pieces of state to be packaged up together as describing a single entity. For example, Processing provides the color() function to collect red, green, and blue values into a single thing that can be stored in a variable of type color. When dealing with moving objects, it's very helpful to be able to do that ourselves -- keep together the x and y coordinates, step sizes, radius, etc. of each object in a sketch.
The class mechanism allows us to do that (see Programming Notes for details). Let us reimplement the random wanderer sketch. First, we define a new class, called Wanderer; objects of this class will have two fields, storing the current x and y coordinates. We create a new Wanderer object using (new) followed by the class name and some parameters (in our case the initial x and y value). We then access and update the coordinates with the dot notation, getting the x and y fields of the ball object as ball.x and ball.y.
let wanderer; function setup() { createCanvas(400,400); noStroke(); background(0); wanderer = new Wanderer(200,200); // Create a Wanderer at the center } function draw() { fill(0,3); noStroke(); rect(0,0,width,height); fill(255); stroke(128); ellipse(wanderer.x,wanderer.y,20,20); // Move the position by random steps in x and y wanderer.x = wanderer.x+random(-10,10); wanderer.y = wanderer.y+random(-10,10); }
// A class describing what a Wonderer is class Wanderer { constructor (x, y) { this.x = x; this.y = y; } }
It's important to distinguish between the declaration of a variable (and creation of a new object) and the definition of a class. Here, a wanderer object is stored in a variable called "let wanderer". The declaration works the same as we've seen with other types (although we use new to create it, providing some initial values). The class definition simply says what the objects will look like.
This version also introduces a constructor, a method used to initialize an object when we create it (and called with the same name as the call itself). When we call new Wanderer(50,50) we provide two values (50,50), which are x and y in the constructor. The constructor then initializes the object's this.x value to x (which is 50) and its this.y value to y (also 50).
This first object-oriented sketch is a bit of an improvement over the original sketch in that it keeps the x and y coordinates together. It also gives a name to the package, which improves the readability of the code. However, the real power comes in restructuring the code further to define functions, in the class definition, called methods. A method operates on the state of an object; by putting it in the class definition, we're keeping things together (state and functions) that belong together, leading to more coherent code.
let wanderer; // will create in setup() function setup() { createCanvas(400,400); noStroke(); background(0); wanderer = new Wanderer(random(width),random(height),10,color(255)); } function draw() { background(0,20); wanderer.draw(); wanderer.update(); }
// A class describing what a Wonderer is, including what he can do class Wanderer { // Initialize a Wanderer to be at the given coordinates constructor(x, y, r, c) { this.x = x; this.y = y; this.r = r; this.c = c; } // Draw a Wanderer, wherever it happens to be now draw() { fill(this.c); stroke(red(this.c)/2,green(this.c)/2,blue(this.c)/2); ellipse(this.x,this.y,this.r*2,this.r*2); } // Update the state of a Wanderer update() { let r = this.r this.x+=random(-r,r); this.y+=random(-r,r); } }
Here we separate out the draw() method and the update() method of the Wanderer, to emphasize that they're doing conceptually different things. The methods and their names is totally up to us -- we don't have to call a draw() method from the draw() function, it just makes things clear. Note that within the bodies of these methods, we don't have to use the dot notation to access the fields. When we call wanderer.update(), within the body of update(), "x" automatically refers to wanderer.x. This makes sense when considering both the fields and the methods to be part of the object. Here we also extended the class to include a radius and color.
This packaging of state and functionality makes it easy to have multiple wanderers. Later in the course we will cover how to create and minulate multiple objects. For now, let us see an example with just two wonderers (using the same Wanderer class).
let wanderer1, wanderer2; // will create in setup() function setup() { createCanvas(400,400); noStroke(); wanderer1 = new Wanderer(random(width), random(height),10,color(200,255,200)); wanderer2 = new Wanderer(random(width), random(height),10,color(200,200,255)); } function draw() { bacgkround(0,20); wanderer1.draw(); wanderer2.draw(); wanderer1.update(); wanderer2.update(); }
// A class describing what a Wonderer is, including what he can do class Wanderer { // Initialize a Wanderer to be at the given coordinates constructor(x, y, r, c) { this.x = x; this.y = y; this.r = r; this.c = c; } // Draw a Wanderer, wherever it happens to be now draw() { fill(this.c); stroke(red(this.c)/2,green(this.c)/2,blue(this.c)/2); ellipse(this.x,this.y,this.r*2,this.r*2); } // Update the state of a Wanderer update() { let r = this.r this.x+=random(-r,r); this.y+=random(-r,r); } }
Agents
We've already seen several examples on how move objects by updating the current x and y coordinates in different manners (moving in straight lines, bouncing, orbiting, etc.). With classes, we can ember these different behavior in different class and populate worls with agents doing different things. Let us start by creating a class for a ball that bouncing off the walls. Note that in this example we initialize the velocity inside the object itself, instead of passing everything to the constructor.
let ball; // the bouncing ball; created in setup() function setup() { createCanvas(400,400); noStroke(); ball = new BouncingBall(random(50,width-50),random(50,height-50), 10, color(255)); } function draw() { background(0,20); ball.draw(); ball.update(); } class BouncingBall { constructor(x, y, r, c) { this.x = x; this.y = y; this.r = r; this.c = c; this.vx = random(-5,5); this.vy = random(-5,5); } draw() { fill(this.c); stroke(red(this.c)/2,green(this.c)/2,blue(this.c)/2); ellipse(this.x,this.y,this.r*2,this.r*2); } update() { // Move in the appropriate direction by the step size this.x+=this.vx; this.y+=this.vy; // If too close to the wall (consider the radius of the ball), // move back and change direction if (this.x > width-this.r || this.x < this.r) { this.x = constrain(this.x,this.r,width-this.r); this.vx = -this.vx; } if (this.y > height-this.r || this.y < this.r) { this.y = constrain(this.y,this.r,height-this.r); this.vy = -this.vy; } } }
We can adapct other behaviors to class, such as for example wandering around the mouse position. Note there how we use mouseX nad mouseY inside the class, i.e. a class can access global variables and change its behvaiour accordingly.
let orbiter; function setup() { createCanvas(400,400); noStroke(); orbiter = new MouseOrbiter(15, 5, color(255)); } function draw() { background(0,20); orbiter.draw(); orbiter.update(); }
class MouseOrbiter { constructor(d, r, c) { this.d = d; this.r = r; this.c = c; this.v = random(-5,5) this.a = 0 } draw() { fill(this.c); ellipse(mouseX+this.d*cos(radians(this.a)),mouseY+this.d*sin(radians(this.a)),this.r*2,this.r*2); } update() { this.a+=this.v; } }
we can take this one step furter and access the variables of another object. For example, we can make an object orbiting around a moving ball. This is what is demonstrated io the following example, where we also make the ball a bit more interesting by letting it change its direction once in a while.
let ball; let orbiter; function setup() { createCanvas(400,400); noStroke(); ball = new WanderBall(random(50,width-50),random(50,height-50), 10, color(255)); orbiter = new BallOrbiter(30, 5, color(200,255,200)); } function draw() { background(0,20); ball.draw(); orbiter.draw(); ball.update(); orbiter.update(); } class WanderBall { // Initialize a Ball constructor(x, y, r, c) { this.x = x; this.y = y; this.r = r; this.c = c; this.vx = random(-5,5); this.vy = random(-5,5); } draw() { fill(this.c); stroke(red(this.c)/2,green(this.c)/2,blue(this.c)/2); ellipse(this.x,this.y,this.r*2,this.r*2); } update() { // Change direction randomly once in a while if(frameCount % 50 == 0) { this.vx = random(-3,3); this.vy = random(-3,3); } // Move in the appropriate direction by the step size this.x+=this.vx; this.y+=this.vy; // If too close to the wall (consider the radius of the ball), // move back and change direction if (this.x > width-this.r || this.x < this.r) { this.x = constrain(this.x,this.r,width-this.r); this.vx = -this.vx; } if (this.y > height-this.r || this.y < this.r) { this.y = constrain(this.y,this.r,height-this.r); this.vy = -this.vy; } } } class BallOrbiter { constructor(d, r, c) { this.d = d; this.r = r; this.c = c; this.v = random(-5,5) this.a = 0 } // this object position depends on the ball draw() { fill(this.c); let r = this.r ellipse(ball.x+this.d*cos(radians(this.a)),ball.y+this.d*sin(radians(this.a)),r*2,r*2); } update() { this.a+=this.v; } }