CS 2, Winter 2008
Programming for Interactive Digital Arts

Jan 30: 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). Here's the "random wanderer" sketch from the State lecture restructured that way. We define a new class, called Wanderer; objects of this class will have two fields, storing the current x and y coordinates. When we create a Wanderer object (new), we give the initial coordinates to the constructor method. 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.

sketch1
Wanderer wanderer = new Wanderer(50,50);  // Create a Wanderer at the center

void setup()
{
  smooth();
  noStroke();
  background(0);
}

void draw()
{
  fill(0,3);
  rect(0,0,width,height);
  fill(255);
  ellipse(wanderer.x,wanderer.y,5,5);
  // Move the position by random steps in x and y
  wanderer.x = wanderer.x+random(-2,2);
  wanderer.y = wanderer.y+random(-2,2);
}
Wanderer
class Wanderer {
  float x, y;    // the position of the Wanderer

  // Initialize a Wanderer to be at the given coordinates
  Wanderer(float x0, float y0)
  {
    x = x0; 
    y = y0;
  }
}
screenshot[applet]

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 "wanderer", which is of type "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. We don't have to do that for float and int, because Processing already knows. We do for Wanderer, because it's a new type of our own creation.

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 (that would be even more helpful in the pulsar sketch, where we have a bunch of aspects of an object's state). 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 (called "methods"), in the class definitions. 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
Wanderer wanderer = new Wanderer(50,50);  // Create a Wanderer at the center

void setup()
{
  smooth();
  noStroke();
  background(0);
}

void draw()
{
  fill(0,3);
  rect(0,0,width,height);
  wanderer.draw();
  wanderer.update();
}
Wanderer
class Wanderer {
  float x, y;    // the position of the Wanderer
  
  // Initialize a Wanderer to be at the given coordinates
  Wanderer(float x0, float y0)
  {
    x = x0; y = y0;
  }
  
  // Draw a Wanderer, wherever it happens to be now
  void draw()
  {
    fill(255);
    ellipse(x,y,5,5);
  }

  // Update the state of a Wanderer  
  void update()
  {
    x+=random(-2,2);
    y+=random(-2,2);
  }
}
screenshot[applet]

Here we separate out the draw() method and the update() method, 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. We could extend this sketch to keep other aspects of the state (colors, sizes, etc.) in the fields, and update them in the update() method. We can keep arrays of objects, much like we keep arrays of floats, and will do so later.

Bouncing

Okay, now that we've got the state of our moving object nicely packaged up, let's start considering how it should move. We've already seen several examples where we update the current x and y coordinates by steps in those directions (rather than wandering aimlessly). Let's put those in our new framework, and add a little twist: bouncing off a wall.

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

  ball = new Ball(width/2,height/2, 5, 5); 
}

void draw()
{
  fill(0,10);
  rect(0,0,width,height);
  ball.draw();
  ball.update();
}
Ball
class Ball {
  float x, y;          // position
  float r;             // radius
  // the following also provide initial values for the fields,
  // equivalent to putting that in the constructor
  float s = 5;         // speed, intialized to 5
  int dx = 1, dy = 1;  // direction, initialized to diagonal

  // Initialize a Ball at position (x0,y0), with speed s0 and radius r0
  Ball(float x0, float y0, float s0, float r0)
  {
    x = x0; 
    y = y0;
    s = s0; 
    r = r0;
  }

  void draw()
  {
    fill(255);
    ellipse(x,y,r*2,r*2);
  }

  void update()
  {
    // Move in the appropriate direction by the step size
    x+=s*dx;
    y+=s*dy;
    // If too close to the wall (consider the radius of the ball),
    // move back and change direction
    if (x > width-r) { 
      x = width-r; 
      dx = -1; 
    }
    else if (x < r) { 
      x = r; 
      dx = 1; 
    }
    if (y > height-r) { 
      y = height-r; 
      dy = -1; 
    }
    else if (y < r) { 
      y = r; 
      dy = 1; 
    }
  }
}
screenshot[applet]

When the ball touches one of the boundaries of the window (the distance from the center to the wall is the radius of the ball), it reverses the appropriate direction. The ball object is created in the setup() function this time, and placed at the center. Note that the initial dx and dy values are predetermined at their declaration, rather than being passed to the constructor. That's a convenient way to handle default values.

We can take this one step further, and check where the ball bounces off the wall. If it's close to a "paddle", we update the color of the ball accordingly. Pong Art!

sketch4
PaddleBall ball;
int paddleR=20, wallR=5;  // size of the paddle and wall
color paddleColor=color(random(255),random(255),random(255));

void setup()
{
  size(400,300);
  smooth();
  background(0);
  strokeCap(SQUARE); // so paddles look right
  ball = new PaddleBall(width/2,height/2, 5, 5);
}

void draw()
{
  // wall
  noFill(); stroke(0); 
  rect(0,0,width,height); 
 
  // paddles
  strokeWeight(wallR*2); stroke(paddleColor);
  line(mouseX-paddleR,0,mouseX+paddleR,0);
  line(mouseX-paddleR,height,mouseX+paddleR,height);
  line(width,mouseY-paddleR,width,mouseY+paddleR);
  line(0,mouseY-paddleR,0,mouseY+paddleR);

  ball.draw();
  ball.update();
}

void keyPressed()
{
  if (key=='c')  // random color
    paddleColor = color(random(255),random(255),random(255));
  else if (key=='Z') // bigger
    ball.r++;
  else if (key=='z') {
    if (ball.r > 1) ball.r--;
  }
  else if (key=='S') // faster
    ball.s++;
  else if (key=='s') {
    if (ball.s > 1) ball.s--;
  }
}
PaddleBall
class PaddleBall {
  float x, y;          // position
  float s;             // speed
  int dx = 1, dy = 1;  // direction, defaulting to diagonal
  float r;             // radius
  color c = color(255,255,255);  // color, starting black
  float a = 255;       // transparency, starting opaque
  
  // Initialize at position (x0,y0), with speed s0 and radius r0
  PaddleBall(float x0, float y0, float s0, float r0)
  {
    x = x0; y = y0;
    s = s0; r = r0;
  }
  
  void draw()
  {
    fill(c,a); noStroke();
    ellipse(x,y,r*2,r*2);
  }
  
  void update()
  {
    if (a>100) a--;    // fade away until hit paddle again
    x+=s*dx;
    y+=s*dy;
    // Have to account for thickness of wall when checking for bounce
    if (x > width-wallR-r) {
      x = width-wallR-r; dx = -1;
      // See if bounced off paddle
      if (abs(y-mouseY) < paddleR) paddle(); 
    }
    else if (x < r+wallR) {
      x = r+wallR; dx = 1; 
      if (abs(y-mouseY) < paddleR) paddle(); 
    }
    else if (y > height-wallR-r) {
      y = height-wallR-r; dy = -1; 
      if (abs(x-mouseX) < paddleR) paddle(); 
    }
    else if (y < r+wallR) { 
      y = r+wallR; dy = 1; 
      if (abs(x-mouseX) < paddleR) paddle(); 
    }
  }
  
  // When hit paddle, update the color, and go back to opaque
  void paddle()
  {
    c = paddleColor;
    a = 255;
  }
}
screenshot[applet]

Or we can combine the wall-bouncing idea with the orbiting-ellipse idea, keeping extra fields for the orbit. Packaging these fields up into an object helps make clear how they all fit together. Note that it's our decision as to what parameters to put in the constructor for initializing the various fields, and what to just give default values to when declaring the fields.

sketch5
Orbiter ball;

void setup()
{
  size(400,300);
  smooth();
  noStroke();
  background(0);
  
  ball = new Orbiter(width/2,height/2); 
}

void draw()
{
  fill(0,10);
  rect(0,0,width,height);
  ball.draw();
  ball.update();
}

void keyPressed()
{
  if (key=='a') { // orbit angle step size
    if (ball.da > 5) ball.da-=5;
  }
  else if (key=='A') ball.da+=5;
  else if (key=='o') { // orbit size
    if (ball.o > 5) ball.o-=5;
  }
  else if (key=='O') ball.o+=5;
}
Orbiter
class Orbiter {
  float cx, cy;        // position of moving center
  float x, y;          // actual position of ball (revolving around center)
  float s=5;           // speed
  int dx = 1, dy = 1;  // direction
  float r=5;           // radius of ball
  float o = 20;        // radius of orbit
  float a = 0;         // orbit angle
  int da = 25;         // angle step (degrees)

  // New orbiter with center at (cx0, cy0)
  Orbiter(float cx0, float cy0)
  {
    cx = cx0; cy = cy0;
  }
  
  void draw()
  {
    fill(255); noStroke();
    ellipse(x,y,r*2,r*2);
  }
  
  void update()
  {
    // Move the center and the orbit angle
    cx += 5*dx;
    cy += 5*dy;
    a += da;
    // Compute the actual position of the orbiting object,
    // according to the center and angle
    x = cx+o*cos(radians(a));
    y = cy+o*sin(radians(a));
    // Check for bounce
    if (x > width-r || x < r) { 
      dx = -dx; da = -da;
    }
    if (y > height-r || y < r) {
      dy = -dy; da = -da;
    }
  }
}
screenshot[applet]

Finally, we can have spiralling motion by having the update method increase both the current angle and the current radius. By changing dr and da, we can control the tightness of the spiral.

sketch6
Pinwheel pinwheel;

void setup()
{
  size(400,400);
  smooth();
  background(0);
  noStroke();
  pinwheel = new Pinwheel(width/2,height/2);
}

void draw()
{
  fill(0,5); 
  rect(0,0,width,height); 
 
  pinwheel.draw();
  pinwheel.update();
}

void mousePressed()
{
  pinwheel.moveTo(mouseX, mouseY);
}
Pinwheel
class Pinwheel {
  float cx, cy;          // position
  float r = 0;           // radius
  float a = 0;           // angle
  float dr = 1, da = radians(57);  // steps
  
  Pinwheel(float cx0, float cy0)
  {
    cx = cx0; cy = cy0;
  }
  
  void draw()
  {
    fill(255); noStroke();
    ellipse(cx+r*cos(a),cy+r*sin(a),20,20);
  }
  
  void update()
  {
    // Expand both the radius and the angle
    r += dr;
    a += da;
    // When too big or too small, switch direction
    if (r>width/2 || r>height/2 || r<=0) {
      dr = -dr;
      da = -da;
    }
  }
  
  // Change the center of the pinwheel
  void moveTo(float cx0, float cy0)
  {
    cx = cx0; cy = cy0;
    r = 0; a = 0;
    dr = abs(dr); da = abs(da);
  }
}
screenshot[applet]

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 {
  field declarations  // same syntax as variable declarations

  // constructor definition
  Name(parameters)    // same syntax as function definition except no return type
  {
    initialization of fieds
  }

  method definitions  // same syntax as function definitions
}
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, as we saw with arrays. 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., "float 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".
Extending
I probably won't use this, but you'll find in the book and in various on-line examples cases where one class "extends" another. That means the "subclass" has all the stuff from the "superclass", along with some new stuff, and perhaps replacing some of the old stuff. The subclass can be used wherever the superclass can.

Practice problems

  1. Give the Wanderer a color. First just give a constant color; as a further extension, have it change over time. [hints]
  2. Modify the Ball class so that it doesn't bounce off the walls. [hints]
  3. Define a new class Pulsar, which moves around the screen like a Ball, except that its radius also expands and shrinks as it moves. [hints]