Classes & Objects in Depth
Master object-oriented programming in Processing with inheritance, interfaces, polymorphism, and factory patterns to build a multi-type 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
extendsandsuper()to share code across related types without duplication. @Overrideprotects you from silent method signature mismatches.- Interfaces define contracts that let unrelated code collaborate without tight coupling.
- Iterate
ArrayListbackwards when removing elements during iteration. - The factory pattern isolates construction logic; object pooling eliminates GC pressure in tight loops.