Created
January 17, 2022 06:02
-
-
Save frankandrobot/e93be4b3af781f779a26be096201d348 to your computer and use it in GitHub Desktop.
Flocking - An implementation of Daniel Shiffman's Boids program to simulate the flocking behavior of birds. Each boid steers itself based on rules of avoidance, alignment, and coherence. https://processing.org/examples/flocking.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import "@pixi/math-extras"; | |
import { Point } from "pixi.js"; | |
/** | |
* | |
* Limit the magnitude of the point to the given max. | |
*/ | |
function limit(p: Point, max: number): Point { | |
const x = p.x; | |
const y = p.y; | |
const l = p.magnitude(); | |
if (l > max) { | |
const a = Math.atan2(y, x); | |
return new Point(Math.cos(a) * max, Math.sin(a) * max); | |
} | |
return new Point(p.x, p.y); | |
} | |
/** | |
* Returns the distance between two points. | |
*/ | |
function distance(p: Point, q: Point) { | |
return p.subtract(q).magnitude(); | |
} | |
interface BoidConfig { | |
width: number; | |
height: number; | |
maxSpeed: number; | |
maxForce: number; | |
separationWeight: number; | |
alignmentWeight: number; | |
cohesionWeight: number; | |
} | |
export class Boid { | |
public get position() { | |
return new Point(this._position.x, this._position.y); | |
} | |
private _position: Point; | |
private velocity: Point; | |
private acceleration: Point; | |
private renderCallback: (p: Point) => void; | |
private config: BoidConfig; | |
private get r() { | |
return 1.0; | |
} | |
constructor(opts: { | |
x: number; | |
y: number; | |
render: (p: Point) => void; | |
config: Partial<BoidConfig> & | |
Required<Pick<BoidConfig, "width" | "height">>; | |
}) { | |
this.acceleration = new Point(0, 0); | |
const angle = Math.random() * (2 * Math.PI); | |
this.velocity = new Point(Math.cos(angle), Math.sin(angle)); | |
this._position = new Point(opts.x, opts.y); | |
this.renderCallback = opts.render; | |
this.config = { | |
width: opts.config.width, | |
height: opts.config.height, | |
maxSpeed: opts.config.maxSpeed ?? 4, | |
maxForce: opts.config.maxForce ?? 0.03, | |
separationWeight: opts.config.separationWeight ?? 1.5, | |
alignmentWeight: opts.config.alignmentWeight ?? 1, | |
cohesionWeight: opts.config.cohesionWeight ?? 1, | |
}; | |
} | |
run(boids: Boid[]) { | |
this.flock(boids); | |
this.update(); | |
this.borders(); | |
this.renderCallback(this._position); | |
} | |
private applyForce(force: Point) { | |
// We could add mass here if we want A = F / M | |
this.acceleration = this.acceleration.add(force); | |
} | |
/** | |
* We accumulate a new acceleration each time based on three rules | |
*/ | |
private flock(boids: Boid[]) { | |
let sep = this.separate(boids); // Separation | |
let ali = this.align(boids); // Alignment | |
let coh = this.cohesion(boids); // Cohesion | |
// Arbitrarily weight these forces | |
sep = sep.multiplyScalar(this.config.separationWeight); | |
ali = ali.multiplyScalar(this.config.alignmentWeight); | |
coh = coh.multiplyScalar(this.config.cohesionWeight); | |
// Add the force vectors to acceleration | |
this.applyForce(sep); | |
this.applyForce(ali); | |
this.applyForce(coh); | |
} | |
/** | |
* Method to update position | |
*/ | |
private update() { | |
// Update velocity | |
this.velocity = this.velocity.add(this.acceleration); | |
// Limit speed | |
this.velocity = limit(this.velocity, this.config.maxSpeed); | |
this._position = this._position.add(this.velocity); | |
// Reset acceleration to 0 each cycle | |
this.acceleration.multiplyScalar(0); | |
} | |
/** | |
* A method that calculates and applies a steering force towards a target | |
* | |
* STEER = DESIRED MINUS VELOCITY | |
*/ | |
private seek(target: Point) { | |
const desired = target | |
.subtract(this._position) // A vector pointing from the position to the target | |
// Scale to maximum speed | |
.normalize() | |
.multiplyScalar(this.config.maxSpeed); | |
// Steering = Desired minus Velocity | |
const steer = desired.subtract(this.velocity); | |
return limit(steer, this.config.maxForce); | |
} | |
/** | |
* Wraparound | |
*/ | |
private borders() { | |
if (this._position.x < -this.r) | |
this._position.x = this.config.width + this.r; | |
if (this._position.y < -this.r) | |
this._position.y = this.config.height + this.r; | |
if (this._position.x > this.config.width + this.r) | |
this._position.x = -this.r; | |
if (this._position.y > this.config.height + this.r) | |
this._position.y = -this.r; | |
} | |
/* | |
* Separation Method checks for nearby boids and steers away | |
*/ | |
private separate(boids: Boid[]) { | |
const desiredSeparation = 25.0; | |
let steer = new Point(0, 0); | |
let count = 0; | |
// For every boid in the system, check if it's too close | |
for (const other of boids) { | |
const d = distance(this._position, other._position); | |
// If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself) | |
if (d > 0 && d < desiredSeparation) { | |
const diff = this._position | |
.subtract(other._position) | |
// Calculate vector pointing away from neighbor | |
.normalize() | |
.multiplyScalar(1 / d); // Weight by distance | |
steer = steer.add(diff); | |
count++; // Keep track of how many | |
} | |
} | |
// Average -- divide by how many | |
if (count > 0) { | |
steer = steer.multiplyScalar(1 / count); | |
} | |
// As long as the vector is greater than 0 | |
if (steer.magnitude() > 0) { | |
// First two lines of code below could be condensed with newPoint setMag() method | |
// Not using this method until Processing.js catches up | |
// steer.setMag(maxspeed); | |
// Implement Reynolds: Steering = Desired - Velocity | |
steer = steer | |
.normalize() | |
.multiplyScalar(this.config.maxSpeed) | |
.subtract(this.velocity); | |
steer = limit(steer, this.config.maxForce); | |
} | |
return steer; | |
} | |
/** | |
* Alignment - For every nearby boid in the system, calculate the average | |
* velocity | |
*/ | |
private align(boids: Boid[]) { | |
const neighborDist = 50; | |
let sum = new Point(0, 0); | |
let count = 0; | |
for (const other of boids) { | |
const d = distance(this._position, other._position); | |
if (d > 0 && d < neighborDist) { | |
sum = sum.add(other.velocity); | |
count++; | |
} | |
} | |
if (count > 0) { | |
sum = sum | |
.multiplyScalar(1 / count) | |
// Implement Reynolds: Steering = Desired - Velocity | |
.normalize() | |
.multiplyScalar(this.config.maxSpeed); | |
const steer = sum.subtract(this.velocity); | |
return limit(steer, this.config.maxForce); | |
} else { | |
return new Point(0, 0); | |
} | |
} | |
/** | |
* Cohesion - For the average position (i.e. center) of all nearby boids, | |
* calculate steering vector towards that position | |
*/ | |
private cohesion(boids: Boid[]) { | |
const neighborDist = 50; | |
let sum = new Point(0, 0); // Start with empty vector to accumulate all positions | |
let count = 0; | |
for (const other of boids) { | |
const d = distance(this._position, other._position); | |
if (d > 0 && d < neighborDist) { | |
sum = sum.add(other._position); // Add position | |
count++; | |
} | |
} | |
if (count > 0) { | |
sum = sum.multiplyScalar(1 / count); | |
return this.seek(sum); // Steer towards the position | |
} else { | |
return new Point(0, 0); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Boid } from "./Boid"; | |
export class Flock { | |
private boids: Boid[] = []; // An ArrayList for all the boids | |
get positions() { | |
return this.boids.map((boid) => boid.position); | |
} | |
run() { | |
for (const b of this.boids) { | |
b.run(this.boids); // Passing the entire list of boids to each boid individually | |
} | |
} | |
addBoid(b: Boid) { | |
this.boids.push(b); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Point } from "pixi.js"; | |
import { Boid } from "./Boid"; | |
import { Flock } from "./Flock"; | |
export const flocking = (opts: { | |
count: number; | |
initialize: (initialPositions: Point[]) => void; | |
preRender: () => void; | |
render: (p: Point) => void; | |
config: { | |
width: number; | |
height: number; | |
/** | |
* The maximum speed of the boids. | |
* | |
* Defaults to 4. | |
*/ | |
maxSpeed?: number; | |
/** | |
* The separation, alignment, cohesions forces are all limited by maxForce * | |
* their respective weights. | |
* | |
* Defaults to 0.03. | |
*/ | |
maxForce?: number; | |
/** | |
* If two boids are too close, a separation force will steer them away from | |
* each other. This force will be multiplied by this weight. | |
* | |
* Defaults to 1.5. | |
*/ | |
separationWeight?: number; | |
/** | |
* For the average velocity of all nearby boids, an alignment force will | |
* nudge all nearby boids towards this average velocity. This alignment | |
* force will be multiplied by this weight. | |
* | |
* Defaults to 1. | |
*/ | |
alignmentWeight?: number; | |
/** | |
* For the average position of all nearby boids, a cohesion force will move | |
* all nearby boids towards that position. This cohesion force will be | |
* multiplied by this weight. | |
* | |
* Defaults to 1. | |
*/ | |
cohesionWeight?: number; | |
}; | |
}) => { | |
const flock = new Flock(); | |
// Add an initial set of boids into the system | |
for (let i = 0; i < opts.count; i++) { | |
flock.addBoid( | |
new Boid({ | |
...opts, | |
x: opts.config.width / 2, | |
y: opts.config.height / 2, | |
}) | |
); | |
} | |
opts.initialize(flock.positions); | |
return { | |
draw() { | |
opts.preRender(); | |
flock.run(); | |
}, | |
// Add a new boid into the System | |
mousePressed(mouseX: number, mouseY: number) { | |
flock.addBoid( | |
new Boid({ | |
...opts, | |
x: mouseX, | |
y: mouseY, | |
}) | |
); | |
}, | |
}; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const container = new Container(); | |
container.width = app.screen.width; | |
container.height = app.screen.height; | |
const flockGraphics = new Graphics(); | |
container.addChild(flockGraphics); | |
const drawPoint = (graphics: Graphics, pos: Point) => { | |
graphics.lineStyle(0); | |
graphics.beginFill(rgbToHex("#cdcdcd"), 1); | |
graphics.drawCircle(pos.x, pos.y, 2); | |
graphics.endFill(); | |
}; | |
const flock = flocking({ | |
count: 500, | |
config: { | |
width: app.screen.width, | |
height: app.screen.height, | |
separationWeight: 2, | |
}, | |
initialize(initialPositions: Point[]) { | |
initialPositions.forEach((pos) => { | |
drawPoint(flockGraphics, pos); | |
}); | |
drawPoint(flockGraphics, new Point(0, 0)); | |
app.stage.addChild(container); | |
}, | |
preRender() { | |
flockGraphics.clear(); | |
}, | |
render(p: Point) { | |
drawPoint(flockGraphics, p); | |
}, | |
}); | |
app.ticker.add(flock.draw); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment