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
A particle system
So far, we've managed to create a single particle that we re-spawn whenever it dies. Now, we want to create a continuous stream of particles, adding a new one with each cycle through
draw()
. We could just create an array and push a new particle onto it each time:var particles = [];
draw = function() {
background(133, 173, 242);
particles.push(new Particle(new PVector(width/2, 50)));
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
p.run();
}
};
If you try that out and run that code for a few minutes, you'll probably start to see the frame rate slow down further and further until the program grinds to a halt. That's because we're creating more and more particles that we have to process and display, without ever removing any. Once the particles are dead, they're useless, so we may as well save our program from unnecessary work and remove those particles.
To remove items from an array in JavaScript, we can use the
splice()
method, specifying the desired index to delete and number to delete (just one). We'd do that after querying whether the particle is in fact dead:var particles = [];
draw = function() {
background(133, 173, 242);
particles.push(new Particle(new PVector(width/2, 50)));
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
p.run();
if (p.isDead()) {
particles.splice(i, 1);
}
}
};
Although the above code will run just fine (and the program will never grind to a halt), we have opened up a medium-sized can of worms. Whenever we manipulate the contents of an array while iterating through that very array, we can get ourselves into trouble. Take, for example, the following code:
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
p.run();
particles.push(new Particle(new PVector(width/2, 50)));
}
This is a somewhat extreme example (with flawed logic), but it proves the point. In the above case, for each particle in the array, we add a new particle to the array (thus changing the
length
of the array). This will result in an infinite loop, as i
can never increment past particles.length
.While removing items from the particles array during a loop doesn’t cause the program to crash (as it does with adding), the problem is almost more insidious in that it leaves no evidence. To discover the problem we must first establish an important fact. When an item is removed from an array, all items are shifted one spot to the left. Note the diagram below where particle C (index 2) is removed. Particles A and B keep the same index, while particles D and E shift from 3 and 4 to 2 and 3, respectively.
Let’s pretend we are
i
looping through the array.- when i = 0 → Check particle A → Do not delete
- when i = 1 → Check particle B → Do not delete
- when i = 2 → Check particle C → Delete!
- (Slide particles D and E back from slots 3 and 4 to 2 and 3)
- when i = 3 → Check particle E → Do not delete
Notice the problem? We never checked particle D! When C was deleted from slot #2, D moved into slot #2, but
i
has already moved on to slot #3. This is not a disaster, since particle D will get checked the next time around. Still, the expectation is that we are writing code to iterate through every single item of the array. Skipping an item is unacceptable.There's a simple solution to this problem: just iterate through the array backwards. If you are sliding items from right to left as items are removed, it’s impossible to skip an item by accident. All we have to do is modify the three bits in the for loop:
for (var i = particles.length-1; i >= 0; i--) {
var p = particles[i];
p.run();
if (p.isDead()) {
particles.splice(i, 1);
}
}
Putting it all together, we have this:
OK. Now we’ve done two things. We’ve written an object to describe an individual
Particle
. We’ve figured out how to use arrays to manage many Particle
objects (with the ability to add and delete at will).We could stop here. However, one additional step we can and should take is to create an object to describe the collection of
Particle
objects itself—the ParticleSystem
object. This will allow us to remove the bulky logic of looping through all particles from the main tab, as well as open up the possibility of having more than one particle system.If you recall the goal we set at the beginning of this lesson, we wanted our program to look like this:
var ps = new ParticleSystem(new PVector(width/2, 50));
draw = function() {
background(0, 0, 0);
ps.run();
};
Let's take the program we wrote above and see how to fit it into the
ParticleSystem
object.Here's what we had before:
var particles = [];
draw = function() {
background(133, 173, 242);
particles.push(new Particle(new PVector(width/2, 50)));
for (var i = particles.length-1; i >= 0; i--) {
var p = particles[i];
p.run();
if (p.isDead()) {
particles.splice(i, 1);
}
}
};
Here's how we can rewrite that into an object - we'll make the
particles
array a property of the object, make a wrapper method addParticle
for adding new particles, and put all the particle running logic in run
:var ParticleSystem = function() {
this.particles = [];
};
ParticleSystem.prototype.addParticle = function() {
this.particles.push(new Particle());
};
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);
}
}
};
We could also add some new features to the particle system itself. For example, it might be useful for the
ParticleSystem
object to keep track of an origin point where particles are made. This fits in with the idea of a particle system being an “emitter,” a place where particles are born and sent out into the world. The origin point should be initialized in the constructor.var ParticleSystem = function(position) {
this.origin = position.get();
this.particles = [];
};
ParticleSystem.prototype.addParticle = function() {
this.particles.push(new Particle(this.origin));
};
Here it is, all together now:
Want to join the conversation?
- what are we supposed to do in the next challenge step 3. I keep having the message
"You shouldn't need to use a global variable inside the Fish object. Can you figure out a solution that doesn't use a global variable? " and yet my fish is bouncing and buble follow him.this.position.x = cos(frameCount)*120 + 200;
this is what i have.(5 votes)- You should be moving your fish inside
Fish.prototype.swim = function()
.
I found the wording of "make it swim back and forth across the screen, forever" a bit misleading. To satisfy the grader, your fish only needs to swim across the screen in one direction, over and over.(17 votes)
- I feel like this course would become less challenging to go through if videos instead of documents were made, just like intro to JS.(10 votes)
- It is an advanced course, and it is free. So we are happy for anything given to us.(11 votes)
- One thing I have noticed about particle systems here on KA is runtime issues and glitchy movement and slow, laggy movement....
How do I make the programs less slow? (Other than the obvious, splicing arrays based onthis.timeToLive
, et cetera)(4 votes)- You buy the same kind of machine that the Khan Academy developers use. Otherwise you suffer with the machines used by us plebeians.
You should make sure not to waste the CPU. If you are drawing 1000 particles of the same color, then there is no need for thefill
to be buried inside a loop.(10 votes)
- this is so cool, but when i try to make it, the for loop makes it laggier. Why does that happen?(4 votes)
- There are two reasons I can think of. The first (the most common), is that your particles never die, so you continually build up more and more particles, thus taking up more memory and processing power, and thus creating the lag. Eventually, your program will use up all its resources and should crash. The second reason may be that your computer simply doesn't have enough resources to handle your particle system. This usually happens if you have an older computer. If you were able to read this article though, then this shouldn't be your problem.(9 votes)
- On the "Fish Bubbles" challenge, what does the percent symbol mean after you complete step one? I was stuck and had to look at someone else's code for help, but I don't exactly understand it. Here's the code (lines 93-95 in my program):
if (frameCount % 5 === 0) {
bubbles.addParticle();
}(5 votes)- https://www.khanacademy.org/computer-programming/framecount-processingjs/5893935759097856
a nice demo
framecount is an ever increasing value
the % operator gives remainder
% 5 === 0 means if exactly divisible by 5 (i.e remainder is 0)
this will happen only after each five values(5 votes)
- I have tried removing
get()
from theposition.get()
of the particle object and the animation acts strangely. The origin of the particle system changes it's position similar to the particle itself. I've tried that to the first project above, but nothing wrong with that. I think that the particle system object cause that. Here is the code I've tried to find the reason, addingtest
local variable for theps
object to observe theorigin
behavior.// Adapted from Dan Shiffman, natureofcode.com
// A single 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 = position;//.get();
this.timeToLive = 255.0;
};
Particle.prototype.run = function() {
this.update();
this.display();
};
Particle.prototype.update = function(){
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
this.timeToLive -= 2;
};
Particle.prototype.display = function() {
stroke(255, 255, 255, this.timeToLive);
strokeWeight(2);
fill(210, 210, 255, this.timeToLive);
ellipse(this.position.x, this.position.y, 12, 12);
};
Particle.prototype.isDead = function() {
if (this.timeToLive < 0) {
return true;
} else {
return false;
}
};
var ParticleSystem = function(position) {
this.origin = position.get();
this.test = position.get();
this.particles = [];
};
ParticleSystem.prototype.addParticle = function() {
this.particles.push(new Particle(this.origin));
};
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 ps = new ParticleSystem(new PVector(width/2, 50));
var draw = function() {
background(204, 90, 204);
ps.run();
ps.addParticle();
println(ps.test);
};// Adapted from Dan Shiffman, natureofcode.com
// A single 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 = position;//.get();
this.timeToLive = 255.0;
};
Particle.prototype.run = function() {
this.update();
this.display();
};
Particle.prototype.update = function(){
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
this.timeToLive -= 2;
};
Particle.prototype.display = function() {
stroke(255, 255, 255, this.timeToLive);
strokeWeight(2);
fill(210, 210, 255, this.timeToLive);
ellipse(this.position.x, this.position.y, 12, 12);
};
Particle.prototype.isDead = function() {
if (this.timeToLive < 0) {
return true;
} else {
return false;
}
};
var ParticleSystem = function(position) {
this.origin = position.get();
this.test = position.get();
this.particles = [];
};
ParticleSystem.prototype.addParticle = function() {
this.particles.push(new Particle(this.origin));
};
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 ps = new ParticleSystem(new PVector(width/2, 50));
var draw = function() {
background(204, 90, 204);
ps.run();
ps.addParticle();
println(ps.test);
};
Can someone explain the reason why?(3 votes)- It has to do with the way things are stored in memory. When the variable you're passing to a method is a primitive (integer, character, double), the method receives a copy of that variable, and now you have two variables in memory. And the method can do whatever it needs to do with it's copy, and when it's done, you still have the original variable.
That's not how passing an object works. When you pass an object (and vector is an object), you're not creating any copies, you're passing a memory address of that object. And when the method does it's thing on the object that was passed to it, it changes the original object.
The original constructor creates a vectorthis.position
that has the same parameters asposition
vector that is passed to it (which is what.get()
does, it "gets" the parameters of a vector it was called on, and assigns them to a new vector object). When you remove.get()
part, you're basically saying "this.position
is the same object asposition
", you make this variable point to the memory address of the original object. So whenupdate()
starts changingthis.position
, it actually changesthis.origin
of theParticleSystem
.
Hope that makes sense. If it doesn't, check out this lecture: https://www.youtube.com/watch?v=W8nNdNZ40EQ(6 votes)
- The final code has oh noes saying A for loop is taking too long to run. Perhaps you have a mistake in your code?(5 votes)
- The program, like most programs at Khan Academy that create an arbitrary number of
new
objects, suffers from a memory leak. Eventually, the garbage collector takes too much time in its futile attempt to find free memory that the Khan Academy loop detector blames the nearest bystander. See https://www.khanacademy.org/computer-programming/leak/5149570618228736 for my bug submission, and https://www.khanacademy.org/computer-programming/leak-free-particle-system-revisited/5026071807344640 for a mitigation technique.(2 votes)
- For the next challenge step 2 I have:
var radius = (-this.position.y+200)/5+10;
It still won't let me continue to the next step
Could someone please give the code that the grader likes.(5 votes) - in the next challenge they use the variable radius to define the width and height of the bubble. however the radius of a circle is actually the distance between the center of the circle to the edge.
Wouldn't that mean that the more correct name for that variable would be diameter?(4 votes)- You can use radius to find the height and width of a circle. Since it is the distance from the center to the edge, it is that same distance all around. This means that the height of the bubble is twice that of the radius. Even though diameter doesn't need to have the multiplication of two, radius still can work. I hope this helps.(2 votes)
- Despite the behavior of the grader, is there a difference between
bubbles.origin.set(fish.getMouthPosition());
andbubbles.origin = fish.getMouthPosition().get();
(4 votes)