Previous: Lesson 2. Building a simple particle fountain
First, by looking at Figure 2.1, we note that the number of sparks produced by our minimal fountain depends on the frame rate: lots of particles at 80 fps (left figure) [fps = Frames Per Second - see manual] but a lot less at 30 fps (right figure). Not good since, in a game, this difference would be large enough for the person holding the joystick to notice and wonder what is going on. What is happening here...?
A quick look at action a_minimal_source shows the culprit: with just one effect() statement in the while(1) loop, the action creates a single particle at each frame, no matter the frame rate. So, if the game is running at 80 fps, the source produces 80 particles per second. If, however, it runs at 20 fps, only 20 particles are created per second. This is the difference we see on Figure 2.1.
The easiest solution to this problem would be to limit the maximum frame rate to a value of 30 fps or so (using the fps_max system variable - see manual). However, particle effects, I think, are not a good reason for such drastic measures. And also, the person who paid big bucks for a brand new, high performance graphics card would feel a little ripped off. So we should try to do better...
A smarter solution would be to make the instructions in the action independent of the frame rate. Those of you who have read through the C-script tutorial are thinking that we need to introduce some time factors in the action a_minimal_source. That is exactly right.
Let us define n the number of particles which the action creates at each frame. In a_minimal_source, n is just equal to 1. However, if n could change its value depending on the frame rate, then we could keep the number of particles created per second constant at all times. For instance, if the game is running at 20 fps and n = 4, then the fountain would produce 4*20 = 80 particles. This is the exact same number of particles than if n = 1 and the game is running at 80 fps. What we need now is a formula which reads the frame rate and then gives the right value for n. Here it is:
n = round(n_0*time/time_0) (2.3)
where time_0 = 0.2, n_0 = 1. time is of course the system-defined variable equal to the number of ticks it took to compute and display the previous frame on screen (see manual). round(x) is a function we define below and which rounds x to the nearest integer (= number without decimals). For example, round(1.25) = 1 and round(0.9) = 1:
function round(x) { return(int(x) + (frc(x)>0.5)); } (2.4)
With this equation in hand, let's modify the action of the fountain to compute n at each frame and to produce these n particles:
var n_0 = 2; var time_0 = 0.2; ... my.skill1 = 1; n = round(n_0*time/time_0); while(my.skill1<=n) { // compute velocity ... // create particles effect(improved_part,1,my.x,velocity.x); my.skill1 += 1; } (2.5)
The variable skill1 is used to count the particles produced and a while loop repeats the effect statement n times. Note also that we draw a new velocity vector for each of the n particles. Much better! We now have a fountain with looks the same when the frame rate varies.
"But hang on a second here! How does equation (2.3) work? How do I use it, besides putting it in my code together with a loop like you did in equation (2.5)? How does it take into account the frame rate and where does it come from?" you might think.
Ok, first thing first: the frame rate. n in equation (2.3) knows the current frame rate of the game thanks to the factor time. Indeed, the frame rate is just the number of frames displayed during one second, or equivalently 16 ticks. Since time is the number of ticks it takes the system to compute and display a frame, then dividing 16 by time gives the frame rate. For instance, if time = 0.2, we have the frame rate 16/0.2 = 80 images per second or 80 fps. As the frame rate changes, time will also change, and so will n. If the display rate quickens, time will drop and n will become smaller: we need to produce fewer particles at each frame. On the other hand, if the fps drops, time will increase and so will n: we have to produce more particles to compensate.
Now how do we use equation (2.3)? We just have to adjust n_0 and time_0 according to our needs. These variables specify the number of particles n_0 to be produced on each frame at the frame rate time_0. In the present case, I wish to create 80 particles per second. One way to impose that is to say that I want to produce 1 particle at each frame for a frame rate of 80 fps, so when the time variable is 0.2. Therefore, I set n_0 = 1 and time_0 = 0.2. Equation (2.3) therefore says "tell me how many particles you want to produce each second and I will tell you how many you need to produce on each frame at the present frame rate".
Note that the particular values I chose for n_0 and time_0 do not have anything special. I could have taken instead n_0 = 2 and time_0 = 0.4 (2 particles/frame at 40 fps), n_0 = 4 and time_0 = 0.8 (4 particles/frame at 20 fps), or whatever. Just choose n_0 and time_0 so that (number of particles/frame)*(frame rate) = n_0*(16/ time_0) gives the number of particles/second you need.
To make sure that equation (2.3) works properly, let's replace in it some values for time and see what n comes out. If time = 0.2, corresponding to 80 fps, we get n = round(0.2*1/0.2) = round(1) = 1. That is correct: producing one particle/frame at 80 fps gives 80 particles/second. If time = 0.4, corresponding to 40 fps, we get n = round(0.4*1/0.2) = round(2) = 2 which is also correct: 2*40 = 80 and so on. If you need further confirmation try out other values. You can also display n on screen with a panel in lesson2.wed and observe how it increases or decreases when you toggle the frame rate. :-)
Finally, how does one derive this equation? It just takes a little simple maths. Let us consider two arbitrary frame rates: a 'reference' frame rate '0' (corresponding to time = time_0) and the current frame rate (corresponding to time). Let n_0 be the number of particles we create at each frame at the frame rate time_0, and n that number for the current frame rate. Then, n_0*16/ time_0 and n*16/ time represent the numbers of particles produced per second in each case. Since we want to adjust n so as to have the same number of particles whatever the display speed, we set
n_0*16/time_0 = n*16/time.
Factoring out the 16 and multiplying both sides by time, we get
n = time*n_0/time_0.
Since n is the number of particles to be produced, it has to be an integer. Therefore, we round off any decimals on the right hand side of the equation using the round() function defined above and obtain equation (2.3).
The reader might make the following objection: "But if the game slows down, the formula makes the game produce even more particles. Will that not slow the game even more?"
In theory, yes, but the amount by which this happens seems very small compared to the original display speed loss. In my opinion, this could be because particles are very efficiently programmed in 3DGS (a game can handle up to 10 000 of them). So although their production rate is influenced by the frame rate, they should not be too much of a factor in determining the frame rate itself (unless huge quantities of them are produced or if you have particle beams which are crossing each other in front of the camera - a sure fire way of taxing your graphic card).
Next: 2.b Choosing the initial velocity and position of the particles