TUTORIAL 27 - Reloading Weapons
by Martin Dominguez - Drive C:
It seems everyone has been e-mailing me for the code for reloading your weapons.
I didn't quite understand how or why they thought I had the code.
I DID actually have the completed code, so writing the tutorial wasn't very difficult.
The effects of this code are simple. Everyone knows that not every single weapon
can carry the same amount of ammo, right? In Quake 3 though, somehow they can
hold 200 rounds regardless of the weapon. We're here to change that!
Both cgame and game will be modified:
g_cmds.c
g_items.c
g_client.c
g_weapon.c
g_local.h
bg_public.h
bg_pmove.c
cg_draw.c
Once again, I must disclaim the lack of comment used in the source here.
Please: When you are typing up the code, tell yourself what you are
doing, okay? It really, really helps. For everyone who doesn't
know of this yet, you create an in-source comment with
the two slash characters "//". This leaves the remainder
of the line for your remark.
1. The Premise
Quake 3 coders know that the ammo quantities are stored in the
variable entity->client->ps.ammo, which is an array
of 16(NUM_MAX_WEAPONS) integers. The reload code,
however would require that there be a second array of
that exact size to store the ammount held in
your "bag" versus the ammount in the weapon.
A simple console command would cause an empty
weapon to "reload."
Here's the problem: The function
that checks to see if we can pick up ammo
from the ground is located in bg_misc.c at line ~755.
Since it's located in BG(both games?), it has access to
neither game or cgame structures, but only shared structures.
Thus the ammount in your "bag" must stay in ps.ammo.
When we fire the weapon, ammo must be depleted from
the weapon's clip variable (our new variable).
Firing the weapon and removing ammo also occurs in
BG (bg_pmove.c, line ~1525), so somehow both variables
must occur in the ps(playerstate) structure.
Since no modifications to playerstate have actually resulted
in a stable game, we cannot simply create an integer
array ps.clipammo.
We must use some tricky manipulations to get around the
problem of both ammo quantities being needed in BG.
Here's the trick: since weapon firing functions
get passed from bg to game, we can remove the
ammo depletions from bg_pmove.c to
g_weapon.c and place our ammo in clip
variable in the client structure.
A. Go to g_local.h to about line 277 in struct gclient_s and add:
int lastKillTime; // for multiple kill rewards
qboolean fireHeld; // used for hook
gentity_t *hook; // grapple hook if out
// Our Ammo Variable, ps.ammo, now refers to Bag Ammo
int clipammo[MAX_WEAPONS];
// timeResidual is used to handle events that happen every second
// like health / armor countdowns and regeneration
int timeResidual;
};
void Cmd_Reload( gentity_t *ent );
int ClipAmountForWeapon( int w );
Now it's time to relocate the ammo depletion code to game from bg.
B1. Go into g_weapon.c to line ~572 and insert:
void FireWeapon( gentity_t *ent ) {
//Remove Ammo if not infinite
if ( ent->client->clipammo[ ent->client->ps.weapon ] != -1 ) {
ent->client->clipammo[ ent->client->ps.weapon ]--;
}
if (ent->client->ps.powerups[PW_QUAD] ) {
s_quadFactor = g_quadfactor.value;
} else {
s_quadFactor = 1;
}
B2. Go into bg_pmove.c line ~1525 in PM_Weapon()and use the
/*..........*/ marks to set this text to remark:
// take an ammo away if not infinite
// This will go within "game" in g_weapon.c because
// ammo is stored in gclient_t
/*if ( pm->ps->ammo[ pm->ps->weapon ] != -1 ) {
pm->ps->ammo[ pm->ps->weapon ]--;
}*/
// fire weapon
PM_AddEvent( EV_FIRE_WEAPON );
Another really important thing that should be mentioned is the
check for out of ammo, which is associated with our second
major problem. The check for out of ammo occurs in bg just
above the depletion code, and quite honestly I would prefer
to leave it there. The problem is that bg has no access
to the game structure "gclient_t," so it cannot possibly
check to see if the present weapon is depleted entirely
as the ammo in clip variables are stored there.
Here is where we create a second variable, this time
located in the client->ps.stats indexes. All the out
of ammo check needs to know is the ammo held in the
present weapon's clip, so we can create a variable
ps.stats[STAT_AMMO] that will update the ammo in the
presently equipped weapon automatically. Later, you
will see why this new variable is also needed
(it has to do with cgame, can you figure it out?).
C1. To create the variable, goto bg_public.h, line ~166 and insert:
// player_state->stats[] indexes
typedef enum {
STAT_HEALTH,
STAT_HOLDABLE_ITEM,
STAT_WEAPONS, // 16 bit fields
STAT_ARMOR,
STAT_DEAD_YAW,
STAT_AMMO, // Ammo Held in Current Weapon
STAT_CLIENTS_READY,
C2. Let's set the STAT_AMMO variable to be updated on ClientEndFrame(); insert in g_active.c line ~841:
// burn from lava, etc
P_WorldEffects (ent);
// apply all the damage taken this frame
P_DamageFeedback (ent);
// add the EF_CONNECTION flag if we haven't gotten commands recently
if ( level.time - ent->client->lastCmdTime > 1000 ) {
ent->s.eFlags |= EF_CONNECTION;
} else {
ent->s.eFlags &= ~EF_CONNECTION;
}
//Update the Ammo Amount in weapon
ent->client->ps.stats[STAT_AMMO] = ent->client->clipammo[ent->client->ps.weapon];
ent->client->ps.stats[STAT_HEALTH] = ent->health;
C3. Let's now use the STAT_AMMO variable to check for out of ammo. Insert in
bg_pmove.c, in PM_Weapon(), line ~1520:
pm->ps->weaponstate = WEAPON_FIRING;
// check for out of ammo
// modified for changing ammo from ps->ammo to client->clipammo updated to ps->stats[STAT_AMMO]
if ( ! /*pm->ps->ammo[ pm->ps->weapon ]*/ pm->ps->stats[STAT_AMMO] ) {
PM_AddEvent( EV_NOAMMO );
pm->ps->weaponTime += 500;
return;
}
// take an ammo away if not infinite
D1. HypoThermia
brought this to my attention, and I
think we will all appreciate this. When you pick up a gun from the ground, its
nice that it has ammo in it so you don't have to reload it after picking it up.
Go to g_items.c to line 139 and we will see the PickupWeapon function. You will
later learn what the ClipAmountForWeapon() function is. Simply modify:
if ( ent->count < 0 ) {
quantity = 0; // None for you, sir!
} else {
if ( ent->count ) {
quantity = ent->count;
} else {
quantity = ent->item->quantity;
}
// dropped items and teamplay weapons always have full ammo
if ( ! (ent->flags & FL_DROPPED_ITEM) && g_gametype.integer != GT_TEAM ) {
// respawning rules
// drop the quantity if the already have over the minimum
/*if ( other->client->ps.ammo[ ent->item->giTag ] < quantity ) {
quantity = quantity - other->client->ps.ammo[ ent->item->giTag ];
} else {
quantity = 1; // only add a single shot
}*/
}
}
// add the weapon
other->client->ps.stats[STAT_WEAPONS] |= ( 1 << ent->item->giTag );
/*Add_Ammo( other, ent->item->giTag, quantity );*/
quantity = ClipAmountForWeapon(ent->item->giTag);
if (other->client->clipammo[ent->item->giTag] > 0 )
Add_Ammo( other, ent->item->giTag, quantity );
else
other->client->clipammo[ent->item->giTag] = quantity;
if (ent->item->giTag == WP_GRAPPLING_HOOK)
other->client->ps.ammo[ent->item->giTag] = -1; // unlimited ammo
// team deathmatch has slow weapon respawns
if ( g_gametype.integer == GT_TEAM ) {
return RESPAWN_TEAM_WEAPON;
}
D2. Go to g_client.c to line 931, in ClientSpawn(), and give ammo for player to start:
client->ps.stats[STAT_WEAPONS] = ( 1 << WP_MACHINEGUN );
client->ps.stats[STAT_WEAPONS] = ( 1 << WP_MACHINEGUN );
if ( g_gametype.integer == GT_TEAM ) {
client->ps.ammo[WP_MACHINEGUN] = 50;
} else {
client->ps.ammo[WP_MACHINEGUN] = 100;
}
client->clipammo[WP_MACHINEGUN] = ClipAmountForWeapon(WP_MACHINEGUN);
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_GAUNTLET );
client->ps.ammo[WP_GAUNTLET] = -1;
client->ps.ammo[WP_GRAPPLING_HOOK] = -1;
2. Creating the Command and the ClipAmountForWeapon Function
Reloading the weapon will be activated by the server-side command "/reload."
As we all remember from before: go into g_cmds.c down to line ~1101 in ClientCommand()
to the else if statements, and insert this code:
else if (Q_stricmp (cmd, "gc") == 0)
Cmd_GameCommand_f( ent );
else if (Q_stricmp (cmd, "setviewpos") == 0)
Cmd_SetViewpos_f( ent );
else if (Q_stricmp (cmd, "reload") == 0)
Cmd_Reload( ent );
Jus before the function ClientCommand() in that file(g_cmds.c), we will add our actual reload function:
/*
==================
Cmd_Reload
==================
*/
void Cmd_Reload( gentity_t *ent ) {
int weapon;
int amt;
int ammotoadd;
weapon = ent->client->ps.weapon;
amt = ClipAmountForWeapon(weapon);
ammotoadd = amt;
if (ent->client->clipammo[weapon] >= ClipAmountForWeapon(weapon)) {
trap_SendServerCommand( ent-g_entities,
va("print \"No need to reload.\n\""));
return;
}
ent->client->ps.weaponstate = WEAPON_DROPPING;
ent->client->ps.torsoAnim = ( ( ent->client->ps.torsoAnim & ANIM_TOGGLEBIT )
^ ANIM_TOGGLEBIT ) | TORSO_DROP;
ent->client->ps.weaponTime += 2500;
//Play a sound maybe: thats up to you.
if (ent->client->ps.ammo[weapon] == 0) return;
//We can only add ammo(to weapon) what we need
if (ent->client->clipammo[weapon] > 0) {
ammotoadd -= ent->client->clipammo[weapon];
}
//We can only remove (from bag) what ammo we have
if (ent->client->ps.ammo[weapon] < ammotoadd) {
ammotoadd = ent->client->ps.ammo[weapon];
}
//Remove the ammo from bag
ent->client->ps.ammo[weapon] -= ammotoadd;
//Add the ammo to weapon
ent->client->clipammo[weapon] += ammotoadd;
}
This function simply reloads the weapon. It checks to see first
if no reloading is necessary. It then creates the graphics
of dropping the weapon, removes from the backpack supply of ammo and adds to the weapon clip.
As you saw above, we used the integer function ClipAmountForWeapon to determine
how much ammo each weapon can hold. We need to add
this function right below the Cmd_Reload, but you won't
be able to copy and paste. Here, you will tell the game
exactly how much ammo each weapon can hold, and that
is determined by the coder, not the tutorial.
I have however, set up an example. Hopefully it will help
you to create your own clip quantities. You should be able to figure it out.
Staying in g_cmds.c add this function:
/*
==================
ClipAmountForWeapon for Cmd_Reload
==================
*/
int ClipAmountForWeapon( int w ) {
//How much each clip holds
if ( w == WP_MACHINEGUN ) return 24;
else if ( w == WP_GRENADE_LAUNCHER ) return 1;
else if ( w == WP_SHOTGUN ) return 1;
else return 12; //this wont happen unless you copy-and-paste too much
}
3. Displaying Ammo Quantities
All of us love Q3's HUD right? Yeah Right! Well anyway, I think
it would be nice if we ammended an already skimpy
eye-sore with just one other number. Can you guess
what that number is? Well anyway, I told you before that
we would be using the STAT_AMMO variable again.
Since cgame has no access to our Ammo in Gun
quantities, there is no way that we can display
the ammo in the gun. STAT_AMMO, however, can
be utilized in cgame. Only the current weapon's
ammo needs to be displayed on the HUD, so I think we're in business.
The displaying of statistics on your mod's HUD is
really up to you, so don't feel you have
to do what I am doing here. When I wrote this, I chose a
very simple way of displaying Ammo in Gun,
and I'm sure there are more economical ways to show it.
Anyway, go in cg_draw.c (which contains functions to draw
everything that is 2D on the screen) to line 429 and insert:
CG_DrawPic( 370 + CHAR_WIDTH*3 + TEXT_ICON_SPACE, 432,
ICON_SIZE, ICON_SIZE, cgs.media.armorIcon );
}
}
//
// ammo in gun
//
value = ps->stats[STAT_AMMO];
if (value > 0 ) {
//First Draw the 3D Model of the Weapon
angles[YAW] = ( cg.time & 2047 ) * 360 / 2048.0;
origin[0] = 80;
origin[1] = 0;
origin[2] = 0;
CG_Draw3DModel( CHAR_WIDTH*3 + TEXT_ICON_SPACE, 360, 96,
96, cg_weapons[ cent->currentState.weapon ].weaponModel,
0, origin, angles );
//Draw the Text
trap_R_SetColor( colors[0] );
CG_DrawField (0, 384, 3, value);
trap_R_SetColor( NULL );
// if we didn't draw a 3D icon, draw a 2D icon for weapon
if ( !cg_draw3dIcons.integer && cg_drawIcons.integer ) {
CG_DrawPic( CHAR_WIDTH*3 + TEXT_ICON_SPACE, 384, ICON_SIZE, ICON_SIZE,
cg_weapons[ cg.predictedPlayerState.weapon ].weaponIcon );
}
}
Also, I forgot to mention the out of ammo and low ammo warnings.
They are executed in cg_draw.c but originate in cg_playerstate.c
at line 50. These warnings respond to the amount
of ammo in bag so they are quite useless for the reload
modification. I never liked them anyway. If you comment (with the //)
out line 52, you can remove the low ammo warning
while preserving the out of ammo warning. To make it simpler though,
just go into cg_draw.c and comment out line 1821.
Well that's the tutorial. I hope everyone enjoyed it and that it is easy
to understand and useful. To all, good luck and happy coding.
Drive C:
|