Monday, 20th September 2010
In the previous tutorial we allowed the user to interact with the particles, but the particles didn't interact with one another. Now it's time to make them more tangible. In this tutorial, we will:
- Test whether any two particles have collided
- Make particles that have collided bounce
- Prevent colliding particles from sticking to one another
Things start to get complex once we have multiple particles interacting with one another. In fact, it's mathematically impossible to solve the equations describing three or more interacting objects. In our simulation we'll have to make some simplifications, which we make it 'inaccurate', however, it should still represent a reasonable approximation to reality.
Firstly, we want to check whether any two particles overlap with one other, so we need to compare every particle with every other of particle. We can do this with a nested
for loop. Since we already have one loop going through the particle array, we can use that.
for i, particle in enumerate(my_particles): particle.move() particle.bounce() for particle2 in my_particles[i+1:]: collide(particle, particle2) particle.display()
Note that the second loop is not a full loop; if we had two full loops, we'd compare every particle to every other particle twice over (and compare each particle to itself). Instead, we compare each particle with every particle with a higher index than it in the array. We therefore need to know the index of the current particle which we can get using
enumerate. We use this value (i) to slice the
my_particle array from i+1 to the end to construct the second loop.
Now we need to actually define the
collide() function. The first thing the function has to do is to test whether the two particles have collided. This is very simple as the particles are circles. We simply to measure the distance between them (using math.hypot() again) and test whether this value is less than their combined radius..
def collide(p1, p2): dx = p1.x - p2.x dy = p1.y - p2.y distance = math.hypot(dx, dy) if distance < p1.size + p2.size: print 'Bang!'
So far all that happens when two particles collide is that we print 'Bang!'. I'll complete the function as soon as I get a chance.
The angle of collision
When two particles collide, we want them bounce off each other. Theoretically, when two circular particles collide they contact at an infinitely small point. The angle of this point is the tangent of the particle at this point. As the diagram below I hope shows, this angle is perpendicular to the line that joins the centre of the two particles.
We can treat the collision as though the particles were bouncing off a flat surface with the angle of the tangent. We can find the angle of the tangent with the following code:
tangent = math.atan2(dy, dx)
To reflect the angle of the particles on this surface, we subtract their current angle from two times the tangent. I'll have to explain the logic behind this at a later date. It's at this point that knowing the angle that our particle is travelling starts to become useful.
p1.angle = 2*tangent - p1.angle p2.angle = 2*tangent - p2.angle
Then we need to exchange the speeds of the two particles as they transfer their energy to on another. We can do this in a single line by constructing a tuple.
(p1.speed, p2.speed) = (p2.speed, p1.speed)
Note that writing the exchange as two lines as below does not work.
p1.speed = p2.speed p2.speed = p1.speed
The reason this doesn't work, as you may have realised, is that when we come to assign p2.speed on the second line, we are assigning it to the new value of p1.speed, which we have already made equal to p2.speed. Constructing an tuple, which is immutable, allows us to avoid this problem. I would then add some code to reduce the energy of both particles as a result of the collision:
p1.speed *= elasticity p2.speed *= elasticity
If you run the simulation now, you'll probably find that the particles stick to one another, and can even get stuck in mid air. The reason for this is that we have a discrete simulation (i.e. the time between updating the particle positions is not infinitely small). This introduces errors, which, for the most part, aren't important (though you should be aware of them).
What's happening is that when a particle collision is detected, the particles have actually overlapped slightly. When we alter their angle and speed then move them, we can't be sure that the particles are no longer overlapping (particularly because of drag, gravity and elasticity also altering their movement). If they still overlap then they will 'bounce' back the way they came, towards each other. They can then get trapped in a loop of 'internal bouncing', which gradually reduces their speed to nothing.
To avoid this problem we add some code into the
collide() function to fudge the fact that the particles should have bounced before they overlapped. We calculate the angle between the two particles (which is 90 degrees to the tangent) and move the particles along this angle one pixel. We could work out exactly how far to move the particles along this angle, but I don't think it's worth the extra calculation. The code below moves the particles away from one another by a sufficiently large amount, and seems to work pretty well.
angle = 0.5 * math.pi + tangent p1.x += math.sin(angle) p1.y -= math.cos(angle) p2.x -= math.sin(angle) p2.y += math.cos(angle)
Now the particles should bounce off one another cleanly: