
GreyBear's Quake 2 Tutorials
Chapter
4: Creating Realistic Weapons Part 2 - Realism
Requirements
Contents
Introduction
Hello again. My you are an
impatient bunch! I guess that means that Part One was what you were wanting to
see though, so I'm not complaining. OK, here we go with Part Two, adding
realism to your new weapon. We're going to add the ability to track the ammo capacity
of a weapon per clip, and the ability to model and animate the weapon's ammo
exhaustion and reloading sequences. No more of those 'fire till you drop'
weapons for us... But first, like a bad penny, here is...
GreyBear's
Quake 2 programming philosophy
My philosophy about making
code changes is this:
Make your changes in
your own code files whenever possible. That means for our mods, we'll be
adding NEW files to the Quake 2 DLL, not merely inserting our mods into the
original files. Why? Well, two reasons. One, it's much easier to find your mods
if you keep them in your own files, named by you with descriptive names that
you can remember. Two, it maintains the integrity of the original code. As
programmers we should respect the work of others. Rather than just hack it up,
we should add to it gracefully, and clearly mark our additions as our own.
When we can't follow
the above, as in modifying global include files, or modifying global structs,
we will segregate our mods in the code, and clearly mark them as our additions.
I also highly recommend you use a consistent marker, like your initials. That
way, you can search the code for your initials and easily find every mod you
make in id code. Again, it makes for easy maintenance.
In the body of the tutorial text,
code modifications made by us will have a + sign in a comment field along with my initials, to clue
you in that the line was added to the original surrounding code.
Why
bother with realism?
Good question, and if the
answer is 'I don't know' for you, then you might want to toddle over to Qdevels
and read the cool new Sonic Railgun tutorial that's up.
My reason to bother is
that I enjoy the challenge of creating things that are realistic and present a
challenge to the player. It's easy to grab a BFG or a chaingun and hose away
until your opponent is gibs on the floor, but how well can you do it when you
have to count rounds and reload when you run out? It adds a level of tension
and complexity to the game. If you doubt my word, give it a try. There's no
feeling in the world like shooting at your opponent and seeing the slide go
back on your weapon and hearing the 'click' that tells you you're out of ammo,
and your butt is exposed.
However, if realism isn't
your bag, that's OK too. You can modify the techniques you'll learn here to add
all kinds of weird SF effects to weapons. That's the point of all my verbiage,
is to help you grasp the concepts behind the concrete examples I
present, so you can do your own thing, and not be limited to cutting and
pasting what tutorials provide.
OK, enough of the sermon,
let's get our hands dirty.
Tracking
ammo and clips
Real world weapons have
limited ammo capacities. We want to model those capacities accurately to
convince the player that he's using a specific weapon. In the case of the Mark
23 SOCOM pistol, the ammo capacity is 12 rounds per clip, and the player will
have to change clips every 12 rounds to reload and continue firing. So, to keep
track of the number of rounds fired, and the maximum round capacity of a single
clip, we can use two variables, Mk23_rds, and Mk23_max. The first is the number
of rounds remaining in the current clip, and the second is that maximum number
of rounds the weapon will hold. We can add them to the g_local.h file to
make them available to all code within the DLL. They go into the client
struct like so:
float respawn_time; // can respawn when time > this //+BD - Weapon magazine capacities and rounds left int Mk23_max; int Mk23_rds; //+BD end new variable add
Then
we need to alter the enum struct that holds the current weapon state. It's also
in g_local.h:
typedef enum { WEAPON_READY, WEAPON_ACTIVATING, WEAPON_DROPPING, WEAPON_FIRING, //+BD added to animate weapon reload and last round WEAPON_END_MAG, WEAPON_RELOADING, //+BD end add} weaponstate_t;
Next,
we need to make sure that when the game begins, that the weapon is loaded. In
Part one, I showed you how to make the Mark 23 your default weapon, replacing
the Blaster. If you followed that example, your life is easy. You might recall
that I gave you an extra credit assignment to give yourself ammo when you
started the game. Did you do it? Well, OK, this once I'll give you the answer
to an extra credit problem. Open up p_client.c and add the following
code to the InitClientPersistant() function:
memset (&client->pers, 0, sizeof(client->pers)); //+BD 2/7 Give the user a pistol instead of a blaster //+BD - item = FindItem("Blaster"); item = FindItem("Mk23"); //+BD end add client->pers.selected_item = ITEM_INDEX(item); client->pers.inventory[client->pers.selected_item] = 1; client->pers.weapon = item;
And
the following code to PutClientInServer():
client_respawn_t resp; //+BD added new declaration to give ammo gitem_t *item; //+BD and got to the end of the function gi.linkentity (ent); //+BD and give yourself 4 clips (4 X 12 = 48) item = FindItem("bullets"); Add_Ammo(ent,item,48); //+BD ...and set the max clip size, then fill the current mag... client->Mk23_max = 12; client->Mk23_rds = client->Mk23_max; //+BD end add // force the current weapon up client->newweapon = client->pers.weapon; ChangeWeapon (ent);}
Explanation: What we've done is replaced the
default weapon (the Blaster) with our own Mark 23. We've set the current weapon
as the Pistol to make it come up when we start the game. Then, we've hijacked
the item struct to grab the ammo item, and given ourselves 4 12 round clips of
ammo for our weapon, and set the number of rounds in the pistol to 12, in other
words we've inserted a full clip.
Adding
new animations
Now that we have our clip
and rounds left stuff figured out, we need to make it useful. This is the meat
of this tutorial, and there's a lot of code slinging gonna happen, so pay
attention.
We're going to modify the Weapon_Generic()function
to play animations for the last round of a clip when it's fired, and an
animation to reload the weapon. Then, we'll add the code to the firing
functions for the Pistol itself so that Weapon_Generic() gets the right
messages at the right time.
Open up p_weapon.c
and find the Weapon_Generic() function. Immediately above the function
call declaration, you'll see a list of #defines. Add the new defines for
reload and last round as shown below:
#define FRAME_FIRE_FIRST (FRAME_ACTIVATE_LAST + 1) #define FRAME_IDLE_FIRST (FRAME_FIRE_LAST + 1) #define FRAME_DEACTIVATE_FIRST (FRAME_IDLE_LAST + 1) //+BD - Added to incorporate reload and last round animations #define FRAME_RELOAD_FIRST (FRAME_DEACTIVATE_LAST +1) #define FRAME_LASTRD_FIRST (FRAME_RELOAD_LAST +1) //+BD end add
Explanation: These defines let us refer easily
to specific frames in the model animation sequence. Note that the sequence of
animations in the model become important, as I outlined in Part one.
Now, let's dig into the Weapon_Generic()
function itself. Because we have quite a bit of code to insert, I'm going to
list the entire function here:
//+BD Create a local define to make changing clip size easy #define MK23MAG 12 void Weapon_Generic (edict_t *ent, int FRAME_ACTIVATE_LAST, int FRAME_FIRE_LAST, int FRAME_IDLE_LAST, int FRAME_DEACTIVATE_LAST, /*+BD added*/ int FRAME_RELOAD_LAST, int FRAME_LASTRD_LAST, /*+BD end add*/ int *pause_frames, int *fire_frames, void (*fire)(edict_t *ent)) { int n; //+BD - Added Reloading weapon, done manually via a cmd if( ent->client->weaponstate == WEAPON_RELOADING) { if(ent->client->ps.gunframe < FRAME_RELOAD_FIRST || ent->client->ps.gunframe > FRAME_RELOAD_LAST) ent->client->ps.gunframe = FRAME_RELOAD_FIRST; else if(ent->client->ps.gunframe < FRAME_RELOAD_LAST) { ent->client->ps.gunframe++; //+BD - Check weapon to find out when to play reload sounds if(stricmp(ent->client->pers.weapon->pickup_name, "Mk23") == 0) { if(ent->client->ps.gunframe == 48) gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/clipout.wav"), 1, ATTN_NORM, 0); else if(ent->client->ps.gunframe == 60) gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/clipin.wav"), 1, ATTN_NORM, 0); } } else { ent->client->ps.gunframe = FRAME_IDLE_FIRST; ent->client->weaponstate = WEAPON_READY; if(stricmp(ent->client->pers.weapon->pickup_name, "Mk23") == 0) { if(ent->client->pers.inventory[ent->client->ammo_index] >= ent->client->Mk23_max) ent->client->Mk23_rds = ent->client->Mk23_max; else ent->client->Mk23_rds = ent->client->pers.inventory[ent->client->ammo_index]; } } } //+BD - Empty or unloaded weapon if( ent->client->weaponstate == WEAPON_END_MAG) { if(ent->client->ps.gunframe < FRAME_LASTRD_LAST) ent->client->ps.gunframe++; else ent->client->ps.gunframe = FRAME_LASTRD_LAST; } if (ent->client->weaponstate == WEAPON_DROPPING) { if (ent->client->ps.gunframe == FRAME_DEACTIVATE_LAST) { ChangeWeapon (ent); return; } ent->client->ps.gunframe++; return; } if (ent->client->weaponstate == WEAPON_ACTIVATING) { if (ent->client->ps.gunframe == FRAME_ACTIVATE_LAST) { ent->client->weaponstate = WEAPON_READY; ent->client->ps.gunframe = FRAME_IDLE_FIRST; return; } //+BD - Check the current weapon to find out when to play reload sounds if(stricmp(ent->client->pers.weapon->pickup_name, "Mk23") == 0) { if(ent->client->ps.gunframe == 3) gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/mk23sld.wav"), 1, ATTN_NORM, 0); ent->client->Mk23_max = MK23MAG; //set mag rounds ent->client->Mk23_rds = MK23MAG; //fill the mag... } ent->client->ps.gunframe++; return; } if ((ent->client->newweapon) && (ent->client->weaponstate != WEAPON_FIRING)) { ent->client->weaponstate = WEAPON_DROPPING; ent->client->ps.gunframe = FRAME_DEACTIVATE_FIRST; return; } if (ent->client->weaponstate == WEAPON_READY) { if (((ent->client->latched_buttons|ent->client->buttons) & BUTTON_ATTACK)) { ent->client->latched_buttons &= ~BUTTON_ATTACK; if ((!ent->client->ammo_index) || ( ent->client->pers.inventory[ent->client->ammo_index] >= ent- >client->pers.weapon->quantity)) { ent->client->ps.gunframe = FRAME_FIRE_FIRST; ent->client->weaponstate = WEAPON_FIRING; // start the animation ent->client->anim_priority = ANIM_ATTACK; if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) { ent->s.frame = FRAME_crattak1-1; ent->client->anim_end = FRAME_crattak9; } else { ent->s.frame = FRAME_attack1-1; ent->client->anim_end = FRAME_attack8; } } else { if (level.time >= ent->pain_debounce_time) { gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/noammo.wav"), 1, ATTN_NORM, 0); ent->pain_debounce_time = level.time + 1; } //+BD - Disabled for manual weapon change //NoAmmoWeaponChange (ent); } } else { if (ent->client->ps.gunframe == FRAME_IDLE_LAST) { ent->client->ps.gunframe = FRAME_IDLE_FIRST; return; } if (pause_frames) { for (n = 0; pause_frames[n]; n++) { if (ent->client->ps.gunframe == pause_frames[n]) { if (rand()&15) return; } } } ent->client->ps.gunframe++; return; } } if (ent->client->weaponstate == WEAPON_FIRING) { for (n = 0; fire_frames[n]; n++) { if (ent->client->ps.gunframe == fire_frames[n]) { if (ent->client->quad_framenum > level.framenum) gi.sound(ent, CHAN_ITEM, gi.soundindex("items/damage3.wav"), 1, ATTN_NORM, 0); fire (ent); break; } } if (!fire_frames[n]) ent->client->ps.gunframe++; if (ent->client->ps.gunframe == FRAME_IDLE_FIRST+1) ent->client->weaponstate = WEAPON_READY; }}
Whew!
That was a chunk o' code! A couple of things to note. You may want to cut and
paste this in as a replacement for your Weapon_Generic() function
altogether to make your life easy. If you decide to do this, be careful about
preserving lines. The web format forces me to break lines in unnatural places,
and they can cause the compiler to puke out really weird errors.
You'll need now to do a
search for ALL instances of Weapon_Generic, and add 0,0 to the function
call to preserve the correct number of arguments, otherwise you'll get a bunch
of return errors coupled with incorrect parameter errors.
You'll also need to find
the Weapon_Pistol() function we created in Part one, and uncomment the
two values in the Weapon_Generic() call at the end of the function.
Explanation: All that code was to basically
implement two things; reload and last round animation. When the WEAPON_END_MAG
flag is set we play the last round animation. When the WEAPON_RELOADING flag is
et, we play the reload animation. We do a check first to insure that the weapon
that's current is our Mark 23, because playing these animations for other
weapons would cause frame errors on the console. Not a pretty sight.
Wiring it
up
Now we need to revisit our
Pistol_Fire() code, and uncomment some code that was commented out for
Part one to function properly. You've seen the placement of all the other parts
to make reloading and last round animations work, so as you go through and
uncomment code, you'll see it all fall together (I hope). Here's the
code again with annotations on what to uncomment:
// +BD NEW CODE BLOCK //====================================================================== //Mk23 Pistol - Ready for testing - Just need to replace the blaster anim with //the correct animation for the Mk23. void Pistol_Fire(edict_t *ent) { int i; vec3_t start; vec3_t forward, right; vec3_t angles; int damage = 15; int kick = 30; vec3_t offset; //If the user isn't pressing the attack button, advance the frame and go away.... if (!(ent->client->buttons & BUTTON_ATTACK)) { ent->client->ps.gunframe++; return; } ent->client->ps.gunframe++; //Oops! Out of ammo! if (ent->client->pers.inventory[ent->client->ammo_index] < 1) { ent->client->ps.gunframe = 6; if (level.time >= ent->pain_debounce_time) { gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/noammo.wav"),1, ATTN_NORM, 0); ent->pain_debounce_time = level.time + 1; } //Make the user change weapons MANUALLY! //NoAmmoWeaponChange (ent); return; } //Hmm... Do we want quad damage at all in NS2? //No, but if you do, uncomment the following 5 lines //if (is_quad) //{ // damage *= 4; // kick *= 4; //} //Calculate the kick angles for (i=1 ; i<3 ; i++) { ent->client->kick_origin[i] = crandom() * 0.35; ent->client->kick_angles[i] = crandom() * 0.7; } ent->client->kick_origin[0] = crandom() * 0.35; ent->client->kick_angles[0] = ent->client->machinegun_shots * -1.5; // get start / end positions VectorAdd (ent->client->v_angle, ent->client->kick_angles, angles); AngleVectors (angles, forward, right, NULL); VectorSet(offset, 0, 8, ent->viewheight-8); P_ProjectSource (ent->client, ent->s.origin, offset, forward, right, start); //BD 3/4 - Added to animate last round firing... // Don't worry about this now. We'll come back to it later. //+BD OK, it's LATER now. Uncomment these next 9 lines if (ent->client->pers.inventory[ent->client->ammo_index] == 1 || (ent->client->Mk23_rds == 1)) { //Hard coded for reload only. ent->client->ps.gunframe=64; ent->client->weaponstate = WEAPON_END_MAG; fire_bullet (ent, start, forward, damage, kick, DEFAULT_BULLET_HSPREAD, DEFAULT_BULLET_VSPREAD,MOD_Mk23); ent->client->Mk23_rds--; } else { //If no reload, fire normally. fire_bullet (ent, start, forward, damage, kick, DEFAULT_BULLET_HSPREAD, DEFAULT_BULLET_VSPREAD,MOD_Mk23); //+BD and uncomment these two also ent->client->Mk23_rds--; } //BD - Use our firing sound gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/mk23fire.wav"), 1, ATTN_NORM, 0); //Display the yellow muzzleflash light effect gi.WriteByte (svc_muzzleflash); gi.WriteShort (ent-g_edicts); //If not silenced, play a shot sound for everyone else gi.WriteByte (MZ_MACHINEGUN | is_silenced); gi.multicast (ent->s.origin, MULTICAST_PVS); PlayerNoise(ent, start, PNOISE_WEAPON); //Ammo depletion here. ent->client->pers.inventory[ent->client->ammo_index] -= ent->client->pers.weapon->quantity; }
Explanation: We uncommented the portions of our
Pistol_Fire() function that keep track of whether we're out of ammo or
not. Weapon_Generic() handles reloading, via input from a new user
command. That's the last thing we need to add.
Next, open up g_cmds.c.
I'm going to break my own philosophical rule here, but I do have a reason.
Because of the way weapons are bound into the code, it's hard to separate them
out into a new code block. It can be done, but the work isn't worth the
reward. So, we're going to just add our new command function to the end of g_cmds.c,
declare it at the top, and be done with it. So sue me. Here's the function:
//+BD ENTIRE CODE BLOCK NEW // Cmd_Reload_f() // Handles weapon reload requests void Cmd_Reload_f (edict_t *ent) { int rds_left; //+BD - Variable to handle rounds left //+BD - If the player is dead, don't bother if(ent->deadflag == DEAD_DEAD) { gi.centerprintf(ent, "I know you're a hard ass,\nBUT YOU'RE FUCKING DEAD!!\n"); return; } //First, grab the current magazine max count... if(stricmp(ent->client->pers.weapon->pickup_name, "Mk23") == 0) rds_left = ent->client->Mk23_max; else //We should never get here, but... //BD 5/26 - Actually we get here quite often right now. Just exit for weaps that we // don't want reloaded or that never reload (grenades) { gi.centerprintf(ent,"Where'd you train?\nYou can't reload that!\n"); return; } if(ent->client->pers.inventory[ent->client->ammo_index]) { if((ent->client->weaponstate != WEAPON_END_MAG) && (ent->client->pers.inventory[ent->client->ammo_index] < rds_left)) { gi.centerprintf(ent,"Buy a clue-\nYou're on your last magazine!\n"); } else //Set the weaponstate... ent->client->weaponstate = WEAPON_RELOADING; } else gi.centerprintf(ent,"Pull your head out-\nYou've got NO AMMO!\n"); } //+BD END CODE BLOCK
Now,
jump to the top of the g_cmds.c file and add the following line:
//+BD local declaration of our new command function void Cmd_Reload_f (edict_t *ent); //+BD end add /*****************************************************************************/ char *ClientTeam (edict_t *ent) {
And
finally, enable the command invocation by adding it to the list of commands to
look for within ClientCommand(), also within the g_cmds.c file:
else if (Q_stricmp (cmd, "invdrop") == 0) Cmd_InvDrop_f (ent); //+BD - for handling reload commands else if (Q_stricmp (cmd, "reload") == 0) Cmd_Reload_f (ent);
Explanation: We connected up the reload and
last round animations to the triggers that cause them to be played. We also
created a new command, Cmd_Reload_f() that lets the player reload the
weapon when it exhausts it's clip. Note that the player can reload at any time.
So, what happens if a player reloads in the middle of a clip? Well, the player
still gets a new clip of ammo, BUT the old partial clip stays with him
(we never throw away good ammo). The last clip fired becomes that partial clip,
so unless the player is counting rounds he can get surprised by a short clip!
The
result
We now have a functional,
realistic Quake2 weapon that shoots a defined clip, and runs out of ammo, like
a real weapon. It can also be reloaded until all ammo is exhausted.
To create a keyboard
shortcut for your reload command, you can add a line to the config.cfg file:
bind r "cmd reload"
You can
also type this at the console. Then, just press the R key to reload your
weapon.
Next time we'll add the
ability to fire flares from our pistol. This is a bit wacky, but there are
flare rounds available for some pistols. It also allows me to demonstrate to
you how to make a weapon capable of firing more than one type of ammunition.
Until next time, then... ENJOY!
Contacting
the Author
Questions? Problems? Write
me, and I'll try to answer your question or help you with debugging. Send your
queries to me here.
Tutorial written by GreyBear