Goals

  • Composition and Data
    • Objects in Objects
    • Bookkeeping
    • Example
  • Recursive Objects (sort of)
    • Constructor Setup
    • Seeding Randomly
    • Sampling Area

Resources

Dynamic Programming
Recursive Object Pattern (paper)

"No object is so beautiful that, under certain conditions, it will not look ugly." ~ Oscar Wilde

Objects in Objects

Every time we make an object it has some fields. But what if those fields are other objects. Then there is a much more complex relationship that must be maintained in your code.

Take for example the car class in the image to the left. Two relationships are shown: aggregation and composition.

Aggregation: the Tyre (or Tire...) has an "is part of" relationship with the Car. Since a Tyre will still be a Tyre if not used specifically as a field in a Car class.

Composition: the Carburetor on the other hand has an "uses a" relationship with the Car. Since a Carburetor is essentially useless without a Car to use it, we say that a Car is "composed of" the class Carburetor.

The types and numbers of objects stored within other objects may change, these are basic relationships to think about when designing your classes.

Bookkeeping

It's important to keep all objects up to date. Including those stored inside another object.

A typical scenario is updating a list inside an object. Here's some code from an object that stores and updates a list of positions:

pieces.add(0, new PVector( xPos, yPos, atan2(yVel, xVel) ) );
 1
2
3
4
5
6
7
8
9
10
 
//add the latest position
pieces.add(0, new PVector( xPos, yPos, atan2(yVel, xVel) ) );
//remove old position if ribbon is too long
if (pieces.size() > maxLength) {
   pieces.remove( pieces.size() - 1 );
}
//update all positions by adding some gravity
for (int i = 0; i < pieces.size(); i++) {
  pieces.get(i).add( grav );
}
Composition means Object A stores types of Object B and Object B is essentially useless without Object A.
Aggregation means Object A stores types of Object B but Object B can be used on it's own.
Bookkeeping methods update the information inside an object including lists containing other objects.
      vertex(p.x + (newHalfWidth * cos(p.z - PI/2)), p.y + (newHalfWidth * sin(p.z - PI/2)));
  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
 
ArrayList<Ball> ballList = new ArrayList<Ball>();
void setup() {
  size(600, 600);
  smooth();
  stroke(0, 128);
  strokeWeight(2);
  fill(255, 0, 0, 128);
  for (int i = 0; i < 16; i++) {
    ballList.add(new Ribbon());
  }
}
void draw() {
  background(255);
  for (int i = 0; i < ballList.size(); i++) {
    Ball curBall = ballList.get(i);
    curBall.update();
    curBall.checkWalls();
    curBall.drawMe();
  }
}
class Ball {
  float mass, xPos, yPos, xVel, yVel;
  color c;
  Ball() {
    xPos = random(16, width - 16);
    yPos = random(16, height - 16);
    xVel = random(-2, 2);
    yVel = random(-2, 2);
    mass = random(1, 8);
    c = color(random(128, 255), random(128), 0);
  }
  void update() {
    //add an attraction to the mouse position
    xVel += (mouseX - xPos)/width * 1/mass;
    yVel += (mouseY - yPos)/height * 1/mass;
    //dampen the velocity
    xVel *= 0.99;
    yVel *= 0.99;
    //update position
    xPos += xVel;
    yPos += yVel;
  }
  void checkWalls() {
    if (abs(xPos - width/2) > width/2 - 16) {
      xVel *= -1;
    }
    if (abs(yPos - height/2) > height/2 - 16) {
      yVel *= -1;
    }
  }
  void drawMe() {
    pushMatrix();
    translate(xPos, yPos);
    scale(mass);
    ellipse(0, 0, 4, 4);
    popMatrix();
  }
}
class Ribbon extends Ball {
  ArrayList<PVector> ribbonPieces = new ArrayList<PVector>();
  PVector ribbonGrav;
  int ribbonLength, halfRibbonWidth;
  Ribbon() {
   super();
   ribbonGrav = new PVector(0, 0.8, 0);
   ribbonLength = 16;
   halfRibbonWidth = (int)mass * 4;
   for (int i = 0; i < ribbonLength; i++) {
     ribbonPieces.add( new PVector() );
   }
  }
  void update() {
    //add previous position to beginning of arraylist
    //also put previous angle into z coordinate, we need it for drawing
    ribbonPieces.add(0, new PVector( xPos, yPos, atan2(yVel, xVel) ) );
    //remove old position if ribbon is too long
    if (ribbonPieces.size() > ribbonLength) {
       ribbonPieces.remove( ribbonPieces.size() - 1 );
    }
    for (int i = 0; i < ribbonPieces.size(); i++) {
      ribbonPieces.get(i).add( ribbonGrav );
    }
    //do regular position update
    super.update();
  }
  void drawMe() {
    //declare all local variables before looping (increases speed)
    float newHalfWidth;
    int ribbonSize = ribbonPieces.size();
    int halfRibbonSize = ribbonSize/2;
    PVector p;
    int i;
 
    /* each loop will draw a quarter of the ribbon:
     * fan out on the first side
     * fan in on the first side
     * fan out on the second side
     * fan in on the second side
     */
    noStroke();
    fill(c, 128);
    beginShape();
    for (i = 0; i < halfRibbonSize; i++) {
      p = ribbonPieces.get(i);
      newHalfWidth = (halfRibbonWidth * (float)i/ribbonSize);
      vertex(p.x + (newHalfWidth * cos(p.z - PI/2)), p.y + (newHalfWidth * sin(p.z - PI/2)));
    }
    for (i = halfRibbonSize; i < ribbonSize; i++) {
      p = ribbonPieces.get(i);
      newHalfWidth = halfRibbonWidth - (halfRibbonWidth * (float)i/ribbonSize);
      vertex(p.x + (newHalfWidth * cos(p.z - PI/2)), p.y + (newHalfWidth * sin(p.z - PI/2)));
    }
    for (i = ribbonSize-1; i > halfRibbonSize; i--) {
      p = ribbonPieces.get(i);
      newHalfWidth = halfRibbonWidth - (halfRibbonWidth * (float)i/ribbonSize);
      vertex(p.x + (newHalfWidth * cos(p.z + PI/2)), p.y + (newHalfWidth * sin(p.z + PI/2)));
    }
    for (i = halfRibbonSize; i >= 0; i--) {
      p = ribbonPieces.get(i);
      newHalfWidth = (halfRibbonWidth * (float)i/ribbonSize);
      vertex(p.x + (newHalfWidth * cos(p.z + PI/2)), p.y + (newHalfWidth * sin(p.z + PI/2)));
    }
    endShape(CLOSE);
  }
}

Recursive Objects - Constructor Setup

Take a look at the following recursive method call:
drawBranch(x, y, px, py, angle - PI/4 * xMouse, step);
Notice that the drawBranch method call has a number of arguments.
Every recursive method has at least 1 parameter. Recursive objects are no different.

We must set up the constructor of an object with parameters that can define a new object in relation to an old one.

Take a look at this constructor:
Segment(float x, float y, float angle, float life) { ... }
When we call new Segment( ... ) we can provide arguments for the location, angle, and life of this new Segment object.
This means that previous Segment objects can now seed new Segment objects with the appropriate values.

Seeding Randomly

It's been talked about before, but making random decisions is a powerful concept:

The basic setup is this:
if ( random(0, 32) < 1) { ... }
Since the random(0, 32) method call returns a float from 0 to 31.99... We have approximately a 1 in 32 chance that the random number is less than 1, or between 0 and 0.99...

Using this logic, we can decide to seed our recursive objects randomly, and they can also seed objects randomly, and so on.

Why not create new recursive objects all the time? Because we would create so many objects so fast, that it would neither look good nor would it run properly, crashing our program due to too many objects.

Sampling Area

Somewhat off topic. Say we have an object drawing onto the screen, where other objects are also drawing.
Perhaps we don't want this object to "run over" the other objects already drawn. But there's no information there, only the pixels that have been colored. What we need to do is sample the pixels and make sure they haven't been drawn yet.

The basic idea is this:

        if ( red(pixels[checkX + checkY * width]) < 1) {
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
boolean found = false;
loadPixels();
for (int i = -1; i < 1; i++) {
  for (int j = -1; j < 1; j++) {
    int checkX = (int)x + i;
    int checkY = (int)y + j;
    if (checkX != (int)px && checkY != (int)py) {
      try {
        if ( red(pixels[checkX + checkY * width]) < 1) {
          found = true; 
          break;
        }//endif
      } 
      catch (Exception e) {
        println("I'm outside and I don't care!");
      } //normally this would throw an error
    }
  }//for j
  if (found) break;
}//for i    
if (found) { /* do something with your object */ }
Every recursive definition requires at least 1 parameter.
Seeding must be done randomly and not so fast that too many objects are created.
    segs.add( new SmartCurveSegment(mouseX, mouseY, random(TWO_PI), 255) );
  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
 
int step = 2;
ArrayList<Segment> segs = new ArrayList();
void setup() {
  size(600, 600); 
  smooth();
  background(255);
}
void keyPressed() {
  switch (key) {
  case ' ': 
    background(255);
    segs.clear();
    break;
  case 's': 
    save("frame" + frameCount + ".png");
    break;
  case '1':
    segs.add( new Segment(mouseX, mouseY, random(TWO_PI), 255) );
    break;
  case '2':
    segs.add( new CurveSegment(mouseX, mouseY, random(TWO_PI), 255) );
    break;
  case '3':
    segs.add( new SmartSegment(mouseX, mouseY, random(TWO_PI), 255) );
    break;
  case '4':
    segs.add( new SmartCurveSegment(mouseX, mouseY, random(TWO_PI), 255) );
    break;
  }
}
void draw() {
  for (int i = 0; i < segs.size(); i++) {
    Segment seg = segs.get(i);
    seg.update();
    seg.walls();
    seg.seed();
    seg.drawMe();
  }
}
 
class Segment {
  float x, y, px, py, angle, life;
  Segment(float x, float y, float angle, float life) {
    this.px = this.x = x; 
    this.py = this.y = y; 
    this.angle = angle; 
    this.life = life;
  }
  void update() {
    px = x; 
    py = y;
    x += step * cos(angle);
    y += step * sin(angle);
    life--;
    if (life < 0) {
      segs.remove(this);
    }
  }
  void walls() {
    if (x > width || x < 0 || y > height || y < 0) segs.remove(this);
  }
  void seed() {
    if (random(0, 16) < 1)
      if (random(0, 2) < 1) 
        segs.add( new Segment(x, y, angle + PI/2, life/2) );
      else
        segs.add( new Segment(x, y, angle + PI/2, life/2) );
    //endif
  }
  void drawMe() {
    line(x, y, px, py);
  }
}
 
class CurveSegment extends Segment {
  float angleInc;
  int angleDir;
  CurveSegment(float x, float y, float angle, float life) {
    super(x, y, angle, life);
    angleInc = 0.04;
    angleDir = 1;
    if (random(0, 2) < 1) 
      angleDir *= -1;
    super.update();
  }
  void update() {
    super.update();
    if (random(0, 16) < 1) {
      angleDir *= -1;
    }
    angle += angleDir * angleInc;
  }
  void seed() {
    if (random(0, 16) < 1)
      if (random(0, 2) < 1) 
        segs.add( new CurveSegment(x, y, angle + PI/2, life/2) );
      else
        segs.add( new CurveSegment(x, y, angle + PI/2, life/2) );
    //endif
  }
}
 
class SmartSegment extends Segment {
  SmartSegment(float x, float y, float angle, float life) {
    super(x, y, angle, life);
    super.update();
  }
  void update() {
    super.update();
    checkArea();
  }
  void checkArea() {
    boolean found = false;
    loadPixels();
    for (int i = -1; i < 1; i++) {
      for (int j = -1; j < 1; j++) {
        int checkX = (int)x + i;
        int checkY = (int)y + j;
        if (checkX != (int)px && checkY != (int)py) {
          try {
            if ( brightness(pixels[checkX + checkY * width]) < 128) {
              found = true; 
              break; //end the inner loop
            }//endif
          } 
          catch (Exception e) {
            println("I'm outside and I don't care!");
          } //normally this would throw an error
        }
      }//for j
      if (found) break; //end the outer loop
    }//for i
    if (found) segs.remove(this);
  }//checkArea
  void seed() {
    if (random(0, 16) < 1)
      if (random(0, 2) < 1) 
        segs.add( new SmartSegment(x, y, angle + PI/2, life/2) );
      else
        segs.add( new SmartSegment(x, y, angle + PI/2, life/2) );
    //endif
  }
}
 
class SmartCurveSegment extends SmartSegment {
  float angleInc;
  int angleDir;
  SmartCurveSegment(float x, float y, float angle, float life) {
    super(x, y, angle, life);
    angleInc = 0.04;
    angleDir = 1;
    if (random(0, 2) < 1) 
      angleDir *= -1;
  }
  void update() {
    super.update();
    if (random(0, 16) < 1) {
      angleDir *= -1;
    }
    angle += angleDir * angleInc;
    checkArea();
  }
  void seed() {
    if (random(0, 16) < 1)
      if (random(0, 2) < 1) 
        segs.add( new SmartCurveSegment(x, y, angle + PI/2, life/2) );
      else
        segs.add( new SmartCurveSegment(x, y, angle + PI/2, life/2) );
    //endif
  }
}