Processing Intermediate Article 1

Classes & Objects in Depth

Master object-oriented programming in Processing with inheritance, interfaces, polymorphism, and factory patterns to build a multi-type particle system.

⏱ 18 min oop inheritance interfaces particle-system

Processing runs on Java, and Java is a fully object-oriented language. Once you move past basic sketches you will start reaching for classes, inheritance, and design patterns to keep your code manageable. This tutorial walks through every OOP concept you need, then ties it all together with a particle system that has three distinct particle sub-types.

Inheritance: Extending a Base Class

The extends keyword lets one class inherit all fields and methods from a parent.

class Particle {
  PVector pos;
  PVector vel;
  float lifespan;

  Particle(float x, float y) {
    pos = new PVector(x, y);
    vel = PVector.random2D().mult(random(1, 3));
    lifespan = 255;
  }

  void update() {
    vel.mult(0.98);          // friction
    pos.add(vel);
    lifespan -= 4;
  }

  void display() {
    fill(200, lifespan);
    noStroke();
    ellipse(pos.x, pos.y, 8, 8);
  }

  boolean isDead() {
    return lifespan <= 0;
  }
}

Now create a Spark subclass that overrides display() and adds gravity:

class Spark extends Particle {
  color col;

  Spark(float x, float y, color c) {
    super(x, y);                        // must be the first line
    col = c;
    vel = PVector.random2D().mult(random(2, 6));
    lifespan = 200;
  }

  @Override
  void update() {
    vel.add(new PVector(0, 0.15));      // gravity
    super.update();                      // call parent friction + position
  }

  @Override
  void display() {
    stroke(col, lifespan);
    strokeWeight(2);
    float len = vel.mag() * 2;
    PVector tail = PVector.sub(pos, vel.copy().normalize().mult(len));
    line(pos.x, pos.y, tail.x, tail.y);
  }
}

The super(x, y) call passes arguments up to the parent constructor. super.update() lets you reuse the parent logic while adding your own on top. The @Override annotation is optional in Processing but excellent practice — the compiler will warn you if the method signature does not actually match anything in the parent.

Interfaces

An interface defines a contract without providing any implementation. Every class that claims to implement the interface must provide all listed methods.

interface Drawable {
  void draw();
  boolean isFinished();
}

interface Emittable {
  ArrayList<Particle> emit(float x, float y);
}

Interfaces decouple your rendering loop from specific types. A renderer only needs to know that something is Drawable; it does not care whether it is a Spark, a Smoke, or a Bubble.

class Smoke extends Particle implements Drawable {
  Smoke(float x, float y) {
    super(x, y);
    vel = new PVector(random(-0.5, 0.5), random(-2, -0.5));
    lifespan = 180;
  }

  @Override
  void display() { draw(); }  // bridge Particle.display() to Drawable.draw()

  public void draw() {
    noStroke();
    float sz = map(lifespan, 180, 0, 4, 40);
    fill(180, 160, 140, lifespan * 0.4);
    ellipse(pos.x, pos.y, sz, sz);
  }

  public boolean isFinished() { return isDead(); }
}

ArrayList Management

Keep particles in ArrayList<Particle> and iterate backwards when removing dead ones to avoid index-shift bugs:

ArrayList<Particle> particles = new ArrayList<Particle>();

void updateAndCull() {
  for (int i = particles.size() - 1; i >= 0; i--) {
    Particle p = particles.get(i);
    p.update();
    p.display();
    if (p.isDead()) {
      particles.remove(i);
    }
  }
}

Factory Pattern

A factory method centralises object creation. When your emitter needs a specific mix of types, the factory decides:

class ParticleFactory {
  static Particle create(String type, float x, float y) {
    switch (type) {
      case "spark":  return new Spark(x, y, color(random(200,255), random(100,200), 0));
      case "smoke":  return new Smoke(x, y);
      case "bubble": return new Bubble(x, y);
      default:       return new Particle(x, y);
    }
  }
}

Emitters call ParticleFactory.create(...) without needing to know anything about the constructors. Adding a new particle type later only requires adding a case to the factory, not hunting through emitter code.

Object Pooling

Garbage collection in Java can cause brief frame-rate hiccups during intensive simulations. An object pool pre-allocates a fixed collection of objects and recycles them rather than allocating new ones:

class ParticlePool {
  ArrayList<Spark> pool = new ArrayList<Spark>();

  ParticlePool(int size) {
    for (int i = 0; i < size; i++) {
      pool.add(new Spark(-9999, -9999, color(255)));
    }
  }

  Spark acquire(float x, float y, color c) {
    for (Spark s : pool) {
      if (s.isDead()) {
        s.pos.set(x, y);
        s.vel = PVector.random2D().mult(random(2, 6));
        s.lifespan = 200;
        s.col = c;
        return s;
      }
    }
    return null;  // pool exhausted
  }
}

Pre-populate the pool at setup() time. Because you never call new during the draw loop, the GC stays quiet.

Full Example: Multi-Type Particle System

ArrayList<Particle> particles;
ParticlePool pool;

void setup() {
  size(800, 600);
  colorMode(RGB, 255);
  particles = new ArrayList<Particle>();
  pool = new ParticlePool(300);
}

void draw() {
  background(15, 15, 25);

  if (frameCount % 3 == 0) {
    emitBurst(mouseX, mouseY);
  }

  for (int i = particles.size() - 1; i >= 0; i--) {
    Particle p = particles.get(i);
    p.update();
    p.display();
    if (p.isDead()) particles.remove(i);
  }

  fill(255);
  noStroke();
  text("Particles: " + particles.size(), 10, 20);
}

void emitBurst(float x, float y) {
  // Sparks from pool
  for (int i = 0; i < 6; i++) {
    color c = color(random(220, 255), random(80, 160), 0);
    Spark s = pool.acquire(x, y, c);
    if (s != null) particles.add(s);
  }
  // Smoke from factory
  for (int i = 0; i < 2; i++) {
    particles.add(ParticleFactory.create("smoke", x, y));
  }
  // Bubbles
  if (frameCount % 10 == 0) {
    particles.add(ParticleFactory.create("bubble", x, y));
  }
}

The Bubble class (not listed above for brevity) follows the same pattern as Spark — it extends Particle, overrides update() to add upward buoyancy, and overrides display() to draw a translucent circle with a specular highlight. The key insight is that the draw loop treats every particle uniformly through the Particle base type, while each subclass provides its own specialised behaviour through polymorphism.

Key Takeaways

  • Use extends and super() to share code across related types without duplication.
  • @Override protects you from silent method signature mismatches.
  • Interfaces define contracts that let unrelated code collaborate without tight coupling.
  • Iterate ArrayList backwards when removing elements during iteration.
  • The factory pattern isolates construction logic; object pooling eliminates GC pressure in tight loops.