Planet Survivors - part 2

This month we will get to play the first P.S. level. Let's take a look at a few screenshots first:

This first level doesn't include enemies, but there are plenty asteroids that can pump adrenaline in your veins :). The main.wdl script went through some minor changes; all the interesting stuff is coded inside level1.wdl. Let's begin with some panels and a text:

panel shield_pan
{
bmap = shield_tga;
layer = 10;
pos_x = 732;
pos_y = 555;
digits = 7, 15, 3, figures_font, 1, player.shield;
flags = overlay, refresh, visible, transparent;
}

panel ammo_pan
{
bmap = ammo_tga;
layer = 10;
pos_x = 0;
pos_y = 555;
digits = 12, 15, 3, figures_font, 1, player.ammo;
flags = overlay, refresh, visible, transparent;
}

panel crosshair_pan
{
bmap = pointer2_tga;
layer = 20;
pos_x = 368;
pos_y = 282;
flags = transparent, overlay, refresh;
}

panel score_pan
{
bmap = score_tga;
layer = 10;
pos_x = 300;
pos_y = 5;
flags = overlay, refresh, visible;
}

text score_txt
{
pos_x = 380;
pos_y = 5;
layer = 10;
font = score_font;
string = score_str;
flags = visible;
}

These panels display the shield (in the lower right corner) the ammo (in the lower left corner) the crosshair (centered on the screen at 800x600 pixels) and the bitmap at the top of the screen that has "Score:" written on it. The text will display the score (yes, the score will be converted to a string before being displayed).

The player flies with a big speed above the surface of the planet, right? Wrong! The player is actually sitting still (excepting the cut scene) and the "planet" below it is an mdl that changes its "v" parameter in a while loop:

action planet_level1 // attached to the "planet" (a flat mdl, really)
{
my.passable = on;
fog_color = 1;
camera.clip_far = 100000; // works only with A6, delete this line if you own A5
// clip_range = 100000; // remove the comment if you own A5
camera.z = -100;
camera.fog_start = 50;
camera.fog_end = 20000;
while (ready_to_play == 0)
{
my.v += 3 * time;
wait (1);
}
my.z -= 5000;
my.ambient -= 30;
while (player_is_dead == 0)
{
my.v += 2 * time;
wait (1);
}
}

Like I said, the planet is a plane model and it is shifting its texture for as long as player's ship isn't destroyed. The texture that was used for the model is tileable, so the "level" appears to be endless; in fact, it can't end because player's ship is standing still! Now don't go and tell that to your customers! :)

I have activated my secret weapon - the orange fog with RGB = 200, 121, 50 and I have set nice ranges for it using camera.fog_start and camera.fog_end. On a side note, I will make reasonable efforts to keep the project running with both A6 and A5, but you will have to tweak some values (replace camera.clip_far with clip_range, choose different fog ranges, and so on) if you own A5. Take a good look at the screenshots to see how the level is supposed to look like. Ready_to_play is a variable that will be set to 1 when the player is able to control the ship (just right after the cut scene at the beginning). You can see that the "planet" is moved 5000 quants below when the game starts and it is make a bit darker. The "v" texture shifting continues at t smaller speed for as long as the player isn't dead.

I have used a similar model for the sky and a "u" shift for its texture:

action sky_level1
{
my.passable = on;
my.flare = on;
my.ambient = -20;
while (ready_to_play == 0)
{
my.u -= 0.3 * time;
wait (1);
}
ent_remove (my);
}

The sky will be removed as soon as the cut scene ends. The most horrifying action you've ever seen is listed below:

action player_level1
{
player = my;
my.ammo = 200;
my.shield = 100;
my.metal = on;
my.ambient = 50;
my.transparent = on;
my.alpha = 100;
my.enable_impact = on;
my.enable_entity = on;
my.event = player_was_hit;
vec_set(temp, player.x);
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
track_handle = media_play ("track1.wav", null, 50);
fade_factor = 1000;
my.engine = on;
plasma_jet(65, 63);

This is the action attached to the player; it sets the amount of ammo (just another name for skill21) and the shield (skill22). The ship is transparent (we will see later why) but its alpha factor is 100 (opaque) for now. The ship is sensitive to impact with other entities (the asteroids); its player_was_hit event will run if something collides with it. We use a vec_to_angle instruction to rotate the camera towards player's ship from the very beginning, in order to cut down the potential glitches, and then we play the sound track named track1.wav. I had to use a wave file because not all the editions can "media_play" mp3 or ogg files, but you should use compressed audio files.

Do you remember the particle function that is used for player's engine in the menu? Well, I have added a simple fade_factor to that function and now I can control the lifetime of the particles by setting fade_factor to a convenient value. I chose fade_factor = 1000 because the particles need to be destroyed quickly; the speed of the ship is too big for slow particles. Finally, I set the "engine" flag (flag1) and I run the function that creates the plasma jets at the vertices that are passed as parameters (function plasma_jet() from main.wdl was modified a bit too).

while (my.x < 3000)
{
my.roll += 1.5 * time;
ent_playsound (my, engine1_wav, 300);
my.x += 52 * time;
vec_set(temp, my.x);
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
wait (1);
}
ready_to_play = 1;
my.x = -10000;
my.y = 0;
my.z = -100;
my.roll = 0;

crosshair_pan.visible = on;
camera.x = -10230;
camera.y = 0;
camera.z = -50;
camera.pan = 0;
camera.tilt = 0;
camera.fog_start = 15000;
camera.fog_end = 50000;

fade_factor = 100;
snd_play (engage_wav, 70, 0);

The code above plays the cut scene; the ship will move from its initial position (x = -10,000) to x = 3,000 changing its roll angle and playing its engine sound. The camera follows the ship all the time (another vec_to_angle does that). As soon as the player reaches the x = 3000 quants limit, ready_to_play is set to 1 (the sky disappears) and the ship is moved back to -10,000 quants because we don't have a huge "planet" to play with and the customer won't notice the change. We show the crosshair, we set a proper position for the camera and we choose another set of fog ranges. Finally, we set fade_factor to 100 because the player is standing still now (though it appears to fly because of the planet "v" shift) and we play the engage_wav sound, to let the player know that he or she is in control now.

while (my.shield > 0)
{
camera.tilt += 10 * mouse_force.y * time;
camera.pan -= 10 * mouse_force.x * time;
if ((camera.pan > 30) && (camera.pan < 330)) // don't allow the player to blow our cover (if it looks back)
{
if (camera.pan < 180)
{
camera.pan = 30;
}
else
{
camera.pan = 330;
}
}
if (camera.tilt < -35)
{
camera.tilt = -35;
}
if (camera.tilt > 25)
{
camera.tilt = 25;
}
my.pan = camera.pan;
my.tilt = camera.tilt;

As long as player's ship isn't completely destroyed (shield > 0), the camera changes its angles when we move the mouse. Camera's pan angle can range from -30 to 30 degrees, while tilt ranges from -35 to 25 degrees. We do this because we can't allow the player to look back because he would see the level boundary. The ship inherits the same pan and tilt angles with the camera.

vec_set (temp_var, my.pos); // copy player's position to temp_var
vec_to_screen(temp_var, camera); // now temp_var contains the 2D position (x, y) of the ship
if ((abs(temp_var.x - 400) < 50) || (abs(temp_var.y - 300) < 40)) // the crosshair and the ship tend to overlap?
{
my.alpha = (abs(temp_var.x - 400) + abs(temp_var.y - 300));
}
else
{
if (my.alpha < 99)
{
my.alpha += 0.5 * time;
}
}

if ((mouse_left == 1) && (player.ammo > 0))
{
fire_rocket();
}

The lines above convert the 3D position of the ship to a 2D screen position, and adjust the transparency of the ship if the crosshair overlaps with it; take a look at the pictures below to see how it works.

This method allows the player to see any attacker / asteroid, even if it is standing in front of the ship. The last lines of code call function fire_rocket() if the player presses the left mouse button (LMB) and if the ammo is greater than zero.

if ((key_z == on) && (my.y < 100))
{
my.y += 10 * time;
if (my.roll < 30)
{
my.roll += 3 * time;
}
}
if ((key_x == on) && (my.y > -100))
{
my.y -= 10 * time;
if (my.roll > -30)
{
my.roll -= 3 * time;
}
}
if (key_z + key_x == 0)
{
if (my.roll > 1)
{
my.roll -= 1 * time;
}
if (my.roll < -1)
{
my.roll += 1 * time;
}
}
wait (1);
}

Did you know that the player can move to its left and right using the "Z" and "X" keys? This will be his only chance to avoid the enemies; please note that the movement is limited in this range: -100, 100 quants. The ship changes its roll angle when it is moved, and the it recovers slowly if key_z + key_x = 0 (none of them is pressed).

crosshair_pan.visible = off;
camera.pan = 0;
camera.tilt = -10;
while (my.z > -1990)
{
if (camera.tilt > -30) {camera.tilt -= 0.2 * time;}
my.x += 30 * time;
my.z -= 20 * time;
my.roll += 10 * time;
wait (1);
}
player_is_dead = 1;
my.skill10 = 10;
ent_create (explo13_pcx, my.pos, explode_me);
my.tilt = -50; // choose a weird position for the ship
ent_playsound (my, hit_wav, 5000);
while (1)
{
temp.x = 3 - random(6);
temp.y = 3 - random(6);
temp.z = 4 + random(4);
effect(particle_smoke, 10, my.pos, temp);
wait (1);
}
}

The last part of the action runs when the player is dead (shield <= 0). We set a convenient camera angle and then we can watch loosing its height and rolling, while camera.tilt is decreased slowly, in order to keep up with the ship. We set player_is_dead to 1, and this makes the "v" texture shifting stop, and then we create a big explosion sprite, attaching it the explode_me function. My.tilt = -50 sets a weird position for the ship, we hear the explosion and then we emit smoke in a while loop. R.I.P, courageous player... sniff....

Well, this level doesn't include conventional enemies, but hundreds of asteroids. Let's see what makes them tick:

action asteroid_maker
{
var temp_var;
my.passable = on;
my.invisible = on;
while (1)
{
temp.x = my.x;
temp.y = 5000 - random(10000); // -5,000 to 5,000
temp.z = 1000 - random(2000);
if (ready_to_play == 0) // game not started yet?
{
if ((temp.y > -500) && (temp.y < 500)) // the asteroid is too close to the player during the cut scene?
{
temp.y = 500; // then avoid the player until it is able to control the ship
}
}
temp_var = random(1);
if (temp_var < 0.33)
{
ent_create (asteroid1_mdl, temp, move_asteroid);
}
if ((temp_var >= 0.33) && (temp_var < 0.66))
{
ent_create (asteroid2_mdl, temp, move_asteroid);
}
if (temp_var > 0.66)
{
ent_create (asteroid3_mdl, temp, move_asteroid);
}
wait (2);
}
}

The model that has this action attached to it is made passable and invisible. Its x = 10,000 in Wed, so the asteroids will be generated at this x and will have random values on y (-5000 to 5000) and z (-1000 to 1000). If ready_to_play = 0 (this happens during the cut scene) the asteroids that could collide with the player are moved to y = 500 where they can't damage the ship. We generate a random number and then we store it in temp_var; choosing one of the 3 different asteroid models. The function that moves the asteroids is named move_asteroid:

function move_asteroid()
{
var meteor_angle;
var meteor_speed;
my.enable_impact = on;
my.enable_entity = on;
my.event = destroy_asteroid;
my.scale_x = 5 + random(10);
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.skill10 = my.scale_x; // store the scale in skill10 - we will use it to adjust the size of the explosion later
meteor_angle.x = 3 - random(6); // random pan speed
meteor_angle.y = 3 - random(6); // random tilt speed
meteor_angle.z = 3 - random(6); // random roll speed
meteor_speed.x = (-100 - random(80)) * time;
meteor_speed.y = 0;
meteor_speed.z = 0;

The asteroids are sensitive to impact with other entities (other asteroids, player's ship or player's rocket) and run their "destroy_asteroid" event function when they collide with something. We set a random scale for the asteroids, and we store it in their own skill10; later we will use this value to scale the explosion sprite accordingly, because a small asteroid should create a smaller explosion, while a huge asteroid should create a huge explosion. The asteroids rotate with random pan, tilt and roll speeds, and move using a negative speed of -100...-180 quants / frame.

while (my.x > -21000)
{
my.pan += meteor_angle.x * time;
my.tilt += meteor_angle.y * time;
my.roll += meteor_angle.z * time;
move_mode = ignore_you + ignore_passable;
ent_move (nullvector, meteor_speed);
wait (1);
}
ent_remove (my);
}

The asteroids will move toward player's ship until their x goes below -21,000 quants (if they manage to get there), rotating all the time. As soon as the asteroids get past the -21,000 quants limit, they are removed. Let's examine their event function now:

function destroy_asteroid()
{
if (you.skill40 == 1234)
{
score_value += int(1000 / (my.skill10 + 1));
}
my.x = -30000;
wait (1);
my.event = null;
ent_create (explo13_pcx, my.pos, explode_me);
my.passable = on;
my.invisible = on;
ent_playsound (my, hit_wav, 1000);
sleep (2);
ent_remove(my);
}

If the asteroid is hit by player's bullet (its skill40 = 1234, you'll see) we increase the score, adding a bigger score for smaller asteroids, because they can't be shot that easy; the scale is stored in skill10, remember? The asteroid that was hit (it takes 2 for a tango) will be moved to x = -30,000 where it will wait for another asteroid to collide with it; this is how we get those nice explosions at the beginning of the cut scene. The event function is set to null, the explosion sprite is created and an explosion sound is played. We keep the asteroid in the level for 2 more seconds (though it is invisible and passable) because we the hit_wav sound would stop prematurely if we remove the asteroid too quickly. What comes next?

function explode_me()
{
my.oriented = on;
my.flare = on;
my.bright = on;
my.ambient = random(100);
my.passable = on;
my.roll = random(360);
my.skill30 = you.skill10;
while (my.frame < 14)
{
if (my.scale_x < my.skill30)
{
my.scale_x += 5 * time;
my.scale_y = my.scale_x;
}
my.frame += 1 * time;

vec_set (temp.x, camera.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // turn towards the player to make sure that the explo looks great

wait (1);
}
ent_remove (me);
}

That's the function that drives the explosion sprite! The sprite is oriented, passable and has a random ambient value. More than that, it has a random roll angle because we'd like to simulate as many different explosions as we can using a single sprite, in order to save some video memory. We store you.skill10, which is the scale of the asteroid using skill30 from our explosion sprite, and then we play the animated frames, increasing the scale until it matches the one that was used when the asteroid was created. Another vec_to_angle instruction rotates the sprite towards the camera, making sure that the player can see the explosion in its full splendor. The sprite is removed as soon as the last animation frame was played.

Let's see what happens when the player presses the left mouse button (LMB):

function fire_rocket()
{
proc_kill(4);
while (mouse_left == 1) {wait (1);}
player.ammo -= 1;
snd_play (rocket_wav, 50, 0);
ent_create (rocket_mdl, player.pos, move_rocket);
}

The first line of code makes sure that only one instance of the function is running; more than that, we wait until the LMB is released. Player's ammo is decreased, a sound is played and a rocket_mdl file is created at player's position, running the "move_rocket" function:

function move_rocket()
{
my.enable_entity = on;
my.enable_block = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.skill40 = 1234;
my.skill10 = 3; // default explosion scale for the rocket
my.skill20 = 0;
while (my.skill20 < 500)
{
my.skill1 = 500 * time;
my.skill2 = 0;
my.skill3 = 0;
my.skill20 += 1 * time;
ent_move (my.skill1, nullvector);

t emp.x = my.x;
temp.y = my.y;
temp.z = my.z;
effect (particle_trail, 1, temp, normal);
wait (1);
}
remove_rocket();
}

The rocket is sensitive to other entities and level blocks; its event function is named remove_rocket. The rocket gets the same pan and tilt angles with the camera and is identified as a rocket because we set its skill40 to a weird value (1234). We set skill10 (scale of the potential explosion sprite) to 3, and we use skill20 to limit the maximum distance that can be covered by the rocket. The speed of the rocket is given by skill1, and the origin of the particle trail is given by the very coordinates of the rocket.

The speed of the rocket is too big, so we have to use the particle_trail function if we want to see something; take a look at the red laser in the picture below.

function particle_trail()
{
temp.x = 100;
temp.y = 0;
temp.z = 0;
vec_rotate (temp, you.pan);
vec_add (my.vel_x, temp);
my.alpha = 70 + random(30);
my.bmap = ptrail_tga;
my.size = 7;
my.flare = on;
my.bright = on;
my.beam = on; // comment this line if your engine can't use "beam"
my.move = on;
my.lifespan = 30;
my.function = fade_particles;
}

That's your common sense particle function. It has a big speed on x because we want to use "beam" (comment that line if your engine doesn't support it). The particles have the same orientation with their creator (the rocket) and are run by the function named "fade_particles":

function fade_particles()
{
my.alpha -= 100 * time;
if (my.alpha < 0) {my.lifespan = 0;}
}

You can see that these particles are removed quickly; it might be a good idea to decrease "100" if you can't use "beam"; this change might improve the effect. We're coming close to the end now:

function remove_rocket()
{
my.event = null;
ent_playsound (my, destroyed_wav, 1000);
ent_create (explo13_pcx, my.pos, explode_me);
my.invisible = on;
sleep (2);
ent_remove(me);
}

The function above is the event for our rocket; it makes the rocket insensitive to other events, plays a destroyed_wav sound, creates the explosion sprite, and hides the rocket for 2 more seconds, allowing the destroyed_wav sound to be played correctly. The last line of code removes the rocket.

Let's see what happens when the player and one of the asteroids collide:

function player_was_hit()
{
my.event = null; // stop reacting to events for now
my.shield -= you.skill10 * 1.5; // bigger asteroids cause greater damage
my.skill33 = 0;
snd_play (hit_wav, 100, 0);
if (player.shield > 0) // the game isn't over yet?
{
while (my.skill33 < 10)
{
camera.roll = 2 - random(4); // shake the player a bit
my.skill33 += 1 * time;
wait (1);
}
player.roll = 0; // then restore player's roll
}
my.event = player_was_hit;
}

The player stops reacting to events for a while; its "shield" is decreased depending on the size (skill10) of the asteroids. We reset skill33 and then we use it as a counter, shaking the player a bit by choosing a random roll angle (-2...2) every frame, and then we reset player's roll angle. This happens only if "shield" is bigger than zero; the ship won't shake if the player is already dead. The last line of code makes the player sensitive to impact again.

If player.shield <= 0, the ship falls on the ground and explodes; let's see the function that creates smoke particles:

function particle_smoke()
{
my.alpha = 40 + random(50);
my.bmap = smoke_tga;
my.bright = on;
my.flare = on;
my.move = on;
my.beam = on; // if your engine supports it
my.size = 30;
my.function = particle_fade;
}

There isn't too much to talk about this function; don't forget to comment the "beam" line if your engine can't us it. Oh, and we are using the same particle_fade function that was used for the plasma jets. Time to see the last function:

starter compute_score()
{
while (1)
{
str_for_num(temp_str, score_value); // convert the value to string
if (str_len(temp_str) == 1)
{
str_cpy(score_str, "0000000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 2)
{
str_cpy(score_str, "000000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 3)
{
str_cpy(score_str, "00000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 4)
{
str_cpy(score_str, "0000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 5)
{
str_cpy(score_str, "000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 6)
{
str_cpy(score_str, "00");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 7)
{
str_cpy(score_str, "0");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 8)
{
str_cat(score_str, temp_str);
}
wait (1);
}
}

This starter function is pretty big, but I wanted to display the score using one of the old school techniques; a score if 1500 is displayed like this "00001500" instead of "1500". Now you can see why we need to use a string and not a "digit" for the score. We convert the numerical value (score_value) to a string, we get its length and then we add the correct number of "0" digits at its left. That's all!

I think that P.S. has some potential. At this moment I see it as an action / logical / rpg combination, but who knows what can happen with it in the near future? I want to thank Nemisis for his great ideas and his support; the gameplay would have been worse without his help. I'll see you next month!

Grenades

Many of you have asked this question at the forum: where can I find some good grenade throwing code? Look no further, for this article will tell you how to create the code for a grenade that explodes on impact, destroying all the entities in its range. Interested in seeing some chained explosions? I have set up a demo level especially for you!

Let's begin with a starter function:

starter init_grenades()
{
while (player == null) {wait (1);}
while (1)
{
if (mouse_left == 1)
{
ent_create (grenade_mdl, player.x, move_grenade);
while (mouse_left == 1) {wait (1);} // wait until the LMB is released
}
sleep (0.1);
}
}

This function waits until the player is created, and then starts a while (1) loop. Every time we press the left mouse button, a grenade is created and it starts moving as instructed by function move_grenade:

function move_grenade()
{
my.passable = on;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = grenade_event;
while (my != null)
{
if (vec_dist (my.x, player.x) > 50) {my.passable = off;}
my.skill1 = 50 * time; // movement speed
my.skill2 = 0; // speed on y
if (my.skill40 < 100) // covered at least 100 quants?
{
my.skill3 = 0; // if not, don't apply gravity yet
}
else // moved away from the player
{
my.skill3 = -5 * (my.skill40 / 100) * time;
}
move_mode = ignore_you + ignore_passable;
my.skill30 = ent_move (my.skill1, nullvector); // get the distance that was covered this frame in skill30
my.skill40 += my.skill30; // store the distance covered so far in skill40
wait (1);
}
}

This function is similar to the one that was used for the flares in "Beginner's corner"; the grenade is sensitive to impassable entities and to level blocks, and runs its "grenade_event" function as soon as one of these collisions takes place. The grenade will become impassable when the distance between it and the player grows above 50 quants. We apply gravity to the grenade if it has covered at least 100 quants, just like in the picture below; play with "-5" if you want to adjust the gravity. Oh, and we are using skill40 to store the total distance covered by the grenade, skill30 being set to the distance that is covered every frame.

What happens when the grenade hits something? Let's examine its event function:

function grenade_event()
{
vec_set (temp, my.pos);
temp.z += 10; // create the explosion sprite 10 quants above the ground
ent_create(explosion_pcx, temp, animated_explosion);
ent_remove (me);
}

We store the current position of the grenade in temp, we add 10 quants to the z coordinate, and then we create a sprite named explosion_pcx at this position, attaching it the function named animated_explosion. The last line of code removes the grenade.

function animated_explosion()
{
my.passable = on;
my.flare = on;
my.ambient = 100;
ent_playsound (my, explosion_wav, 500);
while (my.frame < 7)
{
my.frame += 1 * time;
my.scale_x += 2 * time;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
player.roll = 2 - random(4); // shake the player a bit
wait (1);
}
player.roll = 0;
my.invisible = on;
temp.pan = 360;
temp.tilt = 180;
temp.z = 400;
scan_entity (my.x, temp);
sleep (2);
ent_remove (me);
}

The explosion sprite is passable, has its flare flag set and its ambient is 100. We play an explosion_wav sound at the impact coordinates, and then we play all the animation frames for our sprite in a while loop.

The same while loop increases the scale for the explosion sprite every frame for a more dramatic effect and shakes the player a bit; in fact, it simply chooses a random value from -2 to 2 for camera.roll. As soon as we get out of the while loop we reset camera.roll, we hide the explosion sprite; we can't remove it yet because the explosion sound associated to it continues to play.

The time has come for us to cause some damage in the area, so we set a scanning range of 360 degrees horizontally, 180 degrees vertically and a range of 400 quants, and then we run scan_entity. This means that every entity that has its enable_scan flag set will trigger its event. The last few lines of code make sure that we wait until the explosion sound has finished, and then the sprite is removed.

How about some entities that can be destroyed by our grenade? See for yourself how simple it is to create them:

action barrel
{
my.enable_scan = on;
my.event = barrel_explodes;
}

These barrels are sensitive to scanning and run their barrel_explodes event function as soon as they are scanned.

function barrel_explodes()
{
my.event = null;
wait (1 + random(20)); // wait up to 20 frames
ent_morph (my, barrel_pcx);
my.flare = on;
my.ambient = 100;
ent_playsound (my, explosion_wav, 200);

The first line sets the event function to null; this way, a barrel can't trigger its event more than once. We wait a random number (1..20) frames because chained explosions look better this way, and then we morph the barrel into an explosion sprite with its flare flag set. We need a bright explosion, so we set the ambient of the sprite to 100. Finally, we play an explosion sound.

while (my.frame < 10)
{
my.frame += 1 * time;
wait (1);
}
my.invisible = on;
temp.pan = 360;
temp.tilt = 180;
temp.z = 150;
scan_entity (my.x, temp);
sleep (2);
ent_remove (me);
}

The explosion sprite plays its animation frames in a while loop, then it becomes invisible and performs a scan on a smaller range (150 quants) because a barrel can (and should) cause some turbulence in the Force ;) by scanning (triggering the events) of the other entities nearby that have their enable_scan set. We wait for 2 more seconds because we want to make sure that the barrel explosion sound has ended, and then we remove the invisible explosion sprite.

Load my office.wmp demo level, throw a grenade and then sit back, grab some popcorn and enjoy the chained barrel explosion show.