Chapter 4: Creating Realistic Weapons Part 2 - Realism
Requirements
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