Tutorial *70*
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.


 
Sign up
Login:
Passwd:
[Remember Me]