| Tutorials are copyrighted by thier author and the Inside3D staff. And may be used in any Quake modification provided that the author and the Inside3D staff are credited. Please direct any comments to the author. |
Created By: | Boolean |
eMail: | d.perry@utoronto.ca |
Difficulty Scale: | Medium |
Introduction
This is an expansion of The Devil's tutorial on jamming shotguns. Although a good tutorial and a neat effect, I thought it was silly that you could still see your shotgun right in front of you while you were theoretically 'unjamming' it. So I played with his code, and made it so the weapon model disappears when you want it to, in a fairly easy interface, that can be easily expanded to, say, reloading weapons (like in TeamFortress).
Please be patient with me, as this is my first tutorial! If you have any comments, good or bad, please mail me!
Since The Devil did the 'weapon unusable' part of the jamming shotgun by extending the player's attack_finished property, my modification is intended to work the same way.
Note
As in any programming language, there are two ways to do things in QuakeC: the pretty way, and the better way. The pretty way makes the code easiest to read, but in QC's case, at the expense of using more memory. The better way is much less readable, but makes the best use of memory that's already allocated. Since each person has their own preference, I've tried to cover both aspects here. If it's confusing let me know.
Step 1:
If you're doing this the pretty way, open up your DEFS.QC and add the following to the end:
// Jamming Shotgun .float weapon_down; // Boolean: is weapon unusable? .void() raise_weapon; // Func to raise/re-ready weaponSave and exit. If you're doing this the better way then you don't need to mess with your DEFS.QC.
Step 2:
Open up your WEAPONS.QC, this is where most of the work gets done.
Right at the top is a function called W_Precache. This caches up all the sounds that the weapons use. Add the following two lines to the end of the function:
precache_sound ("plats/train2.wav"); // jamming shotgun precache_sound ("weapons/lock4.wav"); // unjamming shotgunNow find the function W_FireShotgun. After it, add this (watch the comments to see which parts to add!):
/* =============== W_FireShotUnjam =============== Called when the shotgun has been unjammed. */ void() W_FireShotUnjam= { sound (self, CHAN_WEAPON, "weapons/lock4.wav", 1, ATTN_NORM); // load sound centerprint(self,"Got it fixed.\n"); // Tell them it's all good self.weaponmodel = "progs/v_shot.mdl"; // show the gun self.weaponframe = 0; /* Pretty way */ self.weapon_down = 0; // shotgun is no longer jammed self.raise_weapon = SUB_Null; // clean up pointer to function /* Better way */ // self.aflag = 0; // use to say gun isn't down // self.think1 = SUB_Null; // no longer need to re-raise gun }; /* ============= W_FireShotJam ============= Called below, when the shotgun jams up. */ void() W_FireShotJam= { self.currentammo = self.ammo_shells = self.ammo_shells - 1; // lose the shell sound(self, CHAN_WEAPON,"plats/train2.wav",1,ATTN_NORM); // click sound centerprint(self,"Damn, it jammed!\n"); // Tell them what happened self.weaponmodel = ""; // hide the gun self.weaponframe = 0; /* Pretty way */ self.weapon_down = 1; self.raise_weapon = W_FireShotUnjam; /* Better way */ // self.aflag = 1; // use to say gun is down // self.think1 = W_FireShotUnjam; // func to call to raise gun };I've added some 'gee-whiz' to The Devil's jamming routine, but it's essentially the same as his. The thing to consider right now is, what is happening? Well, when it is determined that the shotgun will jam, W_FireShotJam() is called, and what it does is hide the weaponmodel (sets it to ""), set a flag to remind us the weapon's stuck, and keep track of what function to call when the thing's no longer jammed. This function (in this case, W_FireShotUnjam) sets the model back up again.
But what determines when the shotgun jams? (If you read The Devil's tutorial you should know!) What calls the raise_weapon() function when it's no longer jammed? Read on!
Step 3:
Still in WEAPONS.QC, find the function W_Attack. Find the part that looks like this:
else if (self.weapon == IT_SHOTGUN) { player_shot1 (); W_FireShotgun (); self.attack_finished = time + 0.5; }Add some code so it looks like this:
else if (self.weapon == IT_SHOTGUN) { local float r; // make a new float r=random(); // makes r a random number /* Pretty way */ self.weapon_down=0; // we'll say gun isn't down yet self.raise_weapon=SUB_Null; // so we don't need to raise it /* Better way */ // self.aflag=0; // we'll say gun isn't down yet // self.think1=SUB_Null; // so we don't need to raise it player_shot1 (); if(r<0.03) // give it a 1 in 50 chance of jamming { W_FireShotJam(); // jam it, jim self.attack_finished=time+5; // take 5sec to unjam } else { W_FireShotgun (); self.attack_finished = time + 0.5; } }It should be fairly obvious to see what's happening here: we're flipping a 50-sided coin, and if it comes up heads, rather than firing a shot like normal, we're jamming the shotgun with a simple call to W_FireShotJam() and saying it'll take 5 seconds to unjam.
We're almost done the main part, but not quite.
Step 4:
Still in WEAPONS.QC, find the function W_WeaponFrame:
/* ============ W_WeaponFrame Called every frame so impulse events can be handled as well as possible ============ */ void() W_WeaponFrame = { if (time < self.attack_finished) return; ImpulseCommands (); // check for attack if (self.button0) { SuperDamageSound (); W_Attack (); } };Add a bit to make it look like this:
/* ============ W_WeaponFrame Called every frame so impulse events can be handled as well as possible ============ */ void() W_WeaponFrame = { if (time < self.attack_finished) return; /* Start of Pretty way */ if (self.weapon_down == 1) // make sure to fix downed gun { self.weapon_down = 0; // no longer down self.raise_weapon(); // function called to raise gun self.raise_weapon=SUB_Null; // reset pointer } /* End of Pretty way, Start of Better way */ // if (self.aflag == 1) // make sure to fix downed gun // { // self.aflag = 0; // no longer down // self.think1(); // function called to raise gun // self.think1=SUB_Null; // reset pointer // } /* End of Better way */ ImpulseCommands (); // check for attack if (self.button0) { SuperDamageSound (); W_Attack (); } };Here's where the magic happens! Like the comment says, this gets called every frame, so what better place to check whether your gun is still jammed? The if statement at the start of the function keeps anything from happening if your attack isn't finished yet, this is what The Devil did to keep your jammed gun from firing for a few seconds. I just added a call to the current player's raise_weapon() function, so that when the attack *is* finished, the gun magically pops back up, ready to fire again.
You can compile and run your new mod now. Check it out! Every so often, your shotgun will jam up, and while your onscreen avatar is unjamming it for you, it vanishes from your field of view. Rock, eh? However, try picking up an ammo box or backpack while your gun is down. The gun reappears, but you still can't shoot it until you get the "Got it fixed." message. That defeats the whole purpose of this tutorial, so let's fix it, shall we? (:
Step 5:
Open up WEAPONS.QC again, and find the function W_SetCurrentAmmo. Each of the weapons will have an entry in this function like the following:
if (self.weapon == IT_AXE) { self.currentammo = 0; self.weaponmodel = "progs/v_axe.mdl"; self.weaponframe = 0; } }Change each and every entry so it looks like this:
if (self.weapon == IT_AXE) { self.currentammo = 0; if (self.weapon_down == 0) // Pretty way // if (self.aflag == 0) // Better way { self.weaponmodel = "progs/v_axe.mdl"; self.weaponframe = 0; } }I've used the axe here for illustration, but don't forget to make the change to all the weapons you see (perhaps even new ones you've added yourself!). Theoretically, for this example you only need to change the case for if (self.weapon == IT_SHOTGUN), but then you'd have to come back and change it all again for, say, a jamming rocket launcher! (:
Step 6:
Save and exit, compile and play! Your shotgun jams occasionally, and won't pop up until you get it good and fixed. How sweet is that? What's even sweeter is that, now that we've done the grunt work, we can add more downed weapons just by creating functions for the weapon to be lowered and raised, and by calling them like we did above. Think of the possibilities! Jammed weapons, reloading weapons... dare I say it... dropped weapons? (:
About the Better way:
This is a technique I picked up from reading ShockMan's tutorial on making a cool rifle. You see, for each new variable we add in DEFS.QC, we're either:
-making a global variable that every entity can see; or
-making a class variable that every entity gets.
We did the latter in the Pretty way of jamming our shotgun. This is generally a Bad Thing, because every entity is getting new fields that they may never use, and that's sucking up memory for no good reason. Unfortunately, id has done the same thing with some of the variables and functions; fortunately, we can take advantage of it by re-using them!
So, the way we made our routine Better was to take a .float (aflags: it's only used on doors) and a .void() function (think1(): it's only used in SUBS.QC, for monsters), variables that every entity has anyway, and reuse them. Like I said, I got the idea from ShockMan's tutorial, and that's also where I got the idea to use aflags. I found think1() in the Quake C Reference Manual, which is available for download at www.modscene.com. You can also find "hidden gem" variables by browsing through DEFS.QC.
The downsides to doing it this way are twofold: one is that it's not as obvious what the code is doing when your variables aren't intuitively named. Good comments are essential in this case! The other problem is that you have to keep track of which "hidden gem" variables you are using in which parts of your code. If you use aflags and think1() for this tutorial, you can't use them again on the player entity for your own work; or if you've already used aflags and think1() for something else on the player entity, you have to find different "hidden gems" to use for this mod!
So there you go. A jamming shotgun, AND a programming style lesson all in one day! Hope you have as much fun playing with my tutorial as I did playing with Devil's and ShockMan's! (: