CS 2, Winter 2009
Programming for Interactive Digital Arts

Feb 4: Multiple objects


Arrays

Let's go back to the original random wanderer sketch, from when we first looked at state, putting on hold for now the more recent object-oriented version. We kept a couple of 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:

float x=50, y=50;

void draw()
{
  ellipse(x,y,5,5);
  x += random(-2,2);
  y += random(-2,2);
}

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.

float x=50, y=50;
float x2=25, y2=25;

void draw()
{
  ellipse(x,y,5,5);
  ellipse(x2,y2,5,5);
  x += random(-2,2);
  y += random(-2,2);
  x2 += random(-2,2);
  y2 += random(-2,2);
}

That seems kind of bogus though -- why should we have to duplicate and modify the code ourselves (and worry about maintaining the two versions), when computers should be good at that kind of thing? And what if we want 10 wanderers?

Fortunately, the array programming construct (details in the Programming notes section) makes it much easier to deal with the states of multplie 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 as x[0] and x[1] (instead of x and x2) and similarly with the y values.

float[] x = new float[2];
float[] y = new float[2];

void setup()
{
  x[0] = 50; y[0] = 50;
  x[1] = 25; y[1] = 25;
}

void draw()
{
  ellipse(x[0],y[0],5,5);
  ellipse(x[1],y[1],5,5);
  x[0] += random(-2,2);
  y[0] += random(-2,2);
  x[1] += random(-2,2);
  y[1] += random(-2,2);
}

So far this doesn't seem to be much better. But do you see the pattern? We do basically the same thing for x[0] and y[0] as we do for x[1] and y[1]. By setting up a loop, iterating over which wanderer we're dealing with, we only have to write the code once. Then we can also have as many wanderers as we like.

// How many wanderers
int num=5;
// Their positions
float[] x = new float[num];
float[] y = new float[num];

void setup()
{
  smooth();
  noStroke();
  background(0);
  for (int i=0; i<x.length; i++) {
    // Initialize the i-th wanderer
    x[i] = random(width);
    y[i] = random(height);
  }
}

void draw()
{
  fill(0,3);
  rect(0,0,width,height);
  fill(255);
  for (int i=0; i<x.length; i++) {
    // Draw the i-th wanderer
    ellipse(x[i],y[i],5,5);
    // Update the i-th wanderer
    x[i] += random(-2,2);
    y[i] += random(-2,2);
  }
}
screenshot[applet]

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.

Wanderer[] wanderers = new Wanderer[5];

void setup()
{
  smooth();
  noStroke();
  background(0);
  for (int i=0; i<wanderers.length; i++) {
    // Initialize the i-th wanderer
    wanderers[i] = new Wanderer(random(width),random(height));
  }
}

void draw()
{
  fill(0,3);
  rect(0,0,width,height);
  for (int i=0; i<wanderers.length; i++) {
    wanderers[i].draw();    // Draw the i-th wanderer
    wanderers[i].update();  // Update the i-th wanderer
  }
}
screenshot[applet]

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.

sketch3
Ball[] balls = new Ball[100];

void setup()
{
  size(400,300);
  smooth();
  noStroke();
  background(0);

  // They all start at the center 
  for (int i=0; i<balls.length; i++)
    balls[i] = new Ball(width/2, height/2); 
}

void draw()
{
  fill(0,10);
  rect(0,0,width,height);

  // Draw each, then update each
  for (int i=0; i<balls.length; i++) balls[i].draw();
  for (int i=0; i<balls.length; i++) balls[i].update();
}
Ball
class Ball {
  float x, y;    // position
  float vx, vy;  // velocity
  color c;
  float r;       // radius
  
  Ball(float x0, float y0)
  {
    x = x0; y = y0;
    vx = random(-5,5); vy = random(-5,5);
    c = color(random(255),random(255),random(255));
    r = random(2,10);
  }
  
  void draw()
  {
    fill(c);
    ellipse(x,y,2*r,2*r);
  }
  
  void update()
  {
    x += vx;
    y += vy;
    // Bounce
    if (x > width-r) { x = width-r; vx = -vx; }
    else if (x < r) { x = r; vx = -vx; }
    if (y > height-r) { y = height-r; vy = -vy; }
    else if (y < r) { y = r; vy = -vy; }
  }
}
screenshot[applet]

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

sketch4
Twinkler[] twinklers = new Twinkler[200];

void setup()
{
  size(400,400);
  background(0);
  fill(255);
  smooth();
  noStroke();
  
  for (int i=0; i<twinklers.length; i++) {
    twinklers[i] = new Twinkler(random(width), random(height));
  }
}

void draw()
{
  background(0);
  for (int i=0; i<twinklers.length; i++) {
    twinklers[i].twinkle();
  }
}
Twinkler
class Twinkler {
  float x,y;      // position
  float sz;       // maximum diameter
  float a, da;    // angle and step for sinusoidal pulsing

  Twinkler(float x0, float y0)
  {
    x = x0; y = y0;
    sz = random(10);
    a = random(TWO_PI);
    da = radians(random(0.5,10));
  }
  
  void twinkle()
  {
    // Set the diameter according to the angle
    float d = sz * (1+sin(a))/2.0;
    ellipse(x,y,d,d);
    a += da;
  }
}
screenshot[applet]

Finally, there's a cool sketch called "Amoeba Abstract" in the "Synthesis" section of the Reas & Fry code examples. There's a lot of fine-tuning in the code, so we won't go through it all, but let's take a look at some of the underlying ideas. We'll do them in an object-oriented fashion.

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.)

sketch5
int num = 12;  // how many walkers
Walker[] walkers = new Walker[num];

void setup()
{
  size(100,100);
  smooth();
  noStroke(); fill(10);
  rectMode(CENTER);
  // Divide the height equally among the walkers
  float sz = float(height) / num;
  for (int i=0; i<num; i++) {
    // start at left
    // y = i*sz: equally space from top to bottom
    // dx = 1+i/sz: faster toward bottom
    walkers[i] = new Walker(0, i*sz, 1+i/sz, sz);
  }
}

void draw()
{
  background(200);
  // Draw each, then update each
  for (int i=0; i<walkers.length; i++) walkers[i].draw();
  for (int i=0; i<walkers.length; i++) walkers[i].update();
}
Walker
class Walker {
  float x, y;
  float dx;
  float sz;
  
  Walker(float x0, float y0, float dx0, float sz0)
  {
    x = x0; y = y0; dx = dx0; sz = sz0;
  }
  
  void draw()
  {
    rect(x,y,sz,sz);
  }
  
  void update()
  {
    x += dx;;
    if (x > width) x = 0;    // wrap around
  }
}
screenshot[applet]

Given that, it's not hard to add in some color and size variations. The following is inspired by Greenberg (11-4). What else can you come up with?

sketch6
int num = 30;  // how many walkers
Walker[] walkers = new Walker[num];

void setup()
{
  size(400,400);
  smooth();
  noStroke();
  rectMode(CENTER);
  for (int i=0; i<walkers.length; i++) {
    walkers[i] = new Walker(0,random(height));
  }
}

void draw()
{
  // Draw each, then update each
  for (int i=0; i<walkers.length; i++) walkers[i].draw();
  for (int i=0; i<walkers.length; i++) walkers[i].update();
}
Walker
class Walker {
  float x, y;
  float dx, dy;
  float sz;
  color c;
  
  Walker(float x0, float y0)
  {
    x = x0; y = y0;
    dx = random(5, 10);
    dy = random(-2, 2);
    sz = random(2, 10);
    c = color(random(255),random(255),random(255));    
  }
  
  void draw()
  {
    fill(c,100);  // partially transparent
    rect(x,y,sz,sz);
  }
  
  void update()
  {
    x += dx;
    y += dy;
    if (x > width) {
      // When wrap around horizontlaly, choose new step size
      // and vertical position
      x = 0; y = random(height);
      dx = random(5, 10);
      dy = random(-2, 2);
    }
    if (y > height) y = 0;
  }
}
screenshot[applet]

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., "float[] x;". The array is created with the new command, e.g., "x = new float[3];". The size of the array must be put in square brackets after the type of the objects in the array. 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).