Classes & Objects in Depth
Design patterns for complex sketches — composition, shared state, entity management, and object pooling.
Beyond basic classes
The Beginner level introduced classes as “bundles of data and behaviour.” This article goes further: how do you design class hierarchies that don’t become a mess? How do you share state without global variables? How do you handle thousands of objects efficiently?
Composition over inheritance
Inheritance (extends) is tempting but often leads to rigid hierarchies. Composition — giving objects capabilities as properties — is usually more flexible.
// ❌ Deep inheritance — fragile
class MovingFlashingBall extends FlashingBall extends MovingBall extends Ball {}
// ✅ Composition — mix capabilities
class Entity {
constructor(config) {
this.pos = createVector(config.x, config.y);
this.mover = config.mover || null;
this.drawer = config.drawer || null;
this.alive = true;
}
update() {
if (this.mover) this.mover.update(this);
}
draw() {
if (this.drawer) this.drawer.draw(this);
}
}
class BounceMove {
constructor(vx, vy) {
this.vel = createVector(vx, vy);
}
update(entity) {
entity.pos.add(this.vel);
if (entity.pos.x < 0 || entity.pos.x > width) this.vel.x *= -1;
if (entity.pos.y < 0 || entity.pos.y > height) this.vel.y *= -1;
}
}
class CircleDrawer {
constructor(r, col) { this.r = r; this.col = col; }
draw(entity) {
fill(this.col);
noStroke();
circle(entity.pos.x, entity.pos.y, this.r * 2);
}
}
// Usage
let e = new Entity({
x: random(width),
y: random(height),
mover: new BounceMove(random(-3, 3), random(-3, 3)),
drawer: new CircleDrawer(15, color(100, 200, 255))
});
p5.Vector — stop doing the maths manually
Stop tracking x, y, vx, vy as separate numbers. Use p5.Vector:
class Particle {
constructor(x, y) {
this.pos = createVector(x, y);
this.vel = createVector(random(-2, 2), random(-2, 2));
this.acc = createVector(0, 0);
this.r = random(6, 16);
this.life = 1.0;
}
applyForce(force) {
this.acc.add(force);
}
update() {
this.vel.add(this.acc);
this.vel.limit(4); // max speed
this.pos.add(this.vel);
this.acc.mult(0); // reset acceleration each frame
this.life -= 0.01;
}
isDead() { return this.life <= 0; }
draw() {
fill(200, 120, 255, this.life * 255);
noStroke();
circle(this.pos.x, this.pos.y, this.r * 2);
}
}
// Apply gravity to all particles
let gravity = createVector(0, 0.1);
function draw() {
background(20);
for (let p of particles) {
p.applyForce(gravity);
p.update();
p.draw();
}
particles = particles.filter(p => !p.isDead());
}
p5.Vector methods: .add(), .sub(), .mult(), .div(), .mag(), .normalize(), .limit(), .dot(), .dist(), .copy(), .lerp().
Object pooling — performance for many objects
Creating and destroying objects constantly puts pressure on the garbage collector. A pool pre-allocates objects and reuses them:
class Pool {
constructor(Factory, size) {
this.items = Array.from({ length: size }, Factory);
this.inactive = [...this.items];
this.active = [];
}
acquire(...args) {
if (this.inactive.length === 0) return null;
let item = this.inactive.pop();
item.init(...args);
this.active.push(item);
return item;
}
update() {
for (let i = this.active.length - 1; i >= 0; i--) {
this.active[i].update();
if (this.active[i].isDead()) {
this.inactive.push(this.active.splice(i, 1)[0]);
}
}
}
draw() {
for (let item of this.active) item.draw();
}
}
Entity manager
For large sketches, a central manager that handles update/draw/removal:
class EntityManager {
constructor() {
this.entities = [];
}
add(entity) {
this.entities.push(entity);
return entity;
}
update() {
for (let e of this.entities) e.update();
this.entities = this.entities.filter(e => e.alive);
}
draw() {
for (let e of this.entities) e.draw();
}
count() { return this.entities.length; }
getByType(Type) {
return this.entities.filter(e => e instanceof Type);
}
}
let manager = new EntityManager();
function setup() {
createCanvas(700, 500);
for (let i = 0; i < 50; i++) {
manager.add(new Particle(random(width), random(height)));
}
}
function draw() {
background(20);
manager.update();
manager.draw();
}
Spatial indexing — don’t check every pair
Checking all N² pairs for collisions is slow. A simple grid-based spatial index cuts it to roughly O(N):
class SpatialGrid {
constructor(cellSize) {
this.cell = cellSize;
this.grid = new Map();
}
key(x, y) {
return `${Math.floor(x / this.cell)},${Math.floor(y / this.cell)}`;
}
insert(entity) {
let k = this.key(entity.pos.x, entity.pos.y);
if (!this.grid.has(k)) this.grid.set(k, []);
this.grid.get(k).push(entity);
}
nearby(entity, range) {
let results = [];
let cells = Math.ceil(range / this.cell);
let gx = Math.floor(entity.pos.x / this.cell);
let gy = Math.floor(entity.pos.y / this.cell);
for (let dx = -cells; dx <= cells; dx++) {
for (let dy = -cells; dy <= cells; dy++) {
let k = `${gx + dx},${gy + dy}`;
if (this.grid.has(k)) results.push(...this.grid.get(k));
}
}
return results;
}
clear() { this.grid.clear(); }
}
Key takeaways
- Prefer composition over deep inheritance — give objects capability objects as properties
p5.Vectorreplaces manual x/y/vx/vy arithmetic with clean vector math- Object pooling eliminates garbage-collector pressure in high-frequency spawn/destroy cycles
- An EntityManager centralises update/draw loops and handles removal
- Spatial grids reduce O(N²) nearest-neighbour checks to near-O(N)