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.

NOTE: 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