Planet Survivors

This month the player crashes its ship and is forced to land on a new planet, jumps out of the ship, gets some primitive movement code (so that you can explore the level) and a gun with unlimited ammo (for now). The level was created by Nemisis, my P.S. partner :) and you will find all the maps and wads for it inside the archive. Let's examine the new actions and functions inside the level2.wdl file.

function ship_explosion(coordinates)
{
    var number_of_gibbits;
    effect (explosion_core, 10 + random(10), coordinates, nullvector);
    sleep (0.3);
    ent_create (explosion_pcx, coordinates, explosion_sprite2);
    sleep (0.1);
    number_of_gibbits = random(15) + 5;
    while (number_of_gibbits > 0) // while without wait!
    {
       ent_create (gibbit_mdl, coordinates, move_gibbits);
       number_of_gibbits -= 1;
    }
}

The player will run into an asteroid field and this time the metallic asteroids can't be destroyed; therefore, player's ship will be severely damaged and you will see a nice looking explosion effect. I won't tell you how it works here; I use the same effect in the "Advance explosions" article at the bottom of this page and you'll learn how to create even better looking effects if you read that article.

action metal_asteroid // unbreakable asteroids
{
    while (first_boss != null) {wait (1);}
    sleep (3);
    my.x -= 33000;
    my.scale_x = 2 + random(5); // set a random scale on x, y, z
    my.scale_y = 2 + random(5);
    my.scale_z = 2 + random(5);
    asteroid_speed.x = -15 * time; // the metal asteroids have a big speed
    asteroid_speed.y = 0;
    asteroid_speed.z = 0;
    my.skill1 = 4 - random(8); // set random rotation (pan, tilt, roll) speeds
    my.skill2 = 4 - random(8);
    my .skill3 = 4 - random(8);
    while (my.x > -300)
    {
        move_mode = ignore_you + ignore_passable;
        ent_move (nullvector, asteroid_speed);
        my.pan += my.skill1 * time;
        my.tilt += my.skill2 * time;
        my.roll += my.skill3 * time;
        wait (1);

These asteroids will wait until the first boss (the big ship at the end of the level) is destroyed; you should comment this line if you want to jump straight to the end of the level. When the boss is destroyed the asteroids will come closer to player's ship by 33,000 quants because they were standing still and they need to cover a lot of space quickly. We set a random scale (2...7) on the x, y and z axis for each asteroid, and then we set their speed on the x axis. We choose a random rotation speed (-4...4) for the pan, tilt and roll axis and then we start moving the asteroids inside the "while" loop.

        if ((vec_dist (player.x, my.x) < 100) && (ship_down == 0))
        {
            ship_down = 1;
            players_shield = 0;
            ship_explosion (vector(player.x, player.y, player.z + 20));
            snd_play (explostart_wav, 100, 0);
            while (player.z > -10000)
            {
                 player.z -= 100 * time;
                 player.roll += 25 * time;
                 wait (1);
            }
            level_load (level3_wmb);
        }
        if (ship_down == 1)
        {
             my.passable = on;
        }
     }
    ent_remove (my); // remove the asteroids as soon as they can't be seen
}

If the player has come closer than 100 quants to one of the asteroids and this has happened for the first time, the ship will be brought down and players_shield will be set to zero. This allows us to take over the control of the ship, so the player won't be able to move it. We create an explosion 20 quants above the ship, we play a sound, and then we start to move the ship downwards, changing its roll angle at the same time. The next level (level3_wmb) will be loaded as soon as the height of the ship is below -10,000 quants. All the asteroids will become passable if the ship was brought down; this way they won't get stuck into each other even if the distance between them is really small. The last line of code removes the asteroids if they can't be seen anymore (if their x is smaller than -300 quants).

action halo_flare
{
    my.ambient = 100;
    my.transparent = on;
    my.bright = on;
    my.light = on;
    my.lightred = 255;
    my.lightgreen = 255;
    my.lightblue = 255;
    my.lightrange = 0;
    my.facing = on;
    my.passable = on;
    while(1)
    {
        my.alpha = min(100, vec_dist(my.x, camera.x) / 20);
        my.scale_x = min(3, my.alpha / 50);
        my.scale_y = my.scale_x;
        wait(1);
    }
}

action crystal_glow
{
    my.transparent = on;
    my.bright = on;
    my.ambient = 100;
    my.light = on;
    my.lightred = 255;
    my.lightgreen = 255;
    my.lightblue = 100;
    my.lightrange = 0;
}

These two actions control the light flare sprites and the light bulbs that can be observed in the first picture from this page. I have used passable sprites that are illuminated by pure white light (RGB = 255 255 255) and face the camera all the time for the flares. We get the distance to the camera inside a "while" loop and we compute the transparency factor depending on it. The scale for these flares can increase up to 3 times. The crystal light bulbs have a yellowish glow.

starter level3_camera()
{
    while (level_number != 3) {wait (1);}
    while (player == null) {wait (1);}
    camera.x = 20000;
    camera.y = 9600;
    camera.z = -500;
    camera.pan = 110;
    camera.tilt = 0;
    camera.roll = 0;
}

The lines of code above set the initial camera position for the 3rd level. I have decided to use a "starter" function, so it will start to run as soon as we run the game; therefore, I am using two "while" loops that make it wait until we get to the 3rd level and until player's ship is loaded. The rest of the lines set a convenient position and angles for the camera.

action player_level3
{
    player = my;
    my.ambient = -50;
    level_number = 3;
    my.scale_x = 5;
    my.scale_y = my.scale_x;
    my.scale_z = my.scale_x;
    my.x = 10000;
    my.y = 28000;
    my.z = 8000;
    my.pan = 295; // and angles
    my.tilt = -50;
    my.skill1 = 15000;
    while (my.skill1 > 100)
    {
       vec_set (temp, my.x);
       temp.z -= 15000;
       trace_mode = ignore_me + ignore_you + ignore_passable;
       my.skill1 = trace (my.x, temp);
       my.roll += 20 * time;
       my.x += 70 * time;
       my.y -= 125 * time;
       my.z -= 120 * time;
       effect (particle_smoke, random(10) + 5, my.x, nullvector);
       wait (1);
    }

This action is attached to the falling ship in the 3rd level. We set a convenient position and angles for the ship, we set its skill1 to a huge value (15,000) and then we move it downwards until the distance between it and the terrain goes below 100 quants. This is a simple, effective method that allows us to land the ship in a certain area without using predefined paths or other methods that could work badly because of the big movement speeds. We rotate the ship by increasing its roll angle and we generate smoke using the particle_smoke function from explosions.wdl; read the "Advanced Explosions" article for more information.

   snd_play (explomid_wav, 20, 0);
   my.skill1 = 0;
   while (my.skill1 < 100)
   {
       my.skill1 += 10 * time;
       camera.roll = 2 - random(4);
       wait (1);
   }
   sleep (3);
   ent_create (myhero_mdl, my.x, move_player);
   while (player_1st == null) {wait (1);}
   while (vec_dist (player_1st.x, my.x) < 2000)
   {
       effect (particle_smoke, random(3) + 2, vector(my.x, my.y + 80, my.z + 200), nullvector);
       wait (1);
   }
}

The ship has landed, so we play an explomid_wav sound and then we reset skill1, because we will use it as a counter inside a loop that shakes the camera a bit by changing its roll angle every frame. We wait for 3 more seconds, we create the 1st person player model and we attach it the "move_player" function. We have to wait until the player appears in the level, and then we create thinner smoke until the player has moved at least 2000 quants away from the ship.

Let's examine the code that moves the player:

function move_player()
{
    var players_speed;
    var track2_playing = 0;
    player_1st = my;
    player = null;
    my.invisible = on;
    players_shield = 25;
    players_ammo = 30;
    my.x = 14830;
    my.y = 19420;
    my.z = -80;
    my.pan = 300;
    my.tilt = -50;
    sleep (1);
    camera.x = my.x;
    camera.y = my.y;
    camera.z = my.z;
    camera.pan = my.pan;
    camera.tilt = my.tilt;
    snd_loop (wind3_wav, 15, 0);
    snd_play (smoked_wav, 100, 0);
    sleep (3);

Our player has changed from a ship to a human, right? We will use the new player_1st pointer for the player from now on. The player is invisible (we don't need to see it in 1st person mode) and has 25 shield units and 30 bullets; he was injured during the landing but I bet that he'll find some health packs next month :). We choose a convenient position and angle for the player, close to the top of the ship, because we want to give me (and you) the illusion that the player will get out of the ship. This position and angles (copied to the camera) will hopefully trick us that we are in a smoky cockpit.

We start to play the wind3_wav sound in a loop using the snd_loop instruction that loops a sound perfectly, and then we play a "cough cough" sound that lasts for a few seconds.

   while (my.x < 15000)
   {
       my.x += 20 * time;
       my.y -= 40 * time;
       my.z -= 25 * time;
       my.tilt += 2 * time;
       camera.x = my.x;
       camera.y = my.y;
       camera.z = my.z;
       camera.tilt = my.tilt;
       wait (1);
    }
   snd_play (jump_wav, 80, 0);
   while (my.tilt < 0)
   {
       my.tilt += 3 * time;
       camera.tilt = my.tilt;
       wait (1);
   }
   while (players_shield > 0)
   {
       players_speed.x = 15 * (key_w - key_s) * time;
       players_speed.y = 10 * (key_a - key_d) * time;
       vec_set (temp, my.x);
       temp.z -= 5000;
       trace_mode = ignore_me + ignore_passable + use_box;
       players_speed.z = -trace (my.x, temp);
       my.pan -= 5 * mouse_force.x;
       my.tilt += 5 * mouse_force.y;

The player will jump on the ground by decreasing its z; we play the jump_wav sound and then we increase the tilt angle until the camera looks good. As long as the player is alive, we can move it using the "W", "S", "A", "D" keys and we keep it on the ground by tracing 5,000 quants below its feet and adjusting the height accordingly. Finally, we can look around using the mouse.

       if (mouse_left == 1)
       {
           players_bullet(); // then fire a bullet!
       }
       move_mode = ignore_passable + ignore_passents + glide;
       ent_move(players_speed, nullvector);
       camera.x = my.x;
       camera.y = my.y;
       camera.z = my.z + 30;
       camera.pan = my.pan;
       camera.tilt = my.tilt;
       if (((my.z < -750) || (mouse_left == 1)) && (track2_playing == 0))
       {
           track2_playing = 1;
           media_loop("track2.wav", null, 100);
       }
       wait (1);
    }
   my.skill46 = 0;
   while (my.skill46 < 80)
   {
       ent_cycle("death", my.skill46);
       my.skill46 += 2 * time;
       wait (1);
   }
}

If the player has pressed the left mouse button (LMB) we run the function named "players_bullet". The camera will be placed 30 quants above player's origin and will use the same pan and tilt values with the player. If the height of the player goes below -750 quants (the player has come close to the first base) or if the player fires a bullet, we start playing the "track2.wav" soundtrack in a loop. The lines at the end of the action will run only if the player has died, which isn't the case for now; they simply play the "death" animation in a loop.

function players_bullet()
{
    proc_kill(4);
    while (mouse_left == 1) {wait (1);}
    snd_play (bullet_wav, 50, 0);
    ent_create (pbullet_mdl, player_1st.x, move_players_bullet); // create player's bullet
}

The function above runs every time we press the LMB. We make sure that only one instance of the function is running, and then we wait until the player has released the LMB. We play a bullet_wav sound, and then we create the bullet model, attaching it the "move_players_bullet" function.

function move_players_bullet()
{
   my.pan = you.pan; // the bullet and the player have the same pan angle
   my.tilt = you.tilt; // and tilt angle
   my.passable = on; // at first
   my.enable_entity = on; // the bullet is sensitive to other entities
   my.enable_impact = on;
   my.enable_block = on; // and to level blocks
   my.event = remove_bullet;
   my.ambient = 100;
   my.light = on;
   my.lightred = 150;
   my.lightgreen = 150;
   my.lightblue = 255;
   while (my != null)
   {
      if (vec_dist (player_1st.x, my.x) > 30) {my.passable = off;}
      my.skill1 = 100 * time; // bullet speed
      my.skill2 = 0;
      my.skill3 = 0;
      move_mode = ignore_you + ignore_passable;
      ent_move (my.skill1, nullvector);
      my.roll += 100 * time;
      wait (1);
   }
}

The bullet will have the same pan and tilt angles with its creator (the player). We will make the bullet passable at first, though it is sensitive to impact with other entities or level blocks. Its event function is named "remove_bullet()". The bullet will move using a "while" loop, becoming impassable as long as the distance between it and the player is over 30 quants. Oh, and the bullet rotates by changing its "roll" angle.

function remove_bullet()
{
   my.event = null;
   ent_playsound (my, phit_wav, 1000); // player's bullet hits something
   sleep (0.4);
   ent_remove (me);
}

This is the event function for the bullet: it sets its event to null, plays a sound at the impact position, waits a bit and then removes the bullet for good.

function toggle_passable()
{
    player.passable = (player.passable == off);
    if (player.passable == on)
    {
        player.enable_scan = off;
        player.transparent = on;
        player.alpha = 30;
    }
    else
    {
        player.enable_scan = on;
        player.transparent = off;
        player.alpha = 100;
    }
}

on_t = toggle_passable;

This function allows you to make player's ship invincible during the 2nd level, allowing you to get to the 3rd level without problems. Press the "T" keys as soon as the level is loaded and the player model will become passable, so all the enemy ships, bullets, as well as the asteroids will pass through it. Let's not forget that some of the enemies use "scan_entity" to hurt the player, so we have to disable player's enable_scan too. You will notice that the player is also transparent if you have made it transparent; Press "T" again to make the player vulnerable / opaque.

 

Advanced explosion effects

I'd like to mention from the very beginning that the harder you will work, the nicer your effects will look. However, this explosion effect consists of some simple, overlapping particle and sprite effects:

1) The explosion core:

function explosion_core()
{
    if (random(1) > 0.5)
    {
      my.bmap = core1_tga;
    }
    else
    {
      my.bmap = core2_tga;
    }
    my.transparent = on;
    my.flare = on;
    my.bright = on;
    my.size = random(30) + 20;
    my.alpha = 100;
    my.function = core_fade;
}

function core_fade()
{
   if (my.alpha > 50)
   {
      my.size += 2 * time;
   }
   else
   {
      my.size -= 2 * time;
   }
   my.alpha -= 3 * time;
   if(my.alpha < 0)
   {
      my.lifespan = 0;
   }
}

The first function picks one of the two bitmaps (core1_tga or core2_tga), makes it transparent, bright and so on, and then it gives it a random size. The second function will increase the bitmap size until its alpha goes below 50, and then it will start decreasing it. The particles will be removed if their alpha is below zero.

 

2) The sprite ring (shockwave, whatever):

function explosion_ring()
{
    my.passable = on;
    my.scale_x = 0.1;
    my.scale_y = 0.1;
    my.tilt = 90;
    my.oriented = on;
    my.bright = on;
    my.flare = on;
    my.transparent = on;
    my.alpha = 100;
    my.ambient = 100;
    my.lightred = 255;
    my.lightgreen = 200;
    my.lightrange = 100;
    while (my.scale_x < 4)
    {
       my.alpha -= 10 * time;
       my.lightrange += 50 * time;
       my.scale_x += 0.5 * time;
       my.scale_y = my.scale_x;
       wait(1);
    }
    ent_remove(my);
}

The sprite starts with a small scale and light up an area of 100 quants at first; the "while" loop will decrease its transparency factor and will increase its scale and lightrange. The ring is removed as soon as its scale is equal to or bigger than 4.

 

3) The rotating sprite:

function explosion_sprite1()
{
    my.passable = on;
    my.facing = on;
    my.bright = on;
    my.flare = on;
    my.alpha = 100;
    my.ambient = 100;
    while (my.scale_x < 4)
    {
       my.scale_x += 1 * time;
       my.scale_y = my.scale_x;
       my.alpha -= 10 * time;
       my.roll += 25 * time;
       wait(1);
    }
    ent_remove(my);
}

This sprite will face the camera all the time. We increase its scale in a "while" loop, decreasing its alpha and rotating it at the same time. When its scale is bigger than 4, the sprite is removed.

 

4) The animated sprite:

function explosion_sprite2()
{
    my.passable = on;
    my.facing = on;
    my.bright = on;
    my.flare = on;
    my.ambient = 100;
    my.red = 255;
    my.lightrange = 100;
    while (my.frame < 20)
    {
       my.frame += 1 * time;
       my.scale_x += 0.1 * time;
       my.scale_y = my.scale_x;
       my.lightrange = 300 - random(100);
       wait(1);
    }
    ent_remove(my);
}

This sprite will go through all its animation frames, increasing its scale and having a random lightrange. The sprite is removed as soon as it has played the last animation frame.

 

5) The gibbit generator and its particle trails:

function move_gibbits()
{
    var gib_coords;
    var gib_speed;
    my.passable = on;
    my.scale_x = 0.5 + random(1);
    my.scale_y = my.scale_x;
    my.scale_z = my.scale_x;
    vec_set (gib_coords, my.x);
    gib_speed.x = (10 - random(20)) * time;
    gib_speed.y = (10 - random(20)) * time;
    gib_speed.z = (20 + random(10)) * time;
    my.skill10 = 10;
    while (my.skill10 > 0)
    {
       if (vec_dist (my.x, gib_coords.x) > 70)
       {
          my.passable = off;
       }
       ent_move (nullvector, gib_speed);
       effect (debris_trail, 1, my.x, nullvector);
       if (bounce.z != 0)
       {
          gib_speed.z = -(gib_speed.z * min(0.8, random(1)));
          if (gib_speed.z < 0.1)
          {
              gib_speed.x = 0;
              gib_speed.y = 0;
              gib_speed.z = 0;
          }
       }
       gib_speed.z -= 2 * time;
       my.skill10 -= 0.3 * time;
       wait (1);
    }
    my.transparent = on;
    my.alpha = 100;
    while (my.alpha > 0)
    {
       my.alpha -= 3 * time;
       wait (1);
    }
    ent_remove (my);
}

The gibbets are small 3D models with a random size (0.5 ... 1.5) and random speeds on the x, y and z axis. Please note that the speed on the z axis is positive, so the giblet will fly upwards at the moment of creation. We set skill10 to 10 and then we run a while loop that will stop when skill10 goes below zero. What happens inside the while loop? First of all, the giblet becomes impassable if it has managed to get away from its starting point; this trick prevents the giblets from getting stuck into each other. Play with "70" - you'll find better values for your own projects. The giblet moves with the speed given by gib_speed and creates particles at its position every frame using the debris_trail function. If the giblet hits something (the ground, a wall, and so on) it changes its speed, picking a smaller value (up to 0.8 from the initial value) and a changed sign. If gib_speed is too small, gib_speed is reset. The last few lines inside the loop decrease gib_speed.z every frame, simulating the effect of the gravity and decrease skill10 because the loop must end at a certain moment. What happens next? The giblet model is made transparent and fades away, being removed when its alpha goes below zero.

function debris_trail()
{
   my.bmap = debristrail_tga;
   my.flare = on;
   my.bright = on;
   my.size = 2;
   my.alpha = 30;
   my.function = fade_debtrail;
}

function fade_debtrail()
{
   my.size += 0.8 * time;
   my.alpha -= 1.5 * time;
   if (my.alpha < 0)
   {
      my.lifespan = 0;
   }
}

These particles simply increase their size and decrease their alpha, being removed as soon as their transparency factor goes below zero.

 

6) The smoke:

function particle_smoke()
{
    my.bmap = smoke_tga;
    my.vel_x = 2 - random(4);
    my.vel_y = 2 - random(4);
    my.vel_z = 2 + random(1);
    my.lifespan = 200;
    my.flare = on;
    my.transparent = on;
    my.alpha = 50;
    my.move = on;
    my.size = 50;
    my.function = fade_smoke;
}

function fade_smoke()
{
   my.size += 0.5 * time;
   my.alpha -= 0.4 * time;
   if (my.alpha < 0)
   {
      my.lifespan = 0;
   }
}

That's your typical smoke effect, with random speeds on the x and y axis and a positive speed on the z axis. The particle increases its size as it moves upwards, decreasing its transparency.

These are all the functions that are being used for the effect, so let's see how they are assembled together:

function start_explosion(coordinates)
{
    var sound_volume;
    var number_of_gibbits;
    sound_volume = 30000 / (vec_dist(coordinates.x, camera.x) + 100);
    snd_play (explostart_wav, sound_volume, 0);
    effect (explosion_core, 10 + random(10), coordinates, nullvector);
    sleep (0.5);
    ent_create (exploring_tga, coordinates, explosion_ring);
    snd_play (explomid_wav, sound_volume, 0);
    sleep (0.1);
    ent_create (exploflare_pcx, coordinates, explosion_sprite1);
    sleep (0.1);
    ent_create (explosion_pcx, coordinates, explosion_sprite2);
    sleep (0.1);
    number_of_gibbits = random(15) + 5;
    while (number_of_gibbits > 0) // while without wait!
    {
       ent_create (gibbit_mdl, coordinates, move_gibbits);
       number_of_gibbits -= 1;
    }
    snd_play (exploend_wav, sound_volume, 0);
    sleep (0.5);
    effect (particle_smoke, random(20) + 10, coordinates, nullvector);
}

I have computed the value for sound_volume using the distance between the coordinates of the explosion and the camera; this way you will be able to play the explosion sounds anywhere in the level (not necessarily at an entity's position). We start by running the explosion_core function, with a random (10...20) number of particles, at the position given by "coordinates", which is the vector that is passed to the start_explosion() function. We wait for 0.5 seconds, and then we create the explosion ring. We wait 0.1 seconds, we create the rotating star effect, we wait another 0.1 seconds and then we create the animate sprite effect. We generate a random (15...20) number of gibbits and we create them inside a fast while loop. Finally, we wait 0.5 seconds and we create the smoke.

Feel free to experiment by changing the order, adding several identical lines that create the same effect, playing with the sleep intervals and so on. I'll see you soon!

Hold on, mister! Aren't you going to show us how to use this great effect in our projects?

Ok, let's take a look at 2 examples:

1) Create the explosion in a specific area of your level:

function explo_test()
{
    start_explosion (vector(0, 0, 100));
}

on_e = explo_test;

When you press the "E" key you will generate an explosion at x = 0, y = 0, z = 100 quants in your level. Simply call explo_test() when you need it to trigger the explosion.

2) Create exploding barrels:

function destroy_me()
{
    start_explosion (vector(my.x, my.y, my.z + 20));
    sleep (0.5);
    my.invisible = on;
    my.passable = on;
}

action explodable_barrel
{
   my.enable_impact = on;
   my.enable_entity = on;
   my.enable_shoot = on;
   my.event = destroy_me;
}

The barrel is sensitive to impact with other entities as well as to "trace" and "c_trace" instructions; its event function triggers the explosion at barrel's position (ok, 20 quants higher) waits a bit and then hides the barrel. You will find both examples in the included office.wmp file, so feel free to experiment with them. Don't forget that if you run out of ammo you can run into the barrels to destroy them! :)