Skip to content

Instantly share code, notes, and snippets.

@HarryStevens
Last active June 19, 2020 16:10
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save HarryStevens/a82fc63f8bed339d64ccdcc49a635a05 to your computer and use it in GitHub Desktop.
Bad Boids
license: gpl-3.0

This is my first attempt at implementing Craig Reynolds’s boids flocking simulation. Click and drag to add boids.

If you compare this to successful implementations, such as this, this and this, you’ll see that adjusting the parameters should have a more significant affect on the behavior of the boids. I don’t know how to implement the separation parameter correctly; I don’t know what factors should affect the boids’ velocity; and I don’t understand why the boids have a tendency to move to the right. Finally, this implementation uses no spatial index, so it runs in quadratic time.

Even though it is very flawed, I thought it’d be fun to publish a notebook of this early version to track my progress. And the boids still exhibit some pretty cool emergent behavior.

<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
}
#controls {
font-family: sans-serif;
padding: 5px;
position: absolute;
text-align: center;
width: 100%;
bottom: 0px;
}
#controls .control {
background: rgba(255, 255, 255, .95);
display: inline-block;
padding: 10px;
text-align: left;
width: 200px;
}
#controls .control .range input, #controls .control .range .value {
display: inline-block;
}
#controls .control .range .value {
font-size: 12px;
margin-top: -5px;
vertical-align: middle;
}
#controls .control .description {
font-size: 12px;
}
</style>
</head>
<div id="controls">
<div class="control">
<div class="title">Alignment</div>
<div class="range">
<input data-parameter="alignment" type="range" min="0" max="1" value=".5" step=".1" />
<div class="value">0.5</div>
</div>
<div class="description">Steer towards the average heading of local flockmates</div>
</div>
<div class="control">
<div class="title">Cohesion</div>
<div class="range">
<input data-parameter="cohesion" type="range" min="0" max="1" value=".5" step=".1" />
<div class="value">0.5</div>
</div>
<div class="description">Steer to move toward the average position of local flockmates</div>
</div>
<div class="control">
<div class="title">Separation</div>
<div class="range">
<input data-parameter="separation" type="range" min="0" max="1" value=".5" step=".1" />
<div class="value">0.5</div>
</div>
<div class="description">Steer to avoid crowding local flockmates</div>
</div>
<div class="control">
<div class="title">Distance</div>
<div class="range">
<input data-parameter="distance" type="range" min="1" max="200" value="30" step="1" />
<div class="value">30</div>
</div>
<div class="description">Maximum distance of other boids to consider</div>
</div>
</div>
<div id="simulation"></div>
<body>
<script src="https://unpkg.com/geometric@2.2.3/build/geometric.min.js"></script>
<script src="https://d3js.org/d3-array.v2.min.js"></script>
<script src="https://d3js.org/d3-random.v2.min.js"></script>
<script>
class Boids {
init(opts){
this.width = opts && opts.width ? opts.width : innerWidth;
this.height = opts && opts.height ? opts.height : innerHeight;
this.center = [this.width / 2, this.height / 2];
this.data = [];
this.separation = opts && isFinite(opts.separation) ? opts.separation : .5;
this.alignment = opts && isFinite(opts.alignment) ? opts.alignment : 1;
this.cohesion = opts && isFinite(opts.cohesion) ? opts.cohesion : 1;
this.distance = opts && opts.distance ? opts.distance : 30;
return this;
}
add(datum){
const d = datum || {};
d.angle = d.angle || 0;
d.startAngle = d.angle;
d.pos = d.pos || this.center;
d.speed = d.speed || 1;
this.data.push(d);
return this;
}
tick(){
// Check if any of alignment, cohesion, or separation are greater than 0
const hasValue = this.alignment || this.cohesion || this.separation;
if (hasValue){
// Loop through the boids to find the neighborhood of each
for (let i = 0, l = this.data.length; i < l; i++){
const d = this.data[i];
d.neighborhood = [];
// Find all boids within this.distance
for (let i0 = 0, l0 = this.data.length; i0 < l0; i0++){
const d0 = this.data[i0];
if (geometric.lineLength([d.pos, d0.pos]) < this.distance) d.neighborhood.push(d0);
}
}
}
// Loop through the boids to calculate the new position
for (let i = 0, l = this.data.length; i < l; i++){
const d = this.data[i];
if (d.neighborhood.length && hasValue){
const alignment = d3.mean(d.neighborhood, d0 => d0.angle),
cohesion = geometric.lineAngle([
d.pos,
geometric.polygonMean(d.neighborhood.map(d0 => d0.pos))
]),
separation = d.startAngle;
// The new angle. Alignment needs to be boosted by some coefficient
d.angle = (cohesion * this.cohesion +
separation * this.separation +
alignment * this.alignment * 40) /
(this.cohesion + this.separation + this.alignment * 40);
}
const [x, y] = geometric.pointTranslate(d.pos, d.angle, d.speed);
d.pos = [x < 0 ? this.width : x > this.width ? 0 : x, y < 0 ? this.height : y > this.height ? 0 : y];
}
return this;
}
}
const myBoids = (_ => {
const simulation = new Boids;
simulation.init({
cohesion: .5,
alignment: .5,
separation: .5
});
// Add 500 boids
for (let i = 0; i < 500; i++){
simulation.add({
angle: d3.randomUniform(-360, 360)(),
pos: [
d3.randomUniform(0, simulation.width)(),
d3.randomUniform(0, simulation.height)()
]
});
}
return simulation;
})();
// Draw the simulation
const wrapper = document.getElementById("simulation");
const canvas = document.createElement("canvas");
canvas.width = myBoids.width;
canvas.height = myBoids.height;
wrapper.appendChild(canvas);
const context = canvas.getContext("2d");
context.strokeStyle = "red";
context.fillStyle = "pink";
function tick(){
requestAnimationFrame(tick);
context.clearRect(0, 0, myBoids.width, myBoids.height);
// The simulation.tick method advances the simulation one tick
myBoids.tick();
for (let i = 0, l = myBoids.data.length; i < l; i++){
const boid = myBoids.data[i],
a = geometric.pointTranslate(boid.pos, boid.angle - 90, 3),
b = geometric.pointTranslate(boid.pos, boid.angle, 9),
c = geometric.pointTranslate(boid.pos, boid.angle + 90, 3);
context.beginPath();
context.moveTo(...a);
context.lineTo(...b);
context.lineTo(...c);
context.lineTo(...a);
context.fill();
context.stroke();
}
}
tick();
let holding = false;
canvas.addEventListener("mousedown", e => { holding = true; addBoidOnEvent(e); });
canvas.addEventListener("mouseup", e => { holding = false });
canvas.addEventListener("mousemove", e => { if (holding) addBoidOnEvent(e); });
function addBoidOnEvent(e){
myBoids.add({
angle: d3.randomUniform(-360, 360)(),
pos: [e.pageX, e.pageY]
});
}
addEventListener("resize", _ => {
myBoids.width = innerWidth;
myBoids.height = innerHeight;
canvas.width = myBoids.width;
canvas.height = myBoids.height;
context.strokeStyle = "red";
context.fillStyle = "pink";
});
const controls = document.querySelectorAll(".control");
controls.forEach(control => {
control.addEventListener("input", _ => {
_.target.nextElementSibling.innerHTML = +_.target.value
myBoids[_.target.dataset.parameter] = +_.target.value;
});
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment