Programming for Interactive Digital Arts
CS 2, Fall 2023

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.

[sketch2]
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).

[sketch3]
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.

[sketch5]
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;
  }
}

Let us now make a little Pong-like game. I'll let you work that out for fun.

Programming notes

Multiple files
Processing allows us to store separate parts of a sketch in separate files, which show up as tabs. Create and modify tabs/files using the button on the far right, above the scroll bar (PDE reference). It's not required to put classes in separate tabs, but I think it's good style, from a modularity standpoint.
Class definition
A class definition has the form
class Name {
  // constructor definition
  constructor(parameters)    // "constructor" function
  {
    initialization of class fieds
  }

  method definitions  // similar syntax to function definitions, no "function" keyword
}
There can actually be no constructor, or multiple ones as long as they have different types of parameters. The standard rules apply in naming the class; it's traditional to begin the name with an uppercase letter.
Constructor definition / object creation
The constructor is defined just like a function, but no return type is given (or you can think of the return type as being the class). An object is created with the new function. Here the form is obj = new Name(parameters) where Name is the name of the class, and the parameters match those of the constructor. Thus the constructor is used to initialize the fields of a new object. The body of the constructor is like that of any method (see below).
Field definition / access
A field is defined just like a variable declaration (e.g., "let x;"). An object has a value for each such field (just like a person has a name, an address, etc.). Outside the class definition, a field is accessed with a "dot", e.g., obj.x refers to the x field of the obj object. A field can be treated like any other variable -- used in calculations, assigned to, etc. Object-oriented programming theory says that in general we shouldn't be accessing a field outside the class (but should instead have methods to handle any use of the object's state), but we're not purists here :).
Method definition / invocation
A method works much like a function; the key difference is that it "belongs" to an object. Thus we call it with a "dot" and then it can access any of the object's fields without a "dot". For example, we would call a method as "obj.doSomething()", and inside the definition of doSomething(), we could refer to "x" instead of "obj.x".