Programming for Interactive Digital Arts
CS 2, Fall 2023

12. Multiple objects

Arrays

Let us consider the original random wanderer sketch, from when we first looked at state, putting on hold for now the more recent object-oriented version. We used two variables, x and y, to keep track of where the wanderer was, and then each frame we drew an ellipse at the current (x,y) and updated them by random amounts. The key parts of the code are as follows:

var x=200, y=200;

function setup()
{
  createCanvas(400,400);
}

function draw()
{
  background(0);
  ellipse(x,y,20,20);
  x += random(-10,10);
  y += random(-10,10);
}

Now suppose we wanted to have two wanderers. We could define variables x2 and y2, and duplicate and slightly modify the code to update and use them the same way.

var x=200, y=200;
var x2=200, y2=200;

function setup()
{
  createCanvas(400,400);
}

function draw()
{
  ellipse(x,y,20,20);
  ellipse(x2,y2,20,20);
  x += random(-10,10);
  y += random(-10,10);
  x2 += random(-10,10);
  y2 += random(-10,10);
}

ooking at the previous code it is obvious that there is a major issue. Every single time we have to add more wanderers, we have to copy and mpdify the code by hand. Things become pretty wild if we try say 10 wanderers.

Fortunately, the Array programming construct (details in the Programming notes section) makes it much easier to deal with the states of multiple things. Rather than declaring a single x variable and a single y variable, we declare (and create, via new) two of each. We then access the x values using the array access ([]) operator as x[0] and x[1] (instead of x and x2) and similarly with the y values.

var x = new Array(2);
var y = new Array(2);

function setup()
{
  createCanvas(400,400);
  x[0] = 200; y[0] = 200;
  x[1] = 200; y[1] = 200;
}

function draw()
{
  background(0);
  ellipse(x[0],y[0],20,20);
  ellipse(x[1],y[1],20,20);
  x[0] += random(-10,10);
  y[0] += random(-10,10);
  x[1] += random(-10,10);
  y[1] += random(-10,10);
}

The benefit of this is that we "packed" all similar values together into arrays of things. But the rest of the code did not really get better. Looking carefully at the code though, you should note that we basically do the same thing on all elements of the arrays. This is where loops help. By setting up a loop, iterating over which wanderer we're dealing with, we only have to write the code once. Even better, we can have any number of wanderers.

[sketch1]
// How many wanderers
var num=5;
// Their positions
var x = new Array(num);
var y = new Array(num);

function setup()
{
  createCanvas(400,400);
  smooth();
  noStroke();
  background(0);
  
  // loop to initialize all the coordinates of the wanderers
  for (var i=0; i<x.length; i++) {
    // Initialize the i-th wanderer
    x[i] = width/2;
    y[i] = height/2;
  }
}

function draw()
{
  fill(0,3); noStroke();
  rect(0,0,width,height);
  fill(255); stroke(128);
  
  // loop to draw and update each wanderer
  for (var i=0; i<x.length; i++) {
    // Draw the i-th wanderer
    ellipse(x[i],y[i],20,20);
    // Update the i-th wanderer
    x[i] += random(-10,10);
    y[i] += random(-10,10);
  }
}

Note that we made the loop range up to (but not including) x.length. The length field tells us how many elements there are in the array (here 5). Five elements are numbered 0, 1, 2, 3, and 4, so we want the loop variable to start at 0 and go through 4. If you try to access past the end of the array (using more elements than you asked for), Processing will give you an "array index out of bounds" exception.

One other nice example of arrays is the Processing example Basics | Input | StoringInput, which Shiffman also covers in example 9-8. The idea here is that the very last element has the current mouse position, the one before that the previous position, and so forth. Thus for each frame, we move the position for index i to index i-1. But before we do that, we better move i-1 to i-2, etc. The loop handles that. Note that the loop starts from index 1, moving its information to 0; the info for 0 just disappears. The size of the ellipse depends on the index -- larger indices (more recent positions) have larger ellipses.

Arrays of objects

Since x[i] and y[i] go together to establish the position of the i-th wanderer, it makes sense to keep a single array of Wanderer objects, rather than two separate arrays of their coordinates. Here's a rewriting of the above sketch to use the Wanderer class from the earlier lecture.

[sketch2]
var num=5;
var wanderers = new Array(num);

function setup()
{
  createCanvas(400,400);
  smooth();
  noStroke();
  background(0);
  for (var i=0; i<wanderers.length; i++) {
    // Initialize the i-th wanderer
    wanderers[i] = new Wanderer(random(width),random(height),
      10,color(random(200,255),random(200,255),random(200,255)));
  }
}

function draw()
{
  fill(0,3);
  rect(0,0,width,height);
  for (var i=0; i<wanderers.length; i++) {
    wanderers[i].draw();    // Draw the i-th wanderer
    wanderers[i].update();  // Update the i-th wanderer
  }
}
// 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);
    ellipse(this.x,this.y,this.r*2,this.r*2);
  }

  // Update the state of a Wanderer  
  update(){
    var r = this.r
    this.x+=random(-r,r);
    this.y+=random(-r,r);
  }
}

The main difference is that in the initialization, we have to be sure to construct a new object at each position in the array (more discussion in the Programming notes section).

Arrays, loops, and objects work together very nicely, to make it (relatively) easy to have lots of dynamic things moving around. The following sketch, inspired by Greenberg (11-6), has a bunch of bouncing Balls, each with a different random velocity, color, and size. Now it's especially nice to keep all the state together in an object, rather than having several parallel arrays.

// Inspired by Greenberg (11-6)
var balls = new Array(25);

function setup()
{
  createCanvas(400,400);
  noStroke();
  background(0);

  // They all start at the center 
  for (var i=0; i<balls.length; i++)
    balls[i] = new PhysBall(
      random(20,width-20), random(20,height-20), // random position
      random(-5,5), random(-5,5), // random velocity
      10,// radius
      color(random(200,255),random(200,255),random(200,255))); 
}

function draw()
{
  fill(0,25); noStroke();
  rect(0,0,width,height);

  // Draw each, then update each
  for (var i=0; i<balls.length; i++) balls[i].draw();
  for (var i=0; i<balls.length; i++) balls[i].update();
}
class PhysBall {
    x=0; y=0;          // position
    vx=0; vy=0;    // velocity in the two directions   
    r=10;          // radius
    c=255; // color
    gravity=0.1;   // the amount of acceleration
    drag=0.99;     // multiplicative factor for velocity
    frict=0.75;   // multiplicative factor, only when bounce
  
  grabbed=false; // whether I have been grabbed by the mouse
  
  // Initialize a Ball at position (x0,y0) abd velocity (vx0, vy0)
  constructor(x, y, vx, vy, r, c)
  {
    this.x = x; 
    this.y = y;
    this.vx = vx;
    this.vy = vy;
    this.r = r;
    this.c = c;
  }

  draw(){
    fill(this.c);
    ellipse(this.x,this.y,this.r*2,this.r*2);
    if(this.grabbed) {
      fill(red(this.c)/2,green(this.c)/2,blue(this.c)/2);
      ellipse(this.x,this.y,this.r,this.r);
    }
  }

  updateVelocity() {
    // Accelerate according to gravity
    this.vy += this.gravity;
    // Now damp according to drag
    this.vx *= this.drag;
    this.vy *= this.drag;
  }
  
  bounce() {
    if (this.x > width-this.r || this.x < this.r) { 
      this.x = constrain(this.x,this.r,width-this.r); 
      this.vx = -this.vx; 
      this.vy *= this.frict;   // damp
    }
    if (this.y > height-this.r || this.y < this.r) { 
      this.y = constrain(this.y,this.r,height-this.r); 
      this.vy = -this.vy; 
      this.vy *= this.frict;   // damp
    }
  }
  
  updateGrabbed() {
    // if I am not grabbed yet, grab me if the mouse is clicked on me 
    if(!this.grabbed) this.grabbed = mouseIsPressed && dist(mouseX,mouseY,this.x,this.y) < this.r;
    // otherwise stop grabbing me when I release the mouse
    else this.grabbed = mouseIsPressed;
  }

  update(){
    // check if grabbed
    this.updateGrabbed();
    
    // follow the mouse if grabbed
    if(this.grabbed) {
      this.x = mouseX;
      this.y = mouseY;
      this.vx = mouseX - pmouseX;
      this.vy = mouseY-pmouseY;
    } else { // obey physics otherwise
      // update the velocity
      this.updateVelocity();
    
      // Move in the appropriate direction by the step size
      this.x += this.vx;
      this.y += this.vy;
    
      // bounce
      this.bounce();
    }
  }
}

As another example, let's have a number of sinusoidally twinkling objects, each oscillating according to its own period and own maximum size.

var twinklers = new Array(200);

function setup()
{
  createCanvas(400,400);
  background(0);
  fill(255);
  noStroke();
  
  for (var i=0; i<twinklers.length; i++) {
    twinklers[i] = new Twinkler();
  }
}

function draw()
{
  background(0);
  for (var i=0; i<twinklers.length; i++) {
    twinklers[i].update();
    twinklers[i].draw();
  }
}
class Twinkler {

  constructor()
  {
    this. x = random(width); this.y = random(height);
    this.sz = random(5,20);
    this.a = random(TWO_PI);
    this.da = radians(random(2,10));
    this.c = color(random(200,255),random(200,255),random(200,255));
  }
  
  update() {
    this.a += this.da;
  }
  
  draw(){
    // Set the diameter according to the angle
    var d = this.sz * (1+sin(this.a))/2.0;
    fill(this.c);
    stroke(red(this.c)/2,green(this.c)/2,blue(this.c)/2);
    ellipse(this.x, this.y, d, d);
  }
}

There are many other variations of things you can do. For example, there's a cool sketch called "Amoeba Abstract" in the "Synthesis" section of the Reas & Fry code examples. One central idea is having things march across the screen. Here's a modification of a sketch by Reas and Fry (33-13), to do just that. (Shiffman example 9-9 follows a similar approach.) Also look at Greenberg (11-4).

Programming notes

Array declaration and creation
An array stores a set of variables of the same type. The array is declared by putting square brackets after the type, e.g., "var x = new Array(3);". The size of the array must be put in brackets as an argument to the Array constructor. The size has to be fixed ahead of time; another approach is required in order to have the size vary during the course of the sketch. If you forget to "new" the array, you'll get an error.
When an array is created, its elements are all "empty". If they're ints or floats, they're 0; if they're booleans, they're false; etc. If they're objects, they haven't been created yet, and we need to loop over the array, creating an object for each element. Thus we call new first to create the array (e.g., new Wanderer[5] creates an array to hold 5 Wanderers) and then within a loop to create the elements (e.g., wanderers[i] = new Wanderer(...) to create the i-th one).
Array indexing
To access an element in an array, we put square brackets and the element index after the array name, e.g., x[3]. This can then be used exactly like any other variable — we use its value (e.g., ellipse(x[3], ...)) and update it (e.g., x[3] = x[3]+1, or x[3]++). Indexing starts at 0; thus x[0] is the first element, x[1] the second, and so forth.
Array length
So that we don't have to keep an extra variable for an array's length, we can ask it, using the field, e.g., myArray.length (no parentheses -- it's a field rather than a method). Since the first index in an array is 0, the largest index is is length-1. That's why it's common to use a for loop of the form "for (int i=0; i<myArray.length; i++)" to consider each element in the array, from the zeroth up to the (length-1)st (due to the < test).
This
this refers to the particular object whose method (or constructor) has been called. For example, if we call "ball.update()", then in the body of the update method, "this" is the same as "ball". This is useful, for example, when we want to distinguish a field of an object from another variable (or parameter) with the same name or we want to compare ourself to other objects.