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. |