Weapons - part 3

Top  Previous  Next

This month we will learn to create a grenade launcher. How does a grenade's trajectory look like? Here's a picture of me (while I was in the army) throwing one; you will notice that I am a tall, handsome guy :)

 

aum60_workshop1

 

Some of you might argue and say that I'm only throwing an onion in that picture, but trust me: that onion is deadly, especially if you eat it without having a loaf of bread nearby...

 

You can see that the grenade moves in the direction that's faced by the player, changing its height at all times, ascending and descending as it hits the ground. Looks easy in theory, and it's even easier to code thanks to the marvelous "bounce" vector. See for yourself - take a good look at the script file.

 

Oh, I have forgotten to tell you that the main script file, as well as player's script file are similar with the ones that were used last month; we will only examine the content of the new weapons3.wdl script.

 

aum60_workshop2

 

Nothing new here! I am using an action that is almost identical with the one that was used for our machine gun from Aum58's workshop (with a different name, of course). I didn't want us to create a grenade launcher that uses auto-fire, so we've got a slightly modified firing action: when the player presses the left mouse button, we get the starting point coordinates for the grenade (located on the 37th vertex on our gun model), we create a grenade_mdl at that position, we attach it the action named move_grenade and then we play a grenade_wav sound with a volume of 100.

 

Let's move on to the new stuff.

 

aum60_workshop3

 

The first line of code inside function move_grenade() sets grenade's skill99 to 1234; this way we will be able to detect (later) if our enemies have interacted with a grenade or with another scanning entity. You can see that the grenade will have the same pan and tilt angle with the camera, so it will start moving in the direction that is faced by the player.

 

The grenade is sensitive to impact with entities and level blocks and runs its event function (bounce_grenade) as soon as it collides with something. I am using skill1... skill3 as a single, local vector to control the movement speed for the grenade; skill1 will store the horizontal movement speed (100 * time_step), skill2 will store the sideway movement speed (0) and skill3 will store the vertical movement speed (10 * time_step for now).

 

The following "while" loop will allow the grenade to move for 3 seconds, constantly decreasing its vertical movement speed (skill3) and storing the result of the c_move instruction (the distance that was covered in the last frame, in quants) inside skill4.

 

Let's discuss a simplified version of the while loop for a bit:

 

my.skill10 = 0;

while (my.skill10 < 24) // this loop will run for 24 seconds

{

       my.skill10 += time_step / 16;

       wait (1);

}

 

The code is pretty straightforward; we are increasing skill10 until it reaches 24, causing the loop to stop. But how do we know that the loop will run for exactly 24 seconds? First of all, we must make sure that skill10 is set to zero before being modified by the loop. The value will be zero if we didn't use skill10 yet, but if it was used somewhere else we must reset it.

 

What is time_step? It's the duration of the last frame in ticks, with 1 tick = 1 / 16 seconds. If the frame rate is 16 frames per second, time_step = 1. At 32 fps (two times bigger), time_step will be 0.5 (two times smaller) and so on. As a general rule, time_step * frame rate = 1.

 

If I add time_step / 16 to my.skill10, I am adding 1 / 16 to it if the frame rate is 16 fps, 1 / 32 if the frame rate is 32 fps, 1 / 94 if the frame rate is 94 fps and so on, get it? But if my frame rate is 16 fps, I will add 1 / 16 to my.skill10 16 times per second, because all the loops are forced to run at this pace. This means that I will add (1 / 16) * 16 = 1 to my.skill10 every second. In a similar way, if the frame rate is (let's say) 63 fps I will add 1 / 63 to my variable 63 times per second (that's the fps rate!), so I will end up adding the same (1 / 63) * 63 = 1 to my.skill10. The method isn't extremely precise because time_step can change a bit from one frame to the other, but it will work great for most of your needs.

 

Now that we know why the grenade will live for 3 seconds, let's get back to the code. As soon as those seconds have passed, we make the grenade invisible and passable (we hide its model), we create an explosion sprite, we play an explosion sound, and then we scan around it for 2 seconds, on a range of 200 quants. The last line of code removes the grenade model from the level.

 

Please note how I have reset my.skill10 to zero before using the second "while" loop; without that line of code, skill10 would have remained set to 3 from the previous loop and the c_scan loop would have been bypassed completely. You should know your c_scan now; read Aum54 to learn more about it.

 

We are going to talk a bit about "bounce" now. Bounce is a vector which is set for us automatically by a collision or by a c_trace instruction, showing the direction in which an entity would bounce off a surface. If you've ever played an Arkaniod clone, you know how the ball bounces off the paddle - that's exactly how the "bounce" vector works in 3DGS, computing all the needed directions (x, y, z) for us automatically. Ain't life great? :)

 

I have modified the grenade launcher code in order to make it fire particle trails that show the "bounce" angles after impact - take a look!

 

aum60_workshop5

 

In my example, the grenade comes from the left side of the screen, hits the floor, setting the bounce(x, y, z) vector and then continues its journey towards the right side of the screen. As soon as the grenade impacts with the floor, bounce.x is set to the direction which makes the grenade move forward, bounce.y to the direction that moves the grenade sideways and bounce.z is used to move the grenade vertically. All we need to do is to orient the grenade properly, by changing its pan and tilt angles according to the "bounce" vector components and we are set! Why is that happening? Well, let's not forget that our grenade moves in the direction given by its pan and tilt angles at all times - that's how we have coded it using c_move's relative speed vector.

 

With all this fresh knowledge in our minds, let's examine the bounce_grenade() event function.

 

aum60_workshop4

 

You might remember that we are storing the distance (in quants) that was covered by the grenade inside skill4; the event function tells the grenade to adjust its angles according to the "bounce" vector values only if the distance covered by it is greater than 5 quants per frame (the grenade is still moving at a decent speed). This prevents the grenade from bouncing around in a silly way at small movement speeds, making its landing look much more natural. If the grenade is moving at a decent speed, "vec_to_angle (my.pan, bounce)" tells it to change its pan and tilt angles (its movement direction) according to the values that are returned by "bounce", just like in our picture.

 

Each impact decreases the horizontal movement speed stored inside skill1; if the grenade has started to move with 100 * time_step, it will move with 100 * 0.75 * time_step after its first collision, with 100 * 0.75 * 0.75 * time_step after the second collision, and so on.

 

If there has been a collision on the vertical movement axis (bounce.z != 0), the sign of the vertical movement speed will be changed after each vertical collision, making the grenade bounce up and down. This happens because we are multiplying my.skill3 with a negative value, thus getting a negative positive negative positive... series of values for my.skill3. This causes the grenade to move up and down several times, decreasing its vertical movement speed at the same time. If the vertical movement speed goes below 0.01, the grenade is stopped completely by resetting skill1, skill2 and skill3, which are treated as a single vector with 3 components when they are used this way (as consecutive skills).

 

Don't fall asleep! We've only got two tiny function to discuss!

 

aum60_workshop6

 

This function takes care of the explosion sprite: it makes it passable, places it a bit above the ground, sets is scale, a few flags that make it look better and brighter, sets its ambient to 100, picks a random roll angle for it (in order to make the explosions look more random), makes it transparent, plays its animation frames once, and then it fades away the last animation frame, removing the sprite when it is all over.

 

Ok, so now we've got a decent grenade launcher that fires bouncing, exploding grenades... what are we missing? I know! We need a few dummy enemies which are sensitive to c_scan!

 

aum60_workshop7

 

You can see that the enemies are being made sensitive to c_scan instructions, running their got_scanned() event function each time when they are scanned. The event function checks if the scanner is a grenade or not (skill99 = 1234 or not). Now you see why we have set my.skill99 = 1234 for each grenade; our level could include other entities performing "good" c_scan instructions (to start a light or to open a door, for example) and we wouldn't want those instructions to kill the enemies nearby.

 

If the scanning comes from the proper source (skill99 = 1234), the enemy will stop reacting to events for good (no need to die several times) and then it will play a one-shot "death" animation, becoming passable as soon as the animation ends.

 

I hope that you had fun reading this article; I know I had fun writing it! I'll see you all in about a month!