|
I assume you have finished the first three scratch tutorials for this tutorial. If you want to
watch the player animation then you need the fourth, or you can use chase_active 1 (does
not work correctly in id's WinQuake, but does in most ports and GLQuake). In this tutorial
I will show you an interesting way to handle player animations. The code is partially based
on Quake 2 player animation code and code I produced a while back. Its a lot smaller than
id's quake code, and its very different. The code is also more flexible, you can adjust it
for a different player model with very little work. First step is to download this file, the player.qc. Like the original player.qc it will still handle player stuff, like frames, but wont handle everything it use to handle. Now open it up and look at what is already in there. You should have the scratch header, a few definitions and some globals like ANIM_BASIC, and you should also have a bunch of frame definitions. You will soon see how ANIM_BASIC and the others will be used. After all the other code paste in this function:
void () SetClientFrame =
{
// note: call whenever weapon frames are called!
if (self.anim_time > time)
return; //don't call every frame, if it is the animations will play too fast
self.anim_time = time + 0.1;
local float anim_change, run;
if (self.velocity_x || self.velocity_y)
run = TRUE;
else
run = FALSE;
anim_change = FALSE;
// check for stop/go and animation transitions
if (run != self.anim_run && self.anim_priority == ANIM_BASIC)
anim_change = TRUE;
if (anim_change != TRUE)
{
if (self.frame < self.anim_end)
{ // continue an animation
self.frame = self.frame + 1;
return;
}
if (self.anim_priority == ANIM_DEATH)
{
if (self.deadflag == DEAD_DYING)
{
self.nextthink = -1;
self.deadflag = DEAD_DEAD;
}
return; // stay there
}
}
// return to either a running or standing frame
self.anim_priority = ANIM_BASIC;
self.anim_run = run;
if (self.velocity_x || self.velocity_y)
{ // running
self.frame = $rockrun1;
self.anim_end = $rockrun6;
}
else
{ // standing
self.frame = $stand1;
self.anim_end = $stand5;
}
};
This is the heart of the animations, this one function can do everything required to run
the animations. anim_run is used to tell when the player is running or not, and to know when
to switch to and from running. anim_priority is used to know if you are attacking, or in
pain, or dead. anim_time is used to keep the frames from playing too fast. anim_end tells
the code when a scene ends, it would not look good if player just went through all the
animations when he is just standing still. Now open the client.qc and past this into the top
of the PlayerPreThink:
SetClientFrame ();
Final part before you can compile. Open the progs.src and add an entry for player.qc above
the client.qc entry, then compile. Now run quake and turn on the chase cam, run around. The
player animates!Step 2, what about pain, and death animations? Well currently you don't take pain, or die. In this step we will make the player take pain and die, next step we will take care of the pain and death animations. Now create a new file, call it damage.qc. This will do what combat.qc use to do, damage.qc is a better name for it. Make sure you add an entry fo the damage.qc in the progs.src, above the player.qc entry. Paste this into the damage.qc:
/*
+------+
|Damage|
+------+-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-+
| Scratch http://www.inside3d.com/qctut/scratch.shtml |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-+
| T_Damage and other like functions |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-+
*/
/*
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
T_Damage
The damage is coming from inflictor, but get mad at attacker
This should be the only function that ever reduces health.
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
*/
void(entity targ, entity inflictor, entity attacker, float damage) T_Damage=
{
local vector dir;
local entity oldself;
if (!targ.takedamage)
return;
// used by buttons and triggers to set activator for target firing
damage_attacker = attacker;
// figure momentum add
if ( (inflictor != world) && (targ.movetype == MOVETYPE_WALK) )
{
dir = targ.origin - (inflictor.absmin + inflictor.absmax) * 0.5;
dir = normalize(dir);
targ.velocity = targ.velocity + dir*damage*8;
}
// check for godmode
if (targ.flags & FL_GODMODE)
return;
// add to the damage total for clients, which will be sent as a single
// message at the end of the frame
if (targ.flags & FL_CLIENT)
{
targ.dmg_take = targ.dmg_take + damage;
targ.dmg_save = targ.dmg_save + damage;
targ.dmg_inflictor = inflictor;
}
// team play damage avoidance
if ( (teamplay == 1) && (targ.team > 0)&&(targ.team == attacker.team) )
return;
// do the damage
targ.health = targ.health - damage;
if (targ.health <= 0)
{
Killed (targ, attacker);
return;
}
// react to the damage
oldself = self;
self = targ;
if (self.th_pain)
self.th_pain (attacker, damage);
self = oldself;
};
The T_Damage function has only the needed functions for now. More can be added in later
tutorials. Once all health is drained the Killed function is called, we need to add that
function. Just under the header add this function:
/*
=-=-=-=-=
Killed
=-=-=-=-=
*/
void(entity targ, entity attacker) Killed =
{
local entity oself;
if (targ.health < -99)
targ.health = -99; // don't let sbar look bad if a player
targ.takedamage = DAMAGE_NO;
targ.touch = SUB_Null;
oself = self;
self = targ; // self must be targ for th_die
self.th_die ();
self = oself;
};
Now the player can take damage and die, but there is currently nothing that gives damage.
So we are going to make the player drown. Paste this function in at the bottom of damage.qc:
/*
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
WaterMove
Can be used for clients or monsters
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
*/
void() WaterMove =
{
if (self.movetype == MOVETYPE_NOCLIP)
return;
if (self.health < 0)
return;
if (self.waterlevel != 3)
{
self.air_finished = time + 12;
self.dmg = 2;
}
else if (self.air_finished < time && self.pain_finished < time)
{ // drown!
self.dmg = self.dmg + 2;
if (self.dmg > 15)
self.dmg = 10;
T_Damage (self, world, world, self.dmg);
self.pain_finished = time + 1;
}
if (self.watertype == CONTENT_LAVA && self.dmgtime < time)
{ // do damage
self.dmgtime = time + 0.2;
T_Damage (self, world, world, 6*self.waterlevel);
}
else if (self.watertype == CONTENT_SLIME && self.dmgtime < time)
{ // do damage
self.dmgtime = time + 1;
T_Damage (self, world, world, 4*self.waterlevel);
}
};
Now open the client.qc and past this into the top of the PlayerPreThink:
WaterMove ();
The player takes damage and dies... Oh yeah you need to add a few new definitions! Open
the defs.qc and go down to the bottom and paste these in there:
// Damge.qc entity damage_attacker; .float pain_finished, air_finished, dmg, dmgtime;Now we want to add these to the bottom of PutClientInServer in the client.qc
self.th_die = PlayerDie;
And in the player.qc paste this into the bottom:
void () PlayerDie =
{
self.view_ofs = '0 0 -8';
self.angles_x = self.angles_z = 0;
self.deadflag = DEAD_DYING;
self.solid = SOLID_NOT;
self.movetype = MOVETYPE_TOSS;
self.flags = self.flags - (self.flags & FL_ONGROUND);
if (self.velocity_z < 10)
self.velocity_z = self.velocity_z + random()*300;
};
Now compile and go for a swim in lava.Step 3, animations and sounds are needed to make pain and death more realistic. We will start with precaching the sounds we will use. Open the main.qc and paste this into the precaches function:
// pain sounds
precache_sound ("player/drown1.wav"); // drowning pain
precache_sound ("player/drown2.wav"); // drowning pain
precache_sound ("player/lburn1.wav"); // slime/lava burn
precache_sound ("player/lburn2.wav"); // slime/lava burn
precache_sound ("player/pain1.wav");
precache_sound ("player/pain2.wav");
precache_sound ("player/pain3.wav");
precache_sound ("player/pain4.wav");
precache_sound ("player/pain5.wav");
precache_sound ("player/pain6.wav");
// death sounds
precache_sound ("player/h2odeath.wav"); // drowning death
precache_sound ("player/death1.wav");
precache_sound ("player/death2.wav");
precache_sound ("player/death3.wav");
precache_sound ("player/death4.wav");
precache_sound ("player/death5.wav");
We are going to use all these sounds in pain and death. To call the sounds you need to add
these function into the player.qc, above PlayerDie for pain:
/*
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Pain sound, and Pain animation function
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
*/
void() PainSound =
{
if (self.health < 0)
return;
self.noise = "";
if (self.watertype == CONTENT_WATER && self.waterlevel == 3)
{ // water pain sounds
if (random() <= 0.5)
self.noise = "player/drown1.wav";
else
self.noise = "player/drown2.wav";
}
else if (self.watertype == CONTENT_SLIME || self.watertype == CONTENT_LAVA)
{ // slime/lava pain sounds
if (random() <= 0.5)
self.noise = "player/lburn1.wav";
else
self.noise = "player/lburn2.wav";
}
if (self.noise)
{
sound (self, CHAN_VOICE, self.noise, 1, ATTN_NORM);
return;
}
//don't make multiple pain sounds right after each other
if (self.pain_finished > time)
return;
self.pain_finished = time + 0.5;
local float rs;
rs = rint((random() * 5) + 1); // rs = 1-6
if (rs == 1)
self.noise = "player/pain1.wav";
else if (rs == 2)
self.noise = "player/pain2.wav";
else if (rs == 3)
self.noise = "player/pain3.wav";
else if (rs == 4)
self.noise = "player/pain4.wav";
else if (rs == 5)
self.noise = "player/pain5.wav";
else
self.noise = "player/pain6.wav";
sound (self, CHAN_VOICE, self.noise, 1, ATTN_NORM);
};
void () PlayerPain =
{
if (self.anim_priority < ANIM_PAIN)
{ // call only if not attacking and not already in pain
self.anim_priority = ANIM_PAIN;
self.frame = $pain1;
self.anim_end = $pain6;
}
PainSound ();
};
PainSound take care of running the pain sounds (go figure). PlayerPain sets the frames to
use and calls the PainSound function. Now the player needs to use the PlayerPain function,
so paste this into the bottom of the PutClientInServer function in the client.qc:
self.th_pain = PlayerPain;
You should be able to compile now and have the player go through pain so you can see and
hear it. What about death? Paste this into the player.qc just under PlayerPain, and above
PlayerDie:
/*
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Death sound, and Death function
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
*/
void() DeathSound =
{
local float rs;
rs = rint ((random() * 4) + 1); // rs = 1-5
if (self.waterlevel == 3) // water death sound
self.noise = "player/h2odeath.wav";
else if (rs == 1)
self.noise = "player/death1.wav";
else if (rs == 2)
self.noise = "player/death2.wav";
else if (rs == 3)
self.noise = "player/death3.wav";
else if (rs == 4)
self.noise = "player/death4.wav";
else if (rs == 5)
self.noise = "player/death5.wav";
sound (self, CHAN_VOICE, self.noise, 1, ATTN_NONE);
};
And then just below that in PlayerDie add this to the bottom:
local float rand;
rand = rint ((random() * 4) + 1); // rand = 1-5
self.anim_priority = ANIM_DEATH;
if (rand == 1)
{
self.frame = $deatha1;
self.anim_end = $deatha11;
}
else if (rand == 2)
{
self.frame = $deathb1;
self.anim_end = $deathb9;
}
else if (rand == 3)
{
self.frame = $deathc1;
self.anim_end = $deathc15;
}
else if (rand == 4)
{
self.frame = $deathd1;
self.anim_end = $deathd9;
}
else
{
self.frame = $deathe1;
self.anim_end = $deathe9;
}
DeathSound();
Now that finishes up this tutorial. The new way of handling frames has produced smaller,
and I think better code.
|