Planet Survivors - part 5

This month the action goes on as we learn to create three new enemy types, including a boss at the end of the level. Before I begin, let's see a small list with the changes:

- Player's bullets are destroyed if their x is greater than 700 quants; this way he won't be able to kill the enemies that are far away (he has to wait until they become visible). Check function move_bullet1() inside level2.wdl to see how I did that.

- The level is bigger now (40,000 quants) because we have added the new enemies to it and there wasn't enough room for them.

Let's see the action that drives the first new enemy:

action shuttle2
{
my.ambient = 30;
while (player == null) {wait (1);}
my.z = player.z; // set the same height for the enemy ships and the player
var shuttle_speed;
my.skill10 = 1;
my.skill40 = 4; // I'm a ship
my.enable_impact = on;
my.event = destroy_them;
shuttle_speed.x = -2 * time;
shuttle_speed.y = 0;
shuttle_speed.z = 0;
while ((my.x > -300) && (my.skill10 >= 0))
{
if ((vec_dist (my.x, player.x) < 800) && (total_frames % 90 == 1))
{
if (my.x > player.x) // still got the chance to hit the player?
{
snd_play (fire1_wav, 40, 0);
vec_set (temp.x, my.x);
temp.x -= 30;
temp.y -= 83;
ent_create (bolt_mdl, temp.x, move_bullet2);
vec_set (temp.x, my.x);
temp.x -= 30;
temp.y += 83;
ent_create (bolt_mdl, temp.x, move_bullet2);
}
}
move_mode = ignore_you + ignore_passable;
ent_move (nullvector, shuttle_speed);
wait (1);
}
ent_remove (my);
}

This shuttle waits until the player appears in the level, and then it adjusts its height to player's height, making sure that it (or its bullets) can collide with the player if it gets the chance :). We set skill40 to 4 for this shuttle because we want to give it some sort of id that will be used in its destroy_them event function.

The ship will move until its x goes below -300 quants (the player can't see it anymore) if its skill10 is bigger than or equal to zero. We will need to fire 2 shots if we want to destroy one of these shuttles. If the ships are closer than 800 quants to the player, the ship could start firing every 1.5 seconds because the frame rate is set to 60 fps and we are checking total_frames % 90, which would happen every 90 / 60 = 1.5 seconds. I said that the ship "could" start firing because it will fire only if its x is bigger than player's x (its position is above player's position). If this is correct, we play the fire1_wav sound, we set a temporary position for the first laser bolt, we create it, and then we set a temporary position for the second laser bolt and we create it. These bolts are driven by the move_bullet2() function. Oh, and before I show you that function, note that the shuttles move ignoring the passable entities, and get removed if their x goes below -300 or if they are hit twice.

function move_bullet2()
{
var bullet_speed;
my.passable = on;
my.enable_impact = on;
my.enable_entity = on;
my.event = destroy_bullet;
my.pan = you.pan;
bullet_speed.x = 50 * time;
bullet_speed.y = 0;
bullet_speed.z = 0;
my.ambient = 100;
my.bright = on;
my.light = on;
my.red = 50;
my.green = 255;
my.blue = 50;
while (my != null) // as long as the bullet exists
{
if (you != null)
{
if(vec_dist (my.x, you.x) > 50)
{
my.passable = off;
}
}
my.roll += 200 * time;
move_mode = ignore_you + ignore_passable;
ent_move (bullet_speed, nullvector);
wait (1);
}
}

Please note that the bullet is passable at first, and becomes impassable only when the distance between it and its creator (the shuttle) grows above 50 quants. The bolts are rotating with the speed given by 200 * time; play with that value if you'd like to set a different rotation speed.

The second ship is a bit more interesting: it creates a sphere of particles that grows over and over and destroys all the entities (player, ships, asteroids) in the area. Let's examine its code:

action shuttle3
{
my.ambient = 30;
while (player == null) {wait (1);}
ent_create (sphere_mdl, my.x, move_sphere);
my.z = player.z; // set the same height for the enemy ships and the player
var shuttle_speed;
my.skill10 = 1;
my.skill40 = 1; // I'm a ship
my.enable_impact = on;
my.event = destroy_them;
shuttle_speed.x = -2 * time;
shuttle_speed.y = 0;
shuttle_speed.z = 0;
while ((my.x > -300) && (my.skill10 == 1))
{
move_mode = ignore_you + ignore_passable;
ent_move (nullvector, shuttle_speed);
wait (1);
}
ent_remove (my);
}

The second shuttle creates a sphere_mdl entity at its position and assigns it the move_sphere function. The ship will move until its x goes below -300, if it manages to live that long (if my.skill10 continues to remain set to 1).

function move_sphere()
{
proc_late();
var index;
var vertices;
my.passable = on;
my.invisible = on;
vertices = ent_vertices(my);
while (you != null)
{
vec_set (my.x, you.x);
if (you.x < 800)
{
if (my.scale_x == 1)
{
snd_play (scanning_wav, 60, 0); // this sound will be played only once
}
my.scale_x += 1 * time;
my.scale_x = min(30, my.scale_x); // limit the scale to 30
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
temp.pan = 360;
temp.tilt = 360;
temp.z = my.scale_x * 37; // 37 = experimental value, must fit the size of the particle sphere
scan_entity (camera.x, temp);
}

The sphere is invisible and passable; we get its number of vertices, and then we use a while loop that will keep running for as long as the shuttle (you) exists. The loop sets the same coordinates for the ship and for the invisible sphere. If the ship has its x below 800 quants, if the scale of the sphere is 1 (which happens only once) the creepy scanning_wav sound is played. The sphere increases its scale until it reaches 30, and performs scan_entity instructions every frame, using an increased range every frame. You will need to play with 37 - it's an experimental value, and it must fit the size of the particle sphere.

index = 0;
while (index < vertices) // while without wait!
{
vec_for_vertex (temp, my, index);
effect (particle_effect3, 10, temp, normal);
index += 1;
}
my.pan += 3 * time;
my.tilt += 3 * time;
my.roll += 3 * time;
wait (1);
}
ent_remove (my);
}

The visual effect is created by the lines above: we use a fast while loop (without wait!) to pass through all the vertices of the sphere, and we generate particles using the particle_effect function. All this time, the sphere rotates by changing its pan, tilt and roll angles.

function particle_effect3()
{
temp.x = 5 - random (10);
temp.y = 0;
temp.z = 0;
vec_add (my.vel_x, temp);
my.bmap = glow_tga;
my.alpha = 80;
my.size = 5;
my.bright = on;
my.move = on;
my.lifespan = 1;
my.function = destroy_particles;
}

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

There isn't anything special here, really. The particles are destroyed as soon as their alpha goes below zero.

Ready to meet the boss at the end of the level?

action boss_level2
{
my.ambient = -30;
while (player == null) {wait (1);}
my.z = player.z; // set the same height for the enemy ships and the player
var boss_speed;
my.skill10 = 1;
my.skill20 = 100;
my.skill40 = 3; // I'm the boss
boss_speed.x = -2 * time;
boss_speed.y = 0;
boss_speed.z = 0;
while (my.x > 650)
{
move_mode = ignore_you + ignore_passable;
ent_move (nullvector, boss_speed);
wait (1);
}
my.enable_impact = on;
my.event = destroy_them;
boss_speed.x = 0;
boss_speed.z = 0;
while (my.skill20 > 0)
{
if (player.y > my.y)
{
my.y += 5 * time;
}
else
{
my.y -= 5 * time;
}

The boss will start moving towards the player until its x is smaller than 650 quants, and then it will stop, becoming sensitive to impact with player's bullets. The boss will try to set its y to player's y, moving to the left or to the right depending on the position of the player.

if (((my.y - player.y) < 200) && (total_frames % 40 == 1))
{
if (player.invisible == on) {break;}
vec_set (temp.x, my.x);
temp.x -= 120;
snd_play (fire1_wav, 40, 0);
ent_create (laserboss_mdl, temp.x, move_bullet3);
wait (1);
vec_set (temp.x, my.x);
temp.x -= 40;
temp.y += 145;
snd_play (fire1_wav, 40, 0);
ent_create (laserboss_mdl, temp.x, move_bullet3);
wait (1);
vec_set (temp.x, my.x);
temp.x -= 40;
temp.y -= 145;
snd_play (fire1_wav, 40, 0);
ent_create (laserboss_mdl, temp.x, move_bullet3);
}
wait (1);
}
ent_remove (my);

If the boss is pretty close to the player on the y axis, if will start firing every 40 frames, which makes about 40 / 60 = 0.6 seconds. If the player is invisible (it was destroyed) the boss will get out of the loop, removing itself from the level. If the player isn't dead yet, the boss will set 3 firing spots (corresponding to its 3 weapons) and will create 3 laserboss_mdl entities, attaching them the move_bullet3 function.

sleep (3);
my = null;
level_load (level3_wmb);
wait (3);
camera.pan = 0; // set the correct camera position and angles
camera.tilt = -90;
camera.roll = 0;
camera.x = 330;
camera.y = 0;
camera.z = 1000;
}
The boss was destroyed here, so that's the end of the level. We wait for 3 more seconds, and then we load the next level (you'll see it next month), we wait until it is loaded and then we set s new position and new angles for the camera. Let's get back to the function that drives the bullets fired by the boss:

function move_bullet3() // used by the boss at the end of the level
{
var bullet_speed;
my.passable = on;
my.enable_impact = on;
my.enable_entity = on;
my.event = destroy_bullet;
my.pan = you.pan;
bullet_speed.x = 35 * time;
bullet_speed.y = 0;
bullet_speed.z = 0;
my.ambient = 100;
my.bright = on;
my.light = on;
my.red = 50;
my.green = 255;
my.blue = 50;
while (my != null) // as long as the bullet exists
{
if (you != null)
{
if(vec_dist (my.x, you.x) > 50)
{
my.passable = off;
}
}
if (vec_dist(player.x, my.x) > 400) // not too close to the player?
{
vec_set(temp, player.x); // rotate the bullet towards player's ship
vec_sub(temp, my.x);
vec_to_angle(my.pan, temp);
}
move_mode = ignore_you + ignore_passable;
ent_move (bullet_speed, nullvector);
wait (1);
}
}

The only part of the code that needs explanations is the while loop, which will run as long as the bullet exists. If the creator of the bullet (the boss) is alive and the distance between the boss and the bullet is smaller than 50 quants, the bullet is passable; otherwise, it becomes impassable. If the bullets aren't too close to the player (must be more than 400 quants away) they will rotate towards player's ship, which turns them into heat seeking missiles and makes the job harder for the player.

function destroy_them()
{
snd_play (explo1_wav, 50, 0);
if (my.skill40 == 1) // a ship was hit?
{
my.skill10 = 0;
if (you.skill40 == 99) // if it was player's bullet
{
score_value += 100; // get 100 points for a ship
}
effect(particles_explo2, 30, my.x, nullvector);
}
if (my.skill40 == 2) // hit one of the asteriods?
{
my.skill10 = 0;
if (you.skill40 == 99) // if it was player's bullet
{
score_value += 50; // get 50 points for an asteroid
}
effect(particles_explo1, 20, my.x, nullvector);
}

The function that destroys them all has changed quite a bit, so I have decided to show it to you again. If skill40 is set to 1, the player has hit a ship, so skill10 is set to zero, destroyed the ship. We check if the ship was hit by one of player's bullets (you.skill40 = 99) and only if this is true we add 100 points to the score. Finally, we generate a particle effect for the explosion. The same thing happens if we hit the asteroids; the player won't get points if the asteroids are shot by one of the enemy ships.

if (my.skill40 == 3) // hit the boss at the end of the level?
{
my.skill10 = 0;
if (you.skill40 == 99) // if it was player's bullet
{
score_value += 100; // get 100 points for a shot
}
my.skill20 -= 10; // decrease the shield for the boss
if (my.skill20 <= 0)
{
snd_play (bossexplo_wav, 100, 0);
effect(particles_explo3, 100, my.x, nullvector); // similar to the other particle explosions
}
}
if (my.skill40 == 4) // shuttle2 was hit?
{
my.skill10 -= 1;
if (my.skill10 < 0)
{
if (you.skill40 == 99) // if it was player's bullet
{
score_value += 200; // get 200 points for shuttle2
}
effect(particles_explo2, 50, my.x, nullvector);
}
}
}

If the player has hit the boss at the end of the level (skill40 = 3) we will get 100 points for every shot because the boss is destroyed only if the player manages to hit it 10 times. Finally, if the player manages to destroy one of the shuttle2 ships, it gets 200 points because these ships need to be hit twice.

action player_level3
{
while (1)
{
my.roll += 4 * time;
wait (1);
}
}

The action above demonstrates that the 3rd level is loaded when we kill the boss at the end of the level; don't worry, we'll have a more useful action for the player next month, in a fresh and (hopefully) exciting environment.

Deadly spiders

These spooky creatures will move around in your level, avoiding the obstacles and biting any entity that collides with them (excluding their own kind, of course).

action deadly_spider
{
var spider_speed;
var in_front;
my.enable_entity = on;
my.enable_impact = on;
my.event = hurt_them;
while(1)
{
spider_speed.x = 8 * time;
spider_speed.y = 0;
spider_speed.z = 0;
spider_speed *= time;
in_front.x = my.x + 40 * cos(my.pan);
in_front.y = my.y + 40 * sin(my.pan);
in_front.z = my.z;
if (content(in_front) == content_solid)
{
my.skill40 = my.pan;
my.skill41 = 30 + random(90);
while (my.pan < my.skill40 + my.skill41) // rotate 30..120 degrees
{
my.pan += 5 * time;
wait (1);
}
}

The spiders are sensitive to other entities; their event is function hurt_them. The movement takes place inside a while(1) loop, with the movement speed given by spider_speed.x. I have defined a local variable named in_front, and I have set its coordinates 40 quants in front of the player; if the content of in_front is solid (a wall or a map entity), we store the initial pan angle of the spider and then we add a random angle (30...120 degrees) to it using a loop that increases the value slowly. This trick will (hopefully) rotate the spider in a direction that isn't blocked by obstacles.

else // free to move
{
ent_cycle("run", my.skill20); // play the "run" animation
my.skill20 += 10 * time;
my.skill20 %= 100; // loop
move_mode = ignore_passable; // ignore passable entities
result = ent_move (spider_speed, nullvector);
if (result == 0) // got stuck?
{
spider_speed.x *= -1; // then reverse the movement direction
my.skill40 = my.pan;
my.skill41 = 30 + random(90);
while (my.pan < my.skill40 + my.skill41) // rotate 30..120 degrees
{
my.pan += 5 * time;
ent_cycle("run", my.skill20); // play reversed "run" animation
my.skill20 -= 10 * time;
my.skill20 %= 100; // loop
move_mode = ignore_passable; // ignore passable entities
ent_move (spider_speed, nullvector);
wait (1);
}
spider_speed.x *= -1; // restore the initial speed
}
}
wait (1);
}
wait (1);
}

If the spider can move, it plays its "run" animation, ignoring the passable entities that it might encounter on its way. We store the result of its movement in "result" and then we check its value; if the spider got stuck, "result" is set to zero, so we multiply spider_speed.x by -1, which reverses its movement direction. We generate another random angle that will be added to the initial pan angle, and then we move the spider backwards, using its reversed "run" animation and ignoring the passable entities. We multiply spider_speed.x by -1 again at the end of the loop, and this restores the initial speed.

The last thing that needs to be discussed is the event function:

function hurt_them()
{
proc_kill(4);
if (you != null) // collided with an entity (the player, a monster, etc)?
{
snd_play (gotcha_wav, 50, 0);
you.skill9 -= 10; // if "health" for that entity is stored in its skill9
}
}

This function will run when the spider collides with something. If it collides with an entity (you != null) we play the gotcha_wav sound, and then we decrease skill9 for the entity that was bitten. If your player or enemies use another skill for storing their health you should change it as needed. Action player_walk_fight uses skill9 for health, so your player will see its death in my demo level.