Main content
Computer programming
Course: Computer programming > Unit 5
Lesson 8: Particle Systems- Intro to particle systems
- A single particle
- Challenge: Falling leaves
- A particle system
- Challenge: Fish bubbles
- Systems of particle systems
- Challenge: Fire starter
- Particle types
- Challenge: Magical cauldron
- Particle systems with forces
- Challenge: River rocks
- Project: Creature Colonies
© 2023 Khan AcademyTerms of usePrivacy PolicyCookie Notice
Particle systems with forces
So far in this section, we’ve been focusing on structuring our code in an object-oriented way to manage a collection of particles. Maybe you noticed, or maybe you didn’t, but during this process we unwittingly took a couple steps backward from where we were in previous sections. Let’s examine the constructor of our simple
Particle
object:var Particle = function(position) {
this.acceleration = new PVector(0, 0.05);
this.velocity = new PVector(random(-1, 1), random(-1, 0));
this.position = new PVector(position.x, position.y);
this.timeToLive = 255.0;
};
And now let’s look at the
update()
method:Particle.prototype.update = function(){
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
this.timeToLive -= 2;
};
Notice that our acceleration is constant, it's never set beyond the constructor. A much better framework would be to follow Newton’s second law (F, with, vector, on top, equals, M, A, with, vector, on top) and incorporate the force accumulation algorithm we worked so hard on in the Forces section.
The first step is to add in an
applyForce()
method. (Remember, we need to make a copy of the PVector
before we divide it by mass.)Particle.prototype.applyForce = function(force) {
var f = force.get();
f.div(this.mass);
this.acceleration.add(f);
};
Once we have this, we can add in one more line of code to clear the acceleration at the end of
update()
.Particle.prototype.update = function() {
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
this.acceleration.mult(0);
this.timeToLive -= 2.0;
};
And thus, we have a
Particle
object that can have force applied to it. Now, where should we call the applyForce()
function? Where in the code is it appropriate to apply a force to a particle? The truth of the matter is that there’s no right or wrong answer; it really depends on the exact functionality and goals of a particular program. Still, we can create a generic situation that would likely apply to most cases and craft a model for applying forces to individual particles in a system.Adding wind
Let’s consider the following goal: Apply a force globally every time through draw() to all particles. We’ll start with applying a simple wind-like force that pushes the particles to the right:
var wind = new PVector(0.4, 0);
We said it should always be applied, i.e. in
draw()
, so let’s take a look at our draw()
function as it stands.draw = function() {
background(168, 255, 156);
particleSystem.addParticle();
particleSystem.run();
};
Well, it seems that we have a small problem.
applyForce()
is a method written inside the Particle
object, but we don’t have any reference to the individual particles themselves, only the ParticleSystem
object: the variable particleSystem
.Since we want all particles to receive the force, however, we can decide to apply the force to the particle system and let it manage applying the force to all the individual particles.
draw = function() {
background(168, 255, 156);
particleSystem.applyForce(wind);
particleSystem.addParticle();
particleSystem.run();
};
Of course, if we call a new function on the
ParticleSystem
object in draw()
, well, we have to write that function in the ParticleSystem
object. Let’s describe the job that function needs to perform: receive a force as a PVector
and apply that force to all the particles.Now in code:
ParticleSystem.prototype.applyForce = function(f){
for(var i = 0; i < this.particles.length; i++){
this.particles[i].applyForce(f);
}
};
It almost seems silly to write this function. What we’re saying is “apply a force to a particle system so that the system can apply that force to all of the individual particles.” Nevertheless, it’s really quite reasonable. After all, the
ParticleSystem
object is in charge of managing the particles, so if we want to talk to the particles, we’ve got to talk to them through their manager.Here it is, all together. Play around with the wind force and see how it affects the particle movement, and notice how particles of different mass respond differently. Think about why that is.
Adding gravity
Now let's apply a more complex force, gravity, which is different from wind because it varies based on the mass of the objects its applied to.
Let's recall the equation for calculating the force of gravity between two masses: F, start subscript, g, end subscript, with, vector, on top, equals, start fraction, G, m, start subscript, 1, end subscript, m, start subscript, 2, end subscript, divided by, vertical bar, vertical bar, r, vertical bar, vertical bar, squared, end fraction, r, with, hat, on top
Remember that when we're modeling the force of gravity on earth, the force exerted by the earth overwhelms all other gravitational forces, so the only equation we're dealing with is to compute the force of gravity between the earth and the object.G and m, start subscript, 1, end subscript are the same for every particle, and r(the radius from the earth) is basically the same (since earth's radius is so large compared to how little the particles move away from it), so we typically simplify those as simply g, the constant for gravity on earth:
Now, the force of gravity is just some constant g, times the mass of the particles, multiplied by a unit vector in the direction of the force (which will always be down):
In code, that means that we will need to apply a different gravity force to each particle based on its mass. How can we do that? We can't re-use the existing
applyForce
function, because it expects the same force for each particle. We might consider passing a parameter to it that instructs applyForce
to multiply by the mass, but let's leave that function alone and create a new function, applyGravity
, which calculates force based on a global constant vector:// A constant down vector, declared at the top
var gravity = new PVector(0, 0.2);
ParticleSystem.prototype.applyGravity = function() {
for(var i = 0; i < this.particles.length; i++) {
var particleG = gravity.get();
particleG.mult(this.particles[i].mass);
this.particles[i].applyForce(particleG);
}
};
Now, if we've done this correctly, all of our particles should fall at the same rate in the simulation below. That's because the force of gravity is based on multiplying the mass, but the acceleration is based on dividing by the mass, so in the end, the mass doesn't have an effect. It might seem silly to go through that much effort to not have an effect, but its important once we start combining multiple different forces.
Adding repellers
What if we wanted to take this example one step farther and add a repeller object that pushes particles away as they get close? It would be similar to the attractor object we created earlier, only pushing in the opposite direction. Once again, like gravity, we must calculate a different force for each particle, but in the case of the repeller, the difference is that the calculation isn't based on mass, it's based on distance. For gravity, all of our force vectors had the same direction, but for the repeller, all the force vectors will have different directions:
Because the calculation of a repeller force is a little more complex than the calculation of gravity (and we might ultimately want multiple repellers!), we will solve this problem by incorporating a new
Repeller
object into our simple particle system plus gravity example. We’re going to need two major additions to our code:- A
Repeller
object (declared, initialized, and displayed). - A function that passes the
Repeller
object into theParticleSystem
so that it can apply a force to each particle object.
var particleSystem = new ParticleSystem(new PVector(width/2, 50));
var repeller = new Repeller(width/2-20, height/2);
var gravity = new PVector(0, 0.1);
draw = function() {
background(214, 255, 171);
// Apply gravity force to all Particles
particleSystem.applyForce(gravity);
particleSystem.applyRepeller(repeller);
repeller.display();
particleSystem.addParticle();
particleSystem.run();
};
Making a displayable
Repeller
object is easy; it’s a duplicate of the Attractor
object that we created earlier:var Repeller = function(x, y) {
this.position = new PVector(x, y);
};
Repeller.prototype.display = function() {
stroke(255);
strokeWeight(2);
fill(127);
ellipse(this.position.x, this.position.y, 32, 32);
};
The more difficult question is, how do we write the
applyRepeller()
method? Instead of passing a PVector
into a function like we do with applyForce()
, we’re going to instead pass a Repeller
object into applyRepeller()
and ask that function to do the work of calculating the force between the repeller and all particles:ParticleSystem.prototype.applyRepeller = function(r) {
for(var i = 0; i < this.particles.length; i++){
var p = this.particles[i];
var force = r.calculateRepelForce(p);
p.applyForce(force);
}
};
The big difference here is that a new force is calculated for each particle, because, as we saw before, the force is different depending on properties of each particle in relation to the repeller. We calculate that force using the
calculateRepelForce
function, which is the inverse of the calculateAttractionForce
function from our Attractor
s.Repeller.prototype.calculateRepelForce = function(p) {
// Calculate vector for force between objects
var dir = PVector.sub(this.position, p.position);
// Calculate distance between objects
var dist = dir.mag();
// Keep distance within a reasonable range
dist = constrain(dist, 1, 100);
// Calculate repelling force,
// inversely proportional to distance squared
var force = -1 * this.power/ (dist * dist);
// Normalize direction vector
// (discarding distance information)
dir.normalize();
// Calculate force vector: direction * magnitude
dir.mult(force);
return dir;
};
Notice how throughout this entire process of adding a repeller to the environment, we’ve never once considered editing the
Particle
object itself. A particle doesn’t actually have to know anything about the details of its environment; it simply needs to manage its location, velocity, and acceleration, as well as have the ability to receive an external force and act on it.So we can now look at this example in its entirety. Try changing the power of the forces acting on the particles - the gravity and the repeller - and see how that changes them:
Want to join the conversation?
- One thing I noticed about the repeller is that it only repels once the "liquid" particles are "inside" it. Is there a way to check if they are hitting the edges then repel?(7 votes)
- You can change the constraints on line 27. However, no code captures the notion of "inside" or "on the edge". It simply applies the inverse R² rule to the particle and repeller.
Note that both particles and the repeller are represented by points although they are drawn with circles. That non-realistic representation is the root of the issue. This program takes pain to address that issue: https://www.khanacademy.org/computer-programming/elastic-collisions-w-pvector/6239663899672576(7 votes)
- Oh noes guy says the function eval() can be harmful, can anyone tell me why? Also, is there a better place to put questions like this, that don't relate to any of the lessons?(2 votes)
- For your second question: No, this is the right place to be.
For your first question: JavaScript can be powerful.eval()
allows other people to execute their JavaScript code on your website. That means you're giving those people a lot of power...which can be abused in ways you may not have expected. This opens a path for hackers.(12 votes)
- Was this one hard to understand for anybody or is it just me?(6 votes)
- This course as well as Algorithms is where most people struggle to move forward given the information density and evaluator's pickiness. It's been a couple of years, why not give this another try?(1 vote)
angleMode = "radians";
var Repeller = function(x, y) {
this.power = 400;
this.position = new PVector(x, y);
};
Repeller.prototype.display = function() {
image(getImage("cute/Rock"), this.position.x, this.position.y-50, 60, 80);
};
Repeller.prototype.calculateRepelForce = function(p) {
var dir = PVector.sub(this.position, p.position);
var d = dir.mag();
dir.normalize();
d = constrain(d, 1, 100);
var force = -1 * this.power/ (d * d);
dir.mult(force);
return dir;
};
var Particle = function(position) {
this.acceleration = new PVector(0, 0);
this.velocity = new PVector(random(0, 1), random(-1, 0.5));
this.position = position.get();
this.timeToLive = 255.0;
this.mass = 10;
};
Particle.prototype.run = function() {
this.update();
this.display();
};
Particle.prototype.applyForce = function(force) {
var f = force.get();
f.div(this.mass);
this.acceleration.add(f);
};
Particle.prototype.update = function() {
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
this.acceleration.mult(0);
this.timeToLive -= 2.0;
};
Particle.prototype.display = function() {
noStroke();
fill(204, 241, 255, 100);
ellipse(this.position.x, this.position.y, 12, 12);
};
Particle.prototype.isDead = function(){
if (this.timeToLive < 0.0 || this.position.x > width || this.position.y < 0 || this.position.y > height) {
return true;
} else {
return false;
}
};
var ParticleSystem = function(origin, height) {
this.origin = origin.get();
this.height = height;
this.particles = [];
};
ParticleSystem.prototype.addParticle = function() {
for (var i = 0; i < 10; i++) {
var startPos = new PVector(this.origin.x,
this.origin.y + random(-this.height/2, this.height/2));
this.particles.push(new Particle(startPos));
}
};
ParticleSystem.prototype.applyForce = function(f){
for(var i = 0; i < this.particles.length; i++){
this.particles[i].applyForce(f);
}
};
ParticleSystem.prototype.applyRepeller = function(r) {
for(var i = 0; i < this.particles.length; i++){
var p = this.particles[i];
var force = r.calculateRepelForce(p);
p.applyForce(force);
}
};
ParticleSystem.prototype.run = function(){
for (var i = this.particles.length-1; i >= 0; i--) {
var p = this.particles[i];
p.run();
if (p.isDead()) {
this.particles.splice(i, 1);
}
}
};
var repellers = [];
//for (var i = 0; i<10; i++){
mousePressed = function(){
repellers.push(new Repeller(mouseX, mouseY));
};
// Set up water pressure, particle system, and repeller
var pressure = new PVector(0.4, 0);
var particleSystem = new ParticleSystem(new PVector(0, height/2), 200);
//var repeller = new Repeller(width/2, height/2);
draw = function() {
// Draw ground
for (var i = 0; i < 10; i++) {
image(getImage("cute/DirtBlock"), i*95, -50);
image(getImage("cute/DirtBlock"), i*95, 268);
}
// Draw river
noStroke();
fill(163, 230, 255);
rect(0, 86, width, 233);
for (var i = 0; i < repellers.length; i++) {
// Update particle system
particleSystem.applyForce(pressure);
particleSystem.applyRepeller(repellers[i]);
repellers[i].display();
}
//repeller.display();
particleSystem.addParticle();
particleSystem.run();
};
so i did the next challenge and it works but it doesn't pass, why?(4 votes)- First of all, thanks for posting your code, because it helped me pass. Secondly, the grader, as usual, is being a nitpicker--instead of using "mousePressed", write "mouseClicked".(4 votes)
- Visually, I can see what
this.acceleration.mult(0);
does, but what is it actually doing?(3 votes)- Multiplying by the scaler zero is a simple way of zeroing out all the components of the PVector. It is simpler than
this.acceleration.set(0, 0, 0);
(3 votes)
- Why, on KA, does some program's text get misaligned? I can see from the tumbnails that they were supposed to be aligned but for me its all cut off?(1 vote)
- The easiest explanation is "font mismatch". The author's machine uses a font that your machine does not have, so your machine use its default font. Disappointment ensues.
In the example you cite, the author is using his default font. We expect defaults to be (nearly) the same, but there is no guarantee of that!
Scrutinize the word "You" in the thumbnail versus what you see in the running program. The "o" is half of the height of the "Y" in the thumbnail. On my machine, the "o" is 70% or 80% of the height of the "Y". I'm sure this mismatch carries over to the widths too, and thus our disappointment.(7 votes)
- Last example, line 38, I typed
this.position = position
instead ofthis.position = position.get();
and got a different result. I think I know why, but I'm not 100% clear. Please explain.(3 votes)- It's because position.get(); will return the original position variable, but the position variable is being changed in between the point where they first set position and where you changed the code. Just using position will use the current position value, not the original.(2 votes)
- On the second program in this lesson line 49 it says
Particle.call(this, position);
can anyone tell me what .call does(2 votes)- A function's
call
method allows you to invoke the function and explicitly set the keywordthis
to the first argument ofcall
(in this case, it is alsothis
).
The tutorial on inheritance is here: https://www.khanacademy.org/computing/computer-programming/programming/object-oriented/pt/object-inheritance(2 votes)
- How would I get a particle to react to another particle? I know how to adjust each particle one-by-one using a
for
loop. But how could I get an individual particle to react to all other particles? Thanks!(2 votes)- Probably the best way would be using a nested for loop. First loops through all the particles once. Then do it again twice. Then, if the item in the first loop does not equal the item in the second, react!
for (var i = 0; i < particles.length; i++) {
for (var j = 0; j < particles.length; j++) {
if (particles[i] !== particles[j]) {
// Do whatever
}
}
}(2 votes)
- How come the repeller example above doesn't treat the repeller as a solid object that makes things move around it, but instead just randomly scatters some and sightly changes the angle of the rest of the particles.(1 vote)
- If they did treat it as a solid object, the particles would bounce off the surface. It would become difficult to see the effect of the repelling force.(3 votes)