Beaming the player from one place to the other is really easy - all you need to do is change its coordinates and sometimes its angle. The snippet below allows you to set the destination points and angle in Wed so you don't need to change these coordinates in the C-script file every time you set a new destination.
The action attached to the teleport entity is:
action
teleport
{
while (player == null) {wait(1);}
vec_set (my.skill15, strength);
vec_set (my.skill18, astrength);
person_3rd = 0.5;
camera_dist.x = 200;
camera_dist.z = -300;
head_angle.tilt = -45;
We wait until the player is created and then we store its strength and astrength template values (player's speed on x y z and player's angular speeds). I'm using skills15..17 to store strength and skills18..20 for astrength. If you're an older reader of this magazine you already know that we can store any var in 3 consecutive skills.
We force the 3rd person camera mode and we set a decent camera distance on x and z and a good camera angle by changing the values in the templates.
while (1)
{
while (vec_dist (my.x, player.x) > 40) {wait (1);}
vec_set (strength, nullvector); // the player can't move
vec_set (astrength, nullvector); // or rotate from now on
temp.z = my.z;
my.skill10 = particle_emmiters;
my.skill11 = min_radius;
while (my.skill10 > 0)
{
temp.x = player.x + min_radius * cos(my.skill12);
temp.y = player.y + min_radius * sin(my.skill12);
temp.z += up_step;
my.skill12 += angle_step;
min_radius += radius_step;
effect (create_fx, 1, temp.x, normal);
my.skill10 -= 1;
}
When
the player comes closer than 40 quants to the origin of the teleport entity,
we reset its strength and astrength so it won't be able to move or rotate
from now on. We create particles on a circle around the player - the radius
and the height of this circle keeps growing and growing, like in the picture
below:
player.shadow = off;
player.transparent = on;
player.alpha = 100;
while (player.alpha > 0)
{
player.alpha -= 7 * time;
wait (1);
}
player.invisible = on;
waitt (16);
min_radius = my.skill11;
player.x = my.skill1; // set skill1..4 in Wed
player.y = my.skill2;
player.z = my.skill3;
player.pan = my.skill4;
player.transparent = off;
player.invisible = off;
vec_set (strength, my.skill15); // restore player's speed on x, y, z
vec_set (astrength, my.skill18); // and player's angle speeds
wait (1);
}
}
If the player has its shadow flag set, its shadow will disappear; the player will fade away by changing its alpha value. After a second, the player will be moved to its new position set with skills1..4 for x, y, z and pan because sometimes we want the player to face a certain direction after being beamed.
Remember skill15..17 and skill18..20? We have stored the initial strength and astrength in them - now it is the time to get the values back because we want to be able to move the player.
The only think left to be discussed is the particle effect:
function
create_fx()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(2) + 1;
vec_add (my.vel_x, temp);
my.flare = on;
my.bright = on;
my.alpha = 50;
my.bmap = effect_pcx;
my.size = 30;
my.move = on;
my.lifespan = 100;
my.function = fade_fx;
}
function
fade_fx()
{
my.alpha -= 2 * time;
if (my.alpha < 0) {my.lifespan = 0;}
}
You can see that the particles have random speeds on x, y and z but the value on z is always greater than zero so the particles will move upwards, decreasing their alpha value as they go up.
Air combat
This time we have a (let's hope) fun player vs choppers game with lots of interesting features. Every standalone project has its main function:
function
main()
{
sky_clip = -90; // renders the whole sky
on_d = null; // disables the debug panel
level_load (aircombat_wmb);
wait (2);
game_init();
}
The first line renders the whole sky; it is an open area and we need to see it without any clipping. We disable the "D" key because we will use it for movement, we load the level and when it has loaded we call the game_init() function:
function
game_init()
{
while (player == null) {wait (1);}
while (1)
{
if ((mouse_left == 1) && (player.health > 0)) // fire
{
snd_play (shoot_snd, 70, 0);
ent_create (rocket_mdl, player.pos, move_bullet);
recoil = 1;
waitt (2);
recoil = 0;
while (mouse_left == 1) {wait (1);} // disable autofire
}
temp_counter += 1;
if (temp_counter == 50)
{
temp_counter -= 49;
}
if (chopper_index[temp_counter] < distance && chopper_index[temp_counter]
> 0)
{
distance = chopper_index[temp_counter];
found_chopper = temp_counter;
}
else
{
distance = chopper_index[found_chopper];
}
if (remaining_choppers == 0)
{
aiming_pan.visible = off;
}
wait (1);
}
}
This function waits until the player is created. When this happens, a while (1) loop is started. If we press the left mouse button (LMB) and the player is still alive, a sound is played and a rocket is created at player's position. We set the recoil to 1 for about 0.1 seconds and then we wait until the LMB is released; we'll talk about recoil a little later.
The rest of the code deals with the auto targeting (not aiming) system. If the current chopper is closer than the others and it is a valid chopper, its distance to the player will be stored in a var named distance, otherwise we keep the old distance. When all the choppers are down we hide the target panel.
We can have up to 50 choppers; we measure their distance to the player and the closest chopper will place a target panel on the screen. All you have to do is to make sure that you shoot in its center and the chopper will be hit.
The code that moves the player and the camera is included in the action below:
action
player_moves
{
player = me;
my.health = 100;
my.invisible = on;
my.enable_impact = on;
my.enable_entity = on;
my.event = player_event;
while (my.health > 0)
{
vec_set (camera.pos, my.pos);
camera.tilt += 20 * mouse_force.y * time;
camera.pan -= 20 * mouse_force.x * time;
my.pan = camera.pan;
my.tilt = camera.tilt;
player_speed.x = 15 * (key_w - key_s) * time - 0.5 * recoil;
player_speed.y = 10 * (key_a - key_d) * time;
vec_set (temp, my.x);
temp.z -= 1000;
trace_mode = ignore_me + use_box;
player_speed.z = -trace (my.x, temp);
move_mode = ignore_you + ignore_passable;
ent_move(player_speed, nullvector);
wait (1);
}
}
The player has 100 health points and it is invisible because we are in 1st person model; it is sensitive at impact with entities. As long as the player is alive, it can be moved using the WSAD keys and the mouse. You can play with the values for camera.pan and tilt, player_speed.x and y to modify the sensitivity and the speed. The speed for the forward movement is stored in player_speed.x; do you remember that we have set recoil to 1 for 0.1 seconds? This way the player will be moved backwards every time it fires a rocket. You can change 0.5 to get different recoils. The movement code includes gravity code so you can use any level (my example is set up in a flat level).
If the player is hit by an entity (the choppers fire sprites) its player_event function will run:
function
player_event()
{
wait (1);
exclusive_global;
if (my.health > 0)
{
my.health -= 0.5;
snd_play (playerhit_snd, 40, 0);
}
else
{
my.health = 0;
}
}
If
the player is alive, its health is decreased and a sound is played. If
the player is dead, is health is set to zero because it doesn't look good
to see that your dead player has health = -25. Let's take a look at the
function that moves player's bullet (a rocket):
function
move_bullet()
{
my.passable = on;
my.enable_entity = on;
my.enable_block = on;
my.event = remove_me;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.skill1 = 0;
rocket_speed.x = 500 * time;
rocket_speed.y = 0;
rocket_speed.z = 0;
while (my.skill1 < 100)
{
if (vec_dist (my.x, player.x) > 50)
{
my.passable = off;
}
my.roll += 25 * time;
my.skill1 += 1 * time;
effect (create_smoke, 1, my.x, normal);
move_mode = ignore_you + ignore_passable;
ent_move (rocket_speed, nullvector);
wait (1);
}
ent_remove (me);
}
function
remove_me()
{
waitt (2);
ent_remove (me);
}
The rocket is passable at the moment of creation but its passable flag will be set to off as soon as the distance between the rocket and the player is over 50 quants. The rocket rotates around its roll angle and creates a smoke trail. If the rocket has travelled too much without hitting anything, it will be removed. The same thing happens when the rocket hits an entity or a level block.
Time
to see the functions that create the rocket smoke trail:
function
create_smoke
{
temp.x = random(4) - 2;
temp.y = random(4) - 2;
temp.z = random(3) + 3;
vec_add (my.vel_x, temp);
my.flare = on;
my.bright = on;
my.alpha = 50;
my.bmap = smoke_pcx;
my.size = 30;
my.move = on;
my.lifespan = 100;
my.function = fade_smoke;
}
function
fade_smoke()
{
my.alpha -= 2 * time;
my.size -= 2 * time;
if ((my.alpha < 0) || (my.size < 2)) {my.lifespan = 0;}
}
The smoke will move upwards; the particles will disappear as soon as their alpha goes below zero or their size goes below 2 (this should happen first if you are using my values, but you can change them).
Every chopper is created by an object that has the action chopper_generator attached to it.
action
chopper_generator
{
my.invisible = on;
my.passable = on;
waitt (64 + random (64));
ent_create (chopper_mdl, my.pos, move_chopper);
}
I have used a cube for every chopper generator; the cube is made invisible and passable. The chopper will be created 4..8 seconds after the level is loaded and the function associated to it is scary :
function
move_chopper()
{
chopper_number += 1;
remaining_choppers += 1;
my.skill1 = chopper_number;
my.enable_impact = on;
my.enable_block = on;
my.enable_entity = on;
my.event = chopper_event;
my.skill40 = 1;
Every chopper has an unique number associated to it; we are using a var named chopper_number and every time move_chopper() is run, chopper_number is increased and the result is copied in skill1. The chopper will react if it collides with other entities or with level blocks. Finally, we set skill40 to 1 for all the choppers - this will be our method to detect if a certain entity is a chopper or not.
while (my != null)
{
vec_for_vertex(my.skill18, my, 55);
if (my.skill42 == 0 && abs(my.x + my.y - player.x - player.y) >
2000)
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
}
my.tilt = 0;
ent_cycle("fly", my.skill10);
my.skill10 += my.skill11 * time;
if (my.skill11 < 10)
{
my.skill11 += 0.2 * time; // increase the rotation speed
}
if (my.skill10 > 100)
{
my.skill10 = 0; // loop animation
}
As long as the chopper is "alive" we store the vertex that will be used for firing in skill18. If the difference between player's coords and chopper's coords is above 2000 quants, the chopper will rotate and it will move towards the player. We don't need to tilt the chopper but because vec_to_angle does that we set tilt to zero. The chopper will rotate its propeller faster and faster because skill11 is growing from 0 to 10.
if ((my.skill17 < 3 + random(3)) && (my.skill12 == 0))
{
my.skill15 = 0; // skills 15..17 are used a var here
my.skill16 = 0;
my.skill17 += 0.05 * time;
}
else
{
my.skill12 = 1;
if (my.skill15 < 5 * time)
{
my.skill15 += 2 * time;
}
my.skill16 = 0;
my.skill17 = 0;
if (500 < abs(my.x + my.y - player.x - player.y) < 1000) // close
to the player -> fire bullets
{
my.skill25 += 3 * time; // a simple counter
my.skill26 = int (my.skill25);
if ((my.skill26 % 100 == 0) && (vec_dist (player.x, my.x) > vec_dist
(player.x, my.skill18))) // change "100" to adjust the fire rate
{
ent_create (bullet_pcx, my.skill18, chopper_bullet); // don't fire until
the chopper is up
}
}
}
Every chopper has a random altitude; skill15..17 are used as a var here, with skill15 storing the speed on x, skill16 the speed on y and skill17 the speed on z. You can see that the chopper moves only upwards until skill17 reaches the value set by 3 + random(3); you can change these values if you want to set different heights. When the chopper has reached its maximum height, it stops moving upwards and starts to move faster and faster in the direction given by its pan angle. If the difference between player's coords and chopper's coords is bigger than 500 and smaller than 1000, the chopper will fire bullets.
Now we have to face another challenge: we don't want the chopper to fire 50-100 bullets a second (it wouldn't be fair) but we want it move at 50-100 frames a second in the same while loop. If we would use a waitt(8) at the end of the while loop the chopper would fire 2 bullets a second - and this would be ok - but the chopper would move and change its animation frames only twice a second, right?
The solution is easy: we use a skill and we add to its value every frame. When the content of the skill has reached a certain value, we fire a bullet. I'm using skill26 to store the integer part of skill25; if skill26 % 100 = 0 (in other words, if skill26 = 0 or 100 or 200 or 300 or..) we fire a bullet. The good news is that you can change 100 to any other value to adjust the fire rate.
Wait! I see that the if branch has another weird condition inside it. What's with this line?
if (.......vec_dist (player.x, my.x) > vec_dist (player.x, my.skill18)
Well,
you caught me. This part of the if branch makes sure that the chopper can't
shoot with its back! I could have checked player's and chopper's angles
but I want to show you as many different things as possible so I have decided
to use a trick - take a look at the picture below:
If
the distance between the player and chopper's "gun" (stored in skill18)
is smaller than the distance between the player and chopper's origin, the
chopper will fire. If the distance between the player and the origin is
smaller than the distance between the player and the gun, the chopper won't
shoot because the player is behind it.
chopper_index[my.skill1] = vec_dist(my.x, player.x);
if (distance > chopper_index[my.skill1] - 4000)
{
vec_set (temp, my.x);
if (vec_to_screen (temp, camera)) // if the target is visible
{
aiming_pan.pos_x = temp.x - bmap_width(aiming_pcx) / 2;
aiming_pan.pos_y = temp.y - bmap_height(aiming_pcx) / 2;
aiming_pan.visible = on;
}
else
{
aiming_pan.visible = off;
}
}
move_mode = ignore_you + ignore_passable;
ent_move(my.skill15, nullvector);
wait (1);
}
}
The array chopper_index stores the distance between every chopper and the player; if one of the choppers is closer, the target will appear at its position. I have decreased the distance with 4000 quants to add hysteresis - to make sure that the target isn't changing quickly from one target to the other as they come closer and closer to the player. Play with this value - it depends on the size of your level.
If the target is visible on the screen, we set the correct position for the aiming panel and we make it visible, otherwise we hide the panel. Finally, the chopper moves using skills15..17 and ignore passable entities.
If the chopper impacts with a rocket, a wall or with another chopper, its chopper_event() function will run:
function
chopper_event()
{
if (((event_type == event_entity) || (event_type == event_impact)) &&
(you.skill40 == 1))
{
my.pan -= 180;
my.skill42 = 1;
waitt (64);
my.skill42 = 0;
}
else
{
chopper_index[my.skill1] = 0;
distance = 20000;
remaining_choppers -= 1;
ent_playsound (my, chopperx_snd, 1000);
my.passable = on;
my.ambient = 50;
waitt (2);
ent_create (explo13_pcx, my.pos, explode_me);
waitt (4);
my.transparent = on;
ent_create (explo13_pcx, my.pos, explode_me);
ent_remove (me);
}
}
If the chopper collides with an entity and this entity is another chopper (we've set skill40 to 1 for all the choppers, right?), the chopper will get away for 4 seconds and then it will come back. This happens because the chopper chases the player only if its skill42 is set to zero - check the beginning of the while loop in function move_chopper.
If the chopper collides with a rocket, we won't target it anymore and we set a big distance for it - this makes the closest chopper alive to take control. We play an explosion sound and we create two sprites that overlap and run the same function to make the explosion look better and then we remove the chopper.
function
explode_me()
{
my.oriented = on;
my.flare = on;
my.bright = on;
my.ambient = 100;
while (my.frame < 14)
{
if (my.scale_x < 3)
{
my.scale_x += 0.5 * time;
my.scale_y = my.scale_x;
}
my.frame += 1 * time;
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
wait (1);
}
ent_remove (me);
}
When the chopper explodes is creates 2 sprites; their scale will grow until it reaches 3 and at the same time their animation frames will run. The sprites will turn towards the player to make sure that the explosion looks great; a simple sprite won't look that good when you shoot a chopper above your head.
Do you feel a little way too tired? We could stop here but there's a little function left to be to discussed:
function
chopper_bullet()
{
my.passable = on;
my.oriented = on;
my.flare = on;
my.bright = on;
my.scale_x = 0.5;
my.scale_y = my.scale_x;
my.enable_entity = on;
my.enable_block = on;
my.event = remove_me;
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // the bullet moves towards the player
my.skill1 = 0;
bullet_speed.x = 100 * time;
bullet_speed.y = 0;
bullet_speed.z = 0;
while (my != null)
{
if (my.roll > 200) {my.passable = off;} // it works :)
my.roll += 25 * time;
my.skill1 += 1 * time;
move_mode = ignore_you + ignore_passable;
ent_move (bullet_speed, nullvector);
wait (1);
}
}
This function moves chopper's (sprite) bullets. The bullets are passable and they will be rotated towards the player at the moment of creation; if you would put the 3 separate lines in the while loop the player wouldn't be able to dodge any bullet - he would bite the dust every time. The rockets rotate around their roll angle and when roll > 200 their passable flag will be set to off.
I hope
that you will enjoy playing with this project - I did!