This is the boids page
The aim of this workshop is to recreate boids, which are a simulation of birds and other flocking entities (like fishes). Hopefully, you'll be able to apply this in your own projects! Along the way, we'll learn a little bit about p5.js, rendering, and simulating physics.
(This is not an actual boids sketch btw - this is a random stepper)We will be coding in JavaScript but don't worry about knowing the ins and outs of the language; you should be able to pick it up as we go!
To get started, create an account for the web editor.
Open a new sketch (File->New)
A sketch is a small program that will work with p5.js to produce a simulation. Just like a game, your simulation will have frames (an image of your simulation) and p5.js will aim to show 60 frames a second. You'll be able to code what happens just before a frame is shown, and also code what happens just before the simulation starts!
Your new sketch should have 2 functions:
function setup() {
createCanvas(400, 400);
}
function draw() {
let redV = 128; // if you're new to js, use "let" to declare variables
let greenV = 12;
let blueV = 128;
background(redV, greenV, blueV); // my favourite colour; purple!
}
If you're unfamiliar with RGB values, go here
This difference matters when it comes to sketches with changing elements; see the difference in the following sketches, where we draw a circle at the mouse's position every frame. One draws a background once, and the other draws a background every frame.
function setup() {
createCanvas(400, 400);
background(222, 222, 222); // called just once!
}
const circleSize = 10; // js also has constants!! The value of variable "circleSize" cannot be changed
function draw() {
circle(mouseX, mouseY, circleSize);
}
Note the use of mouseX and mouseY which are variables updated every frame!
function setup() {
createCanvas(400, 400);
}
let circleSize = 10;
function draw() {
background(222, 222, 222); // background drawn every frame!
circle(mouseX, mouseY, circleSize);
}
stroke(255, 0, 0); // outlines are now red!
fill(0, 255, 0); // areas are now green!
Part 1: Modify the
Part 2: Modify the
To store our boids, we're going to use arrays and OOP. We'll have a look at how to do this in JavaScript using a modified version of our Circles 2 sketch, to create fixed-length trails (with circles of changing size) like below:
A trail with a length of 20 circles, where the circles grow to a max size then resetTo create a fixed-length trail, we can't just not refresh the background every frame; this will lead to circles from before 20-circles-ago staying! We'll have to save the previous 20 circles, and then draw them every frame. To do this, we'll need:
Let's get the growing circle part out of way; this part isn't important, but the rest of the code makes less sense without it:
function setup() {
createCanvas(400, 400);
}
const maxCircleSize = 15;
const minCircleSize = 5;
let circleSize = 5;
function draw() {
background(222, 222, 222);
// update circleSize
circleSize++;
if(circleSize > maxCircleSize){
circleSize = minCircleSize;
}
// draw circle
circle(mouseX, mouseY, circleSize);
}
Could also use modulo to loop the circleSize, but the if statement is more readable here
Don't worry about how this is performed; it's more important that you understand how objects, classes, and arrays work in JavaScript.
Now let's create a class for the circle!
class trailCircle {
constructor(position, size){
this.position = position;
this.size = size;
}
}
Every class has a constructor which is the function that runs whenever an object is created; we can define the constructor for a class as above. Note the use of this inside the constructor! It's a variable for the object being created.
You might be wondering what position will be, since it's a single variable to represent both x and y. Fortunately, p5.js provides us with the Vector class which allows us to bundle x and y into an object, and gives us a lot of methods to manipulate them as vectors! Have a look at the methods on the handy p5 reference. We can use the createVector() method to make one!
Now we can create an instance of trailCircle like so:
let tc1 = new trailCircle(createVector(100,100), 100);
Now we can draw the object to the screen by writing:
circle(tc1.position.x, tc1.position.y, tc1.size);
It'd be nice to put this drawing stuff into one place though, so that if we wanted to change how circles are rendered, we only need to change it in one place. We might also introduce other shapes like squares, which may have a different way of rendering its shape! Let's add a draw method to our class.
class trailCircle {
constructor(position, size){
this.position = position;
this.size = size;
}
draw(){
circle(this.position.x, this.position.y, this.size);
}
}
We can declare an array for the trail and a trail length pretty easily:
const trails = [];
const trailLength = 20;
Note const here doesn't mean an uneditable array; you can insert and remove from the array, but you can't reassign the trails variable to something else. It will forever be that array, but you can edit the array as you wish.
Now let's create a method updateTrail to add the current circle to the trail; remember that we have circleSize from the growing circle part of the sketch.
Our method will need to:
I'll arbitrarily decide that the circles will be added to the end of the array, so therefore the oldest circles will be in the front of the array. Here's one implementation of the specification above:
function updateTrail(){
if(trails.length == trailLength){ // should remove one
trails.shift(); // this deletes the first element, and shifts the rest down
}
// create trail object to insert
let trailObj = new trailObject(createVector(mouseX, mouseY), circleSize);
trails.push(trailObj);
}
This might be a little confusing because of the use of shift() and push(); they're just JavaScript methods to make our lives easier. You can see JavaScript's array methods here!
We do need to call updateTrail() in draw; let's call it just after we draw the current circle.
function draw() {
background(222, 222, 222);
// update circleSize
circleSize++;
if(circleSize > maxCircleSize){
circleSize = minCircleSize;
}
// draw circle
circle(mouseX, mouseY, circleSize);
// update stuff
updateTrail();
}
Our updated draw method
Okay great, now we just need to display the trail!
All we have to do is loop through our array of trail circles and display each one. I want to draw the oldest first, so I'll iterate from the start to the end of the array.
function drawTrail(){
for(let i = 0; i < trails.length; i++){
let trailObj = trails[i];
// draw the circle!
trailObj.draw();
}
}
And now we'll call drawTrail() before the current circle (so we don't draw the trail over the current circle!)
function draw() {
background(222, 222, 222); // background drawn every frame!
// update circleSize
circleSize++;
if(circleSize > maxCircleSize){
circleSize = minCircleSize;
}
// draw stuff
drawTrail();
circle(mouseX, mouseY, circleSize);
// update stuff
updateTrail();
}
Our final version of draw()!
You can have a look at the full program here
With the help of the Growing Trails code, modify your solution to the task Multicolour to create rainbow trails.
You should keep the colour changing code, and store the previous 10 or so circles! Your simulation should look something like this:
Tip: Once you've finished your sketch, sometimes it's a little hard to see whether the trail circles have the colour and position that they should. You can verify this more easily by slowing down your simulation! Use frameRate(10) in setup to set your sketch to run at 10 frames per second.
To make our boids move, we're going to use forces. We know from physics that $F=ma$, but we're going to simplify things by ignoring mass and jump straight to changing acceleration!
Let's try modelling a simple force; a constant acceleration to the right.
First, let's establish a class for this ball to make things clear to ourselves.
class Ball {
constructor(){
this.position = createVector(0,0);
this.velocity = createVector(0,0);
this.acceleration = createVector(0,0);
}
draw(){
circle(this.position.x, this.position.y, 10);
}
}
This is all stuff we've basically done, but we also need a method to simulate all the physics. From physics, we know that acceleration is the rate of change of velocity, and that velocity is the rate of change of position.
So to simulate our ball's motion, we would:
As we'll soon see, we have to do this in a very careful way.
Until now, we've not really considered how long each frame takes. It'd be reasonable to assume each frame takes about 1/60 seconds to process however each frame takes a slightly different amount of time and, when simulations get more complex, each frame might take a non-slight different amount of time!
delta time docsSimple individual steering behaviours
Key things to mention:
With room for more behaviours!
Link to an extension site with scope for learning how to host your own