Skip to content

Instantly share code, notes, and snippets.

@frankandrobot
Created January 17, 2022 06:02
Show Gist options
  • Save frankandrobot/e93be4b3af781f779a26be096201d348 to your computer and use it in GitHub Desktop.
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
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);
}
}
}
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);
}
}
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,
})
);
},
};
};
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