Chapter XIII
Using Items and Attacking
Alrighty then, let's have some fun now. In this chapter we will actually get to the point where we do damage, kill each other, and keep track of our Lunar Psychosis Rating (score). After this chapter we cruise to the finish. So let's dive right in.
Attacking and Doing Damage
What the heck, let's go ahead and get everything declared right from the start so we don't have to bother ourselves with all of that this chapter. In declarations under Models add this.
string str_photo_jelly = <photojelly.mdl>; // photosynthesis jelly model
string str_lunar_candy = <lunarcandy.mdl>; // lunar candy model
string str_bullet = <bullet.mdl>; // bullet model
string str_petri_dish = <petridish.mdl>; // petri_dish
string str_radiation_beam = <radiationbeam.mdl>; // radiation beam model
Under Bmaps add.
bmap pcxChat = <Chat.pcx>; // Chat Panel
bmap spark_map,<particle.pcx>; // bitmap used for sparks
bmap* scatter_map = spark_map;
Under Defines add.
// Power-up defines
define MAX_POWERUP_TYPE_IN_LEVEL, 10; // Maximum number of any power-up type
// Weapon recharge
define WEAPON_RECHARGE_DELAY, 25; // reload time in frames
// Projectile speed
define PROJECTILE_SPEED, 30; // speed of projectiles
// Player's grace time in frames (cannot be hit upon creation)
define PLAYER_GRACE_TIME, 600;
// Ammunition life_spans
define AMMO_LIFESPAN_NUCLEAR, 200; // lifespan of raditiaon beam
define AMMO_LIFESPAN_EMULATOR, 110; // lifespan of petri-dish
define AMMO_LIFESPAN_RIFLE, 135; // lifespan of bullet
// Local Effects
define EFFECT_HIT_EXPLOSION, 1; // big explosion on hit
define EFFECT_RICOCHET_EXPLOSION, 2; // little explosion on ricochet
Don't let this scare you, but add the following skills under Skill Definitions.
define ammunition_remaining, skill17; // ammunition entity has left
define damage_die_1, skill18; // random damage of die 1
define damage_die_2, skill19; // random damage of die2
define firing_weapon, skill20; // TRUE if ent firing weapon
define life_span, skill21; // projectile life_span
define animation_attack_life, skill22; // how long to run attack anim on fire
define grace_time, skill23; // time left in ent's grace period
define explosion_normal_x, skill24; // normal vec used by projectile for effect
define explosion_normal_y, skill25;
define explosion_normal_z, skill26;
define owned_by_hdl, skill27; // owner of this ent (used for projectile)
We also need to add a few variables under Game Variables.
var photo_jelly_count; // number of photo jellies in level
var lunar_candy_count; // number of lunar candies in level
var server_restart; // server died, server go back to profession selection
var client_restart; // client died, client go back to profession selection
var start_cam_pos[3]; // on death revert back to start cam position
var start_cam_pan[3];
var weapon_recharge_count = WEAPON_RECHARGE_DELAY; // reload time
var send_server_player; // tells server to send his player ent to all client
var effect_pos[3]; // position to do local effect
var effect_normal[3]; // normal for local explosion
var do_effect_type; // type of effect to do locally
var player_number_quit; // number of player who quit or was killed
In Display Strings add.
string str_health; // player's health string
string str_lunar_psychosis_rating; // score
string str_latency;
string str_bps;
Also add this string directly under that.
string str_latency;
string str_bps;
// message strings
string str_status_message; // tells player game status, Loading & Grace Period
Add these entity pointers under, you guessed it, Entity Pointers.
entity* ptrTempEnt; // temp pointer to a entity
entity* ptrOwner; // pointer to owner of an entity
entity* ptrTempOwner; // temp pointer to owner of an entity
Under Text add these.
// text to display this player's health
text txt_health
{
pos_x = 5;
pos_y = 65;
layer = 15;
font fnt_century12;
string str_health;
}
// text that tells player game status
text txt_status_message
{
pos_x = 0; // set in init_display
pos_y = 0; // set in init_display
layer = 15;
font fnt_century12;
string str_status_message;
}
// text to display this player's score
text txt_lunar_psychosis_rating
{
pos_x = 5;
pos_y = 35;
layer = 15;
font fnt_century12;
string str_lunar_psychosis_rating;
}
// text for latency
text txt_latency
{
pos_x = 0; // set in init_display()
pos_y = 95;
layer = 15;
font fnt_century12;
string str_latency;
}
// text for bytes per second
text txt_bps
{
pos_x = 0; // set in init_display()
pos_y = 125;
layer = 15;
font fnt_century12;
string str_bps;
}
There we go. I think that's about it. Now we can get to some programming.
I think we will begin with the grace period code. When a player first joins while he is connecting and getting set up and oriented we don't want the player's that are already in the game killing him before he even has a chance to move. So what we are going to do is give the player a grace period inwhich he cannot be hit and he cannot fire his weapon either. What he can do is move around before he gets blasted and he can also pick up ammo and lunar candy during this period. So let's add that code first in function move_player.
my.pan = random(360); // face random direction
my.transparent = ON; // create as transparent until grace period over
my.alpha = 20;
my.grace_time = PLAYER_GRACE_TIME; // set time left in grace period
send_skill(my.grace_time,0);
my.ammunition_remaining = AMMO_GAME_START; // rounds of ammunition at creation
In this code we set the player's transparency on and set the alpha so he is barely visible to start with. What we will do shortly is cause him to become more visible the closer to the time his grace period is over. He will become completely visible when his grace period is over and he can be attacked. We also set the time left in his grace period and send it to the client(player) who created this entity.
Now in function move_player add this code in the while(1) loop.
while(1)
{
// I am in grace period, amke sure everyone knows that and make me
// transparent until I can be hit
if(my.grace_time)
{
my.grace_time -= 1; // decrement grace time
// make player more visible
my.alpha = 20 + ((PLAYER_GRACE_TIME-my.grace_time)/PLAYER_GRACE_TIME)*50;
// Oops, grace period over
if(my.grace_time == 0)
{
my.transparent = OFF; // make player totally visible, grace is over
send_skill(my.grace_time, 0); // make sure client that created ent knows
// grace period is over
}
}
We first check if grace period is active for this player, if(my.grace_time). If he is still in grace period we next decrement the time left in grace period, my.grace_time -= 1.
Then we use this nifty little nightmare of an equation I made that basically takes the alpha of the player from 20 to 70 not matter what you set the PLAYER_GRACE_TIME to. Don't ask me how it works.
my.alpha = 20 + ((PLAYER_GRACE_TIME-my.grace_time)/PLAYER_GRACE_TIME)*50;
Then we check if the grace_time has expired, if(my.grace_time == 0). If it has we turn off transparency and let the player who was in the grace period he is now free game.
my.transparent = OFF; // make player totally visible, grace is over
send_skill(my.grace_time, 0); // make sure client that created ent knows
// grace period is over
Now in function main() add this code.
// display player specific text
txt_player_name.visible = ON;
txt_health.visible = ON;
txt_ammo_remaining.visible = ON;
// display loading message
str_cpy(str_status_message, "Initializing Player...");
txt_status_message.visible = ON;
// after player has a weapon we can continue to
// grace period message
while(player.weapon == 0)
{
wait(1);
}
str_cpy(str_status_message, "Grace Period Active");
Here we just display a loading message until the player's weapon is set. After that we continue on to the grace period message. In the while(1) loop in main() add this script.
if (dplay_latency == 0 && dplay_bps == 0 && connection == 2)
{
exit;
}
//dedicated server skips this
if (connection != 1)
{
// if grace period over, hide message
if (player.grace_time == 0)
{
txt_status_message.visible = OFF;
}
}
We check if the computer is a dedicated server, if (connection != 1). If the computer is Server then it doesn't need to worry about displaying grace period message. Then we check if garce period is over, if (player.grace_time == 0). If grace period is over we turn off the grace period message, txt_status_message.visible = OFF.
Then in function init_display() add these two lines of code to set status message position to center of screen.
txt_number_of_players.pos_x = screen_size.x - 350;
txt_status_message.pos_x = screen_size.x/2 - 150;
txt_status_message.pos_y = screen_size.y/2;
Let's do a quick test run on Host from SED. You should now see something like this when you select a profession.
I actually think this program is starting to look like a game. Next, let's set up our input code to fire weapon. We are going to do this a bit differently then our movement code. I decided to make it so the player can't just hold down the fire button (<space> key) and continually fire. Instead I decided to use on_space = function command so the player has to let off of key and re-hit it. Also, I put a recharge time on weapon so he can't constantly hit the <space> key. Good players will figure out about the exact recharge time and time their <space> key hits so they can get the maximum number of rounds the fired in the shortest amount of time.
So at bottom of the .wdl file add this line of code.
on_enter = chat_entry; // on <enter> start chat entry
on_space = fire_weapon; // on <space> shoot weapon
The right above all the on_ commands add this function.
//--------------------------------------------------------------------
// Fire_Weapon - player hit key to fire weapon, so let server know
//--------------------------------------------------------------------
function fire_weapon()
{
// fire weapon if have ammo, not reloading and not in grace peiod
if ((player.ammunition_remaining > 0) && (weapon_recharge_count == WEAPON_RECHARGE_DELAY)
&& (player.grace_time == 0))
{
weapon_recharge_count = 0; // set reload timer to 0
player.ammunition_remaining -= 1; // one less round ammo
player.firing_weapon = TRUE; // player is firing weapon
if (connection == 2) // client?
{
send_skill(player.ammunition_remaining,0); // send ammo left to server
send_skill(player.firing_weapon,0); // let server know we are firing weapon
}
}
}
on_server = server_called; // on server event, call server_called()
on_client = client_called; // on client event, call cleint_called()
So when the player hits the <space> key, the fire_weapon() function is called. The program the checks to make sure that the player has ammunition, he is not in grace period and that his weapon is not reloading.
if ((player.ammunition_remaining > 0) && (weapon_recharge_count == WEAPON_RECHARGE_DELAY)
&& (player.grace_time == 0))
If he can fire weapon, the first thing we do is reset his weapon_recharge_count to zero. It will count up until the weapon is considered reloaded when it equal WEAPON_RECHARGE_DELAY. During that period he cannot fire his weapon again. We will add the recharge code here soon.
weapon_recharge_count = 0; // set reload timer to 0
Then we decrement the ammo he has available.
player.ammunition_remaining -= 1; // one less round ammo
After that, set the skill to let the server know the player just fired his weapon, the server uses this information to start attack animation on all the clients and to actually create and move the projectile.
player.firing_weapon = TRUE; // player is firing weapon
Then, if this player is a client, we send the information to the server.
if (connection == 2) // client?
{
send_skill(player.ammunition_remaining,0); // send ammo left to server
send_skill(player.firing_weapon,0); // let server know we are firing weapon
}
Now, lets add the code to make the weapon reload. Add this function right below fire_weapon().
//--------------------------------------------------------------------
// Function Reload_Weapon(): reload weapon (time inbetween attacks)
//--------------------------------------------------------------------
function reload_weapon()
{
while(1)
{
weapon_recharge_count += 1;
if(weapon_recharge_count > WEAPON_RECHARGE_DELAY)
{
weapon_recharge_count = WEAPON_RECHARGE_DELAY;
}
wait(1);
}
}
on_server = server_called; // on server event, call server_called()
on_client = client_called; // on client event, call cleint_called
This is pretty self-explainatory. It contually recharges weapon if it not recharge until it reaches WEAPON_RECHARGE_DELAY.
Let's add the call to this function from main() now.
display_info(); // continually display any information we want to show
init_chat(); // intitialize chat variables
reload_weapon(); // continually recharges weapon
Next we need to add the code to the server to handle the firing_weapon skill we sent from the client to make everything happen. So in while(1) loop in function move_player add this script.
// if firing weapon set how long we want attack animation to run
if(my.firing_weapon == TRUE)
{
my.animation_attack_life = 20;
}
// if still running attack animation, attack animation overides others
if(my.animation_attack_life > 0)
{
my.animation_state = ANIMATION_STATE_ATTACK;
my.animation_attack_life -= 1;
}
// if animation state changed, send new animation state to all clients
// so they can do animation locally
if(my.animation_state != my.prev_anim_state)
{
send_skill(my.animation_state,SEND_ALL);
}
When the server first receives the firing_weapon skill from the client, it immediately set's the number of frames to run the attack animation.
if(my.firing_weapon == TRUE)
{
my.animation_attack_life = 20;
}
Then while the attack animation is still running, it supercedes all other animations and decrements by one each frame.
// if still running attack animation, attack animation overides others
if(my.animation_attack_life > 0)
{
my.animation_state = ANIMATION_STATE_ATTACK;
my.animation_attack_life -= 1;
}
A bit lower in the same while(1) loop in move_player we need to add this code right below the scan floor script.
RESULT = TRACE(vecFrom,vecTo);
my.Z = TARGET.Z + ((my.MAX_Z - my.MIN_Z)/2);//Set to the floor
}
*/
// if ent just hit attack key, create projectile depending on weapon
if (my.firing_weapon == TRUE)
{
if(my.weapon == WEAPON_NUCLEAR_ROD)
{
ent_create(str_radiation_beam, my.X, projectile_function);
}
if (my.weapon == WEAPON_PHOTOSYNTHESIS_EMULATOR)
{
ent_create(str_petri_dish, my.X, projectile_function);
}
if (my.weapon == WEAPON_COMPRESSION_RIFLE)
{
ent_create(str_bullet, my.X, projectile_function);
}
my.firing_weapon = FALSE; // projectile sent, no longer firing
}
This is the code that actually creates the projectile entity and starts it's function depending on the player's weapon. The it set's the firing_weapon skill to FALSE. The projectiles will be moved globally on the server, just like the players are.
Let's go ahead and add the projectile_function() now right above function move_player.
//--------------------------------------------------------------------
// Function Projectile_Function: create correct projectile for weapon
//--------------------------------------------------------------------
function projectile_function()
{
my.enable_entity = ON;
my.enable_block = ON;
my.event = projectile_event;
my.z += 25; // fire above waist level
//my.y = you.y + 8 * sin(you.pan) - 8 * cos(you.pan); //offset projectile right
my.speed = PROJECTILE_SPEED; // get speed
my.pan = you.pan; // set pan to owners pan
my.force_y = 0;
my.force_z = 0;
my.owned_by_hdl = handle(you);
//if player carries nuclear rod 2d18 damage
if(you.weapon == WEAPON_NUCLEAR_ROD)
{
my.life_span = AMMO_LIFESPAN_NUCLEAR; // set range
my.damage_die_1 = 18; // set die 1 (damage dice)
my.damage_die_2 = 18; // set die 2
// play radition beam sound at player
ent_playsound (you, snd_radiation_beam, 40);
}
//if player carries photosynthesis emulator 2d13 damage
if(you.weapon == WEAPON_PHOTOSYNTHESIS_EMULATOR)
{
my.life_span = AMMO_LIFESPAN_EMULATOR; // set range
my.damage_die_1 = 13; // set die 1 (damage dice)
my.damage_die_2 = 13; // set die 2
// play petri dish sound at player
ent_playsound (you, snd_emulator, 45);
}
// if player carries rifle 2d8 damage
if(you.weapon == WEAPON_COMPRESSION_RIFLE)
{
my.life_span = AMMO_LIFESPAN_RIFLE; //set range
my.damage_die_1 = 8; // set die 1 (damage dice)
my.damage_die_2 = 8; // set die 2
// play bullet sound at player
ent_playsound (you, snd_rifle, 100);
}
// while projectile still alive, this is global
while (my.life_span != 0)
{
my.life_span -= 1;
// use time & speed
my.force_x = my.speed * time;
//A5 move
move_mode = ignore_passable + ignore_you + ignore_me;
ent_move(my.force_x,nullvector);
//A6.3 move the player, c_move bugged at time of release so using ent_move instead
//c_move(my, force.x, nullvector, ignore_passable + ignore_you + ignore_me);
wait(1);
}
sleep(.1); // wait for sound to be played at projectile
// if projectile life_span over(at max range), remove projectile
ent_remove(my);
}
//--------------------------------------------------------------------
// MOVE - ATTACHED TO PLAYER IN CREATE_PLAYER() FUNCTION
//--------------------------------------------------------------------
When we first create the projectile we enable_block and enable_entity and set it's event function to projectile_event(), which we will add after we are done with the explaination of this function. Enable_block handles the collisions with walls and static level geometry. Enable_entity handles the collisions with other entities.
my.enable_entity = ON;
my.enable_block = ON;
my.event = projectile_event;
Next we raise the start position of the projectile up some so it is more at chest level of the player, my.z += 25. After that we set the speed and pan of the projectile. Since are bullets will be flying horizontally and straight we set force_y and force_z to zero.
After that we save the handle to the owner of the bullet using the handle() command. We will later use this handle to get a pointer (using ptr_for_handle()) to the player that owns this entity from a local procedure on the clients. Since we got this handle on the server or host, this handle refers to the global player (the player on the server), if we would have gotten this handle from within the client's local procedure, the handle would refer to the entity (clone) on the client.
my.speed = PROJECTILE_SPEED; // get speed
my.pan = you.pan; // set pan to owners pan
my.force_y = 0;
my.force_z = 0;
my.owned_by_hdl = handle(you);
Note: If you are not familiar with handle() and ptr_for_handle() commands you may want to read the manual. Also I created a tutorial on this long ago called the Power of Handles, which if you search the 3DGS forums you should be able to find. Handles are a very useful tool in 3DGS so I would recommend learning how to use them. At some point you will need to use them if you are making a game almost guaranteed.
I am not going to cover all the projectile types since they are the same exept for the damage values and sound they play, so let's just go through what happens if the weapon used was a nuclear rod. The reason we have to check what weapon was used was because all the projectiles when they are created call this one function, the projectile_function().
//if player carries nuclear rod 2d18 damage
if(you.weapon == WEAPON_NUCLEAR_ROD)
{
my.life_span = AMMO_LIFESPAN_NUCLEAR; // set range
my.damage_die_1 = 18; // set die 1 (damage dice)
my.damage_die_2 = 18; // set die 2
// play radition beam sound at player
ent_playsound (you, snd_radiation_beam, 40);
}
First we check if the player that created this projectile is carrying a nuclear rod, if(you.weapon == WEAPON_NUCLEAR_ROD). If he is we then set the life_span of the projectile to AMMO_LIFESPAN_NUCLEAR, which is the range determined by frames. Then we set the damage of the two dice that will be rolled when the projectile hits another player and the roll for armor class save is made.
my.damage_die_1 = 18; // set die 1 (damage dice)
my.damage_die_2 = 18; // set die 2
Then we play the sound for the radiation beam being fired, ent_playsound (you, snd_radiation_beam, 40).
Let's skip the photosythesis emulator and compression rifle code since they are virtually the same.
Now we start the actual movement code with a while loop that continues until the projectile's life_span is over or until the projectile hits something and is removed, which will be handled by the projectile_event().
while (my.life_span != 0)
The first thing we do within the while() loop is to decrement the projectile's life_span and then set it's force_x and move it. If it hits a wall or entity during move the projectile_event() will be called.
{
my.life_span -= 1;
// use time & speed
my.force_x = my.speed * time;
//A5 move
move_mode = ignore_passable + ignore_you + ignore_me;
ent_move(my.force_x,nullvector);
//A6.3 move the player, c_move bugged at time of release so using ent_move instead
//c_move(my, force.x, nullvector, ignore_passable + ignore_you + ignore_me);
And next we wait(1) and do it all again until projectile is removed or it's life_span expires.
wait(1);
}
Then when the weapon's life_span reaches zero we exit the while loop and we wait 1/10th of a second to make sure entity still exist on the clients when we play the explosion sound and then we remove the projectile.
sleep(.1); // wait for sound to be played at projectile
// if projectile life_span over(at max range or hit something), remove projectile
ent_remove(my);
Now let's add the projectile_event() function right above the projectile_function(). Are you feeling like a programmer? With all this code, you ought to. The projectile_event() is called when the projectile hits a entity or level geometry.
//--------------------------------------------------------------------
// Function Projectile_Event: projectile hit something
//--------------------------------------------------------------------
function projectile_event()
{
var random_num;
var dam; // total damage to entity hit
// projectile hit a entity in level
if(event_type == event_entity)
{
// if player hit is not still in grace period
// check for damage
if(you.grace_time == 0)
{
random_num = int(random(20))+1; // get d20 attack roll
// check if enemy's AC saved them from hit
if(random_num >= you.armor_class)
{
dam = two_dice_damage(); // do damage for this ammo type
you.health -= dam; // subtract damage from entity hit
ptrOwner = ptr_for_handle(my.owned_by_hdl);
ptrOwner.lunar_psychosis_rating += dam; // give 1 point per damage point
if(you.health <= 0) // if you killed enemy
{
ptrOwner.lunar_psychosis_rating += 500; // give 500 points for kill
}
// send needed skills to owner clients
send_skill(ptrOwner.lunar_psychosis_rating, 0);
// the below sent skills used for local effects on clients
vec_set(effect_pos.x, my.x); // set explosion position
send_var(effect_pos);
vec_set(effect_normal.x,normal); // set explosion normal
send_var(effect_normal);
do_effect_type = EFFECT_HIT_EXPLOSION; // set effect type
send_var(do_effect_type);
ent_playsound (my, snd_damage, 100); // play damage sound
if (connection != 1) // dedicated server need not do local effects
{
effect_local(effect_explosion,50,effect_pos.X,effect_normal.x);
}
}
else // richocet
{
// the below sent skills used for local effects on clients
vec_set(effect_pos.x, my.x); // set explosion position
send_var(effect_pos);
vec_set(effect_normal.x,normal); // set explosion normal
send_var(effect_normal);
do_effect_type = EFFECT_RICOCHET_EXPLOSION; // set effect type
send_var(do_effect_type);
ent_playsound (my, snd_no_damage, 100); // play ricochet sound
if (connection != 1) // dedicated server need not do local effects
{
effect_local(effect_explosion,20,effect_pos.X,effect_normal.x);
}
}
}
}
// projectile hit level geometry
if (event_type == event_block)
{
// the below sent skills used for local effects on clients
vec_set(effect_pos.x, my.x); // set explosion position
send_var(effect_pos);
vec_set(effect_normal.x,normal); // set explosion normal
send_var(effect_normal);
do_effect_type = EFFECT_RICOCHET_EXPLOSION; // set effect type
send_var(do_effect_type);
ent_playsound (my, snd_no_damage, 100); // play ricochet sound
if (connection != 1) // dedicated server need not do local effects
{
effect_local(effect_explosion,20,effect_pos.X,effect_normal.x);
}
}
my.life_span = 0; // set projectile to dead
}
//--------------------------------------------------------------------
// Function Projectile_Function: create correct projectile for weapon
//--------------------------------------------------------------------
Time for more explaining I suppose. We check to see if the event that got us here was a collision with and entity.
if(event_type == event_entity)
If it was, the we check to see if the entity we hit is in it's grace period.
if(you.grace_time == 0)
If the entity hit wasn't in it's grace period then we go ahead and do a hit roll and see if the armor of the entity deflected the hit.
random_num = int(random(20))+1; // get d20 attack roll
// check if enemy's AC saved them from hit
if(random_num >= you.armor_class)
{
If the armor didn't deflect the projectile then we do damage to the entity it hit.
dam = two_dice_damage(); // do damage for this ammo type
you.health -= dam; // subtract damage from entity hit
Now we get the owner using ptr_for_handle() and the skill that we saved the handle of the owner of this projectile into. Then we add 1 point to score for every hitpoint of damage plus 500 points if we killed a player.
ptrOwner = ptr_for_handle(my.owned_by_hdl);
ptrOwner.lunar_psychosis_rating += dam; // give 1 point per damage point
if(you.health <= 0) // if you killed enemy
{
ptrOwner.lunar_psychosis_rating += 500; // give 500 points for kill
}
We next send the new score to the player that owned the projectile that scored a hit.
send_skill(ptrOwner.lunar_psychosis_rating, 0);
We don't need to send the new health value to the fool that got hit by the projectile because that is going to be handled in the function move_player, which is going to check if the player's health != health_old to determine whether to send health. It will be just about the same method we have used for forces earlier. The reason for this is that we also will be adding some rest and exhaustion code that will also be changing the player's health and we don't want to possibly be sending the health three times for the 3 events that can change the health (got hit, resting, or running(exhaustion)) at the same time.
Then we send all the information need for the local_effect() function (will add that here soon) to do the did damage explosion locally on clients and go ahead and play the damage sound and do the local explosion for the host or single player, a dedicated server doesn't need to waste time doing a local effect it can't see anyway.
// the below sent skills used for local effects on clients
vec_set(effect_pos.x, my.x); // set explosion position
send_var(effect_pos);
vec_set(effect_normal.x,normal); // set explosion normal
send_var(effect_normal);
do_effect_type = EFFECT_HIT_EXPLOSION; // set effect type
send_var(do_effect_type);
ent_playsound (my, snd_damage, 100); // play damage sound
if (connection != 1) // dedicated server need not do local effects
{
effect_local(effect_explosion,50,effect_pos.X,effect_normal.x);
}
If the projectile was deflected by armor we set and send the correct data for ricochet to the clients, play the ricchet sound and do local explosion on host or single player.
else // richocet
{
// the below sent skills used for local effects on clients
vec_set(effect_pos.x, my.x); // set explosion position
send_var(effect_pos);
vec_set(effect_normal.x,normal); // set explosion normal
send_var(effect_normal);
do_effect_type = EFFECT_RICOCHET_EXPLOSION; // set effect type
send_var(do_effect_type);
ent_playsound (my, snd_no_damage, 100); // play ricochet sound
if (connection != 1) // dedicated server need not do local effects
{
effect_local(effect_explosion,20,effect_pos.X,effect_normal.x);
}
}
Now, if the projectile hit level geometry, we also send data, play sound, and do richochet explosion locally on host or single player.
// projectile hit level geometry
if (event_type == event_block)
{
// the below sent skills used for local effects on clients
vec_set(effect_pos.x, my.x); // set explosion position
send_var(effect_pos);
vec_set(effect_normal.x,normal); // set explosion normal
send_var(effect_normal);
do_effect_type = EFFECT_RICOCHET_EXPLOSION; // set effect type
send_var(do_effect_type);
ent_playsound (my, snd_no_damage, 100); // play ricochet sound
if (connection != 1) // dedicated server need not do local effects
{
effect_local(effect_explosion,20,effect_pos.X,effect_normal.x);
}
}
And at the bottom of the projectile_event() function, which is only ever called when a projectile hits something, we set the projectile's life_span to zero after all actions have been completed for the hit and the projectile_function() will remove the projectile.
my.life_span = 0; // set projectile to dead
}
Now we need to add the local_effect() function, which will run on the clients and do the local effects when it receives the information to do so. Go ahead and add it above the projectile_event() function.
//--------------------------------------------------------------------
// Function Local_Effect: continually checks if local effects need done
//--------------------------------------------------------------------
function local_effect()
{
while(connection == 2) // only client connection needs this function
{
if (do_effect_type == EFFECT_HIT_EXPLOSION)
{
effect_local(effect_explosion,50,effect_pos.X,effect_normal.x);
do_effect_type = 0;
}
if (do_effect_type == EFFECT_RICOCHET_EXPLOSION)
{
effect_local(effect_explosion,20,effect_pos.X,effect_normal.x);
do_effect_type = 0;
}
wait(1);
}
}
//--------------------------------------------------------------------
// Function Projectile_Event: projectile hit something
//--------------------------------------------------------------------
We will call this function from main() and it will continually run. What happens is the host or server will first send the necessary data for the effect like the explosion normal and position, then it sends the type of effect to do and when the client gets the do_effect_type it uses the previously sent data to do the correct effect.
Let's go ahead and add the call from main() before we forget.
// if dedicated server skip these
if (connection != 1)
{
init_display(); // initiliaze text positions as necessary
display_info(); // continually display any information we want to show
init_chat(); // intitialize chat variables
reload_weapon(); // continually recharges weapon
local_effect(); // continually check for effect to be run on client
So now let's add the functions for the explosion above local_effect(). These functions are directly from the old 3DGS templates and I am not going to explain what they do.
//--------------------------------------------------------------------
// Function Vec_Randomize(): randomize a vector for explosion
//--------------------------------------------------------------------
function vec_randomize(&vec,range)
{
vec[0] = random(1) - 0.5;
vec[1] = random(1) - 0.5;
vec[2] = random(1) - 0.5;
vec_normalize(vec, random(range));
}
//--------------------------------------------------------------------
// Function Part_Alphafade(): fade particle over time
//--------------------------------------------------------------------
function part_alphafade()
{
my.alpha -= time+time;
if (my.alpha <= 0) { my.lifespan = 0; }
}
//--------------------------------------------------------------------
// Function Effect_Explosion(): do an explosion
//--------------------------------------------------------------------
function effect_explosion()
{
vec_randomize(temp_loc,30);
vec_add(my.vel_x, temp_loc);
my.alpha = 15 + random(5);
my.bmap = scatter_map;
my.flare = on;
my.bright = on;
my.beam = on;
my.move = on;
my.function = part_alphafade;
}
//--------------------------------------------------------------------
// Function Local_Effect: continually checks if local effects need done
//--------------------------------------------------------------------
We also added a function to roll the 2 dice that are stored in the skills, damage_die_1 and damage_die_2, let's add two_dice_damage() now above vec_randomize().
//--------------------------------------------------------------------
// Function Two_Dice_damage - rolls 2 dice stored in skills
//--------------------------------------------------------------------
function two_dice_damage()
{
var random1 = 0; // random numbers
var random2 = 0;
var dam; // total damage
// Also, my.damage_die1 should always be a positive number
if ((my.damage_die_1 <= 0)&&(my.damage_die_2 <= 0)||(my.damage_die_1 <=0))
// The below commented code is better, but would be harder to understand
// it does the exact same thing as above though
// if ((my.damage_die_1 <= 0)||(my.damage_die_2 < 0))
{
return(0);
}
random1 = int(random(my.damage_die_1)) + 1; // get roll die 1
// we will give the ability to have 1 die damage by setting my.damage_die_2
// to 0
if (my.damage_die_2 != 0)
{
random2 = int(random(my.damage_die_2)) + 1; // get roll die 2
}
dam = random1 + random2; // get damage
return (dam); // return damage
}
//--------------------------------------------------------------------
// Function Vec_Randomize(): randomize a vector for explosion
//--------------------------------------------------------------------
This function just rolls 2 dice using the damage_die skills (1 to damage_die_1) + (1 to damage_die_2) and then returns the result.
Are you ready for a test run to see if all that code actually works? Go ahead and Publish or Resource and the run as Host and Client. You should now be able to fire and do damage like this.
If you play around with it a bit you will notice no one dies. This is a humane game where no one is killed. Not! We will add that code here soon. Let's finished displaying all the text first. In init_display() add this code.
txt_number_of_players.pos_x = screen_size.x - 350;
txt_latency.pos_x = screen_size.x - 350;
txt_bps.pos_x = screen_size.x - 350;
txt_status_message.pos_x = screen_size.x/2 - 150;
Then in display_info() function add this.
str_cpy(str_number_of_players, "Number of Players: ");
str_for_num(str_temp, number_of_players);
str_cat(str_number_of_players, str_temp);
str_cpy(str_latency, "Latency: ");
str_for_num(str_temp, dplay_latency);
str_cat(str_latency, str_temp);
str_cpy(str_bps, "BPS: ");
str_for_num(str_temp, dplay_bps);
str_cat(str_bps, str_temp);
And this.
if (player != NULL) // player exist
{
str_cpy(str_player_num, "Player Number: ");
str_for_num(str_temp, player.player_number);
str_cat(str_player_num, str_temp);
str_cpy(str_lunar_psychosis_rating, "Score (LPR): ");
str_for_num(str_temp, int(player.lunar_psychosis_rating));
str_cat(str_lunar_psychosis_rating, str_temp);
Then in function main() add this.
// turn off all text not needed for profession selection
txt_health.visible = OFF;
txt_ammo_remaining.visible = OFF;
txt_player_number.visible = OFF;
txt_lunar_psychosis_rating.visible = OFF;
And this.
// display player specific text
txt_health.visible = ON;
txt_ammo_remaining.visible = ON;
txt_player_number.visible = ON;
txt_lunar_psychosis_rating.visible = ON;
Also, let's add this too.
// if not single player mode, display multiplayer information
if(connection)
{
txt_people_connected.visible = ON;
txt_number_of_players.visible = ON;
txt_player_number.visible = ON;
txt_bps.visible = ON;
txt_latency.visible = ON;
}
Now that our text display is all set up, let's get on to the death code. What we have to consider here is that when a player dies we are not exiting him from game, instead we are going to send him back to the profession selection panels. So when he dies we need to set restart positions in the necessary functions to make this happen. Let's go ahead and start in function main(). Add this code.
// if host, count yourself as a connection
if (connection == 3)
{
people_connected = 1;
}
restart_main: // on death restart here
vec_set(camera.pos,start_cam_pos); // get original camera position
vec_set(camera.pan,start_cam_pan);
The vec_set() commands are placing are camera back to it's original position. Let's go ahead and add the code to save the original camera position now too.
level_load(world_str); // load level
sleep(.5); // make sure level is loaded
vec_set(start_cam_pos,camera.pos); // save camera's original poistion for death
vec_set(start_cam_pan,camera.pan);
Now let's add the goto() statement that will get us back to restart_main label.
// main game loop
while(1)
{
// server disconnected so quit client too
if (dplay_latency == 0 && dplay_bps == 0 && connection == 2)
{
exit;
}
if((!player) && (connection!=1)) // if player is dead restart profession selection
{
profession_not_set = TRUE; // no profession until select again
goto(restart_main);
}
I was just thinking (you didn't know I could think, did you?), that the game wouldn't exit if the server or host disconnects during profession selection, so in main() add this.
vec_set(start_cam_pos,camera.pos); // save camera's original poistion for death
vec_set(start_cam_pan,camera.pan);
check_server_disconnect(); // continually checks is server is gone
Then add this function under main().
wait(1);
}
}
function check_server_disconnect()
{
while(1)
{
// server disconnected so quit client too
if (dplay_latency == 0 && dplay_bps == 0 && connection == 2)
{
exit;
}
wait(1);
}
}
And remove this code from main().
// main game loop
while(1)
{
// server disconnected so quit client too
if (dplay_latency == 0 && dplay_bps == 0 && connection == 2)
{
exit;
}
That kind of sucked didn't it. I made a mistake there, sorry about that.
Now let's make it so the players can actually die. In function move_player change this code.
while(1)
{
// I am in grace period, amke sure everyone knows that and make me
// transparent until I can be hit
if(my.grace_time)
{
To this.
while(my.health > 0) // while this entity is alive
{
// I am in grace period, amke sure everyone knows that and make me
// transparent until I can be hit
if(my.grace_time)
{
And then beneath that while() statement add this code.
my.firing_weapon = FALSE; // projectile sent, no longer firing
}
wait(1);
}
number_of_players -= 1; // decrement number of players
player_number_quit = my.player_number; // save player number who was killed
// Go through all entities, any player# that was above mine, decrement his player number
you = ent_next(NULL);
while(you != NULL)
{
if(you.player_number > player_number_quit)
{
you.player_number -= 1;
send_skill(you.player_number, SEND_ALL); // send new player#
}
you =ent_next(you);
}
send_var(number_of_players); // let everyone know new number of players
ent_remove(my); // remove ent of player that died
This code does exactly the same thing as out player_events() code did when a player got disconnected or quit.
Now, let's go ahead and add the resting and exhaustion code into action move. First lets declare a variable to store what the player's integer health was last frame.
//--------------------------------------------------------------------
// MOVE_PLAYER
//--------------------------------------------------------------------
function move_player
{
var health_old;
Now let's add the code to send the health to the owner of this entity if the integer health has changed and then save this frames health to compare with next frame in move_player.
while(my.health > 0) // while this entity is alive
{
// if integer health change since last frame send it to client
if(int(my.health) != health_old)
{
send_skill(my.health,0); //send changed health to client owner
}
health_old = int(my.health); // save old health for next frame
Under that we add the resting and exhaustion code. This code just adds health if not moving and takes health off if running.
send_skill(my.health,0); //send changed health to client owner
}
health_old = int(my.health); // save old health for next frame
// Resting code
// if standing
if ((my.force_x == 0)&&(my.force_y == 0))
{
my.health += .005; // add some health
if(my.health > my.max_health)
{
my.health = my.max_health;
}
}
// Exhaustion code
// if running
if(abs(my.force_x) > 1)
{
my.health -= .02; // subtract some health
if(my.health < 1) // don't let player run himself to death
{
my.health = 1;
}
}
Ok, lets give it another test run. Run as server and client. When you player runs (<shift> key) now his health will go down over time as shown below and when he is just standing his health will gradually go up.
Oh, what the heck, give it a spin on your primary computer in single player mode by leaving the command line blank.
After that change the command line to, -sv, and start the game and then join a client from your secondary computer. When you run in dedicated server mode you will see a screen like this on your primary computer.
Thank goodness, this chapter is now over. The game is actually pretty much complete, but we still have a few chapters left, but they will be easy ones.
Previous Page Contents Next Page