Adding Firing Modes to your weapons
For help, see the VDC Editing Help and Wikipedia cleanup process. Also, remember to check for any notes left by the tagger at this article's talk page.
Contents
Purpose
Add the ability to toggle firing modes with Half-Life 2 weaponry.
Route
Add firingmode
key, function and variables so the firing modes are able to be changed by the base class
Solution
We'll define a new key on the keyboard configuration and when pressed this key will call a new FireMode() method on the current weapon. The functionality will be implemented following the current implementation of PrimaryAttack(), as you know when you press the left mouse button the PrimaryAttack() method of the current weapon is called.
First thing to do is to define a new key for the keyboard options menu. We'll do this by going to /scripts directory and opening the file kb_act.lst (this is the keyboard actions setup file). After opening the file you will see all the actions within the game with its corresponding descriptions. To add this new functionality we'll be adding it after the "+attack2" action (this is the secondary fire) so...
After this line:
"+attack2" "#Valve_Secondary_Attack"
Add the following line:
"+firemode" "#MOD_Fire_Mode"
Now you want this label to be translated for everyone in the correct language. Now I need you to go to your mod's directory, let's assume it is called MyMod, so you have '../Steam/SteamApps/SourceMods/MyMod'. Now go to your MyMod/resource directory. There should be language files for example mymod_english.txt (mymod because that is your directory name of the mod...) If you don't have this file, extract the ones from the GCF files and rename them to mymod_english.txt, mymod_dutch.txt, mymod_french.txt
Assuming you have extracted/Created/already have the files, open mymod_english.txt.
And add the line
"MOD_Fire_Mode" "Change Fire Mode"
Once we added the new key to this file we have the option to add the "default" key for the action in case the user resets all controls to the default ones. This is optional but if you want to define the default key just open the file kb_def.lst (keyboard default assignment keys) and add a new line, preferably after "+attack2" to keep a correlation with kb_act.lst:
After this line:
"MOUSE2" "+attack2"
Add the following line (here I'm assigning the letter 'x' as the default key if the user resets the controls):
"x" "+firemode"
Note: The first time you run the game remember to assign a key to the new action! Wink
That completes the first piece of adding the new action. Now open the source code because for the rest of the tutorial we'll be working with it... I did my changes using the Single-player source code (for some reason I'm reluctant to work with the MP source code until VALVe releases the HL2DM source code!)
To complete the definition of the new action key we'll be doing changes on two files: in_buttons.h and in_main.cpp. These two files handle all the player's input so open those two files and read on...
Let's start with in_buttons.h as this one is a very simple change. Within this file you will see all the possible actions defined for the game. Because the variable holding this bit masks is an integer we can only define 32 actions. So, let's add our new action to the bit mask...
After this line:
#define IN_BULLRUSH (1 << 22)
Add the following line:
#define IN_FIREMODE (1 << 23)
That completes all the changes to the in_buttons.h header file! (I told you it was going to be easy).
Now, let's do the necessary changes to in_main.cpp to complete the handle of the new key...
First, we'll define a variable to be able to hold the state of the key. This variable will hold a value showing if the key is being pressed or not... to keep the correlation we'll keep adding our new code right after the "+attack2" code so...
After this line:
static kbutton_t in_attack2;
Add the following line:
static kbutton_t in_firemode;
Now, we'll need to link the commands coming from the player to the proper internal functions changing the state of the variable in_firemode so...
After this line:
static ConCommand endattack2("-attack2", IN_Attack2Up);
Add the following two lines:
static ConCommand startfiremode("+firemode", IN_FireModeDown); static ConCommand endfiremode("-firemode", IN_FireModeUp);
By declaring these two ConCommand
variables we are linking the different actions (+firemode is triggered when the user presses the key and -firemode is triggered when the user releases the key) to two functions, these functions are the ones that will change the status of variable in_firemode.
Let's define those two functions. Note: this new code is located above the last piece but I just wanted to keep it here so you see the relationship between all the code we are adding...
After this line:
void IN_Attack2Up(void) {KeyUp(&in_attack2);}
Add the following two lines:
void IN_FireModeDown(void) {KeyDown(&in_firemode);} void IN_FireModeUp(void) {KeyUp(&in_firemode);}
These are very small and simple functions. Every time you press the key the function KeyDown() with our new variable will be called and every time we released the key the KeyUp() function will be called instead.
Now unto the last piece. For the last piece to handle the new key we'll be changing the CInput::GetButtonBits() method. This method gets executed constantly (I guess every frame) by the game to check for the status of the different keys and creates an integer by using the bit mask we defined inside the header file in_buttons.h. I guess it compresses the different key statuses into an integer so it's easier to pass to the player instead of passing 32 different variables... anyway...
After this line:
CalcButtonBits( bits, IN_ATTACK2, s_ClearInputState, &in_attack2, bResetState );
Add the following line:
CalcButtonBits( bits, IN_FIREMODE, s_ClearInputState, &in_firemode, bResetState );
We have now completed the handle of the new key... of course is does nothing yet but at least you have an idea on how to add new keys to your mod... Now, let's add a new FireMode() method to the weapons. I said weapons because I'll be changing the base class CBaseCombatWeapon that almost all weapons derive from. Although I will be implementing this alternate fire mode only on the RPG but if you want to implement it for the other weapons too you'll get more than a clear picture on how to do it...
Let's start with the header file first as these changes are plain and simple so let's open basecombatweapon_shared.h and let's define the new virtual function to handle the fire rate/mode changes:
After this line:
virtual void SecondaryAttack( void ) { return; } // do "+ATTACK2"
Add the following line:
virtual void FireMode( void ) { return; } // do "+FIREMODE"
Here we are just declaring that the class CBaseCombatWeapon will have a new method called FireMode(). It takes no parameters and it doesn't return anything (just like SecondaryAttack()!). As you can see we also defined the new function to do nothing and return. By doing this all the weapons will do nothing unless we overwrite the method on the derived weapon class.
Those are all the changes for the header file, now let's go into the source code... open the file basecombatweapon_shared.cpp... in this file we'll be adding some small code to call the FireMode() method every time the key is pressed. To do this we'll change the method CBaseCombatWeapon::ItemPostFrame(), this method is called for every frame, within this method you will also find the source code calling PrimaryAttack().
After this piece of code handling the reload of a weapon:
// ----------------------- // Reload pressed / Clip Empty // ----------------------- if ( pOwner->m_nButtons & IN_RELOAD && UsesClipsForAmmo1() && !m_bInReload ) { // reload when reload is pressed, or if no buttons are down and weapon is empty. Reload(); m_fFireDuration = 0.0f; }
Add the following piece of code to handle our new fire rate/mode change:
// ----------------------- // Change fire rate/mode // ----------------------- if ( pOwner->m_nButtons & IN_FIREMODE ) { FireMode(); }
As you might have guessed already, m_nButtons contains the bit mask with the different state of the keys being pressed. What we'll do here is check that bit mask to see if the player pressed our new defined key to trigger the FireMode() method.
Now, let's add our new action to the following block so we don't call the WeaponIdle() method when the player is switching the fire rate or fire mode so let's add one more OR to the condition so it looks like this...
// -----------------------
// No buttons down
// -----------------------
if (!((pOwner->m_nButtons & IN_ATTACK) ||
(pOwner->m_nButtons & IN_ATTACK2) ||
(pOwner->m_nButtons & IN_FIREMODE) || // <-- Add this condition too!
(pOwner->m_nButtons & IN_RELOAD)))
{ // no fire buttons down or reloading
if ( !ReloadOrSwitchWeapons() && ( m_bInReload == false ) )
WeaponIdle();
}
Here we just completed the second piece of the puzzle...
HL2:DM continuation
His tutorial might have worked back on the first release of the SDK or on the single player HL2 Mod, but after that, some thing’s have to be changed.
I decided that I would think about the idea and come up with my own solution. What has firing modes... first thing that came to mind... machine guns. The sub machine gun and the pulse rifle should have firing modes... so I opened up their code...
After reading the tutorial on changing the pistol implementation (t3chn0r is the second greatest commenter I have ever met!) I started understanding how the code for weapons worked. But let’s go through this step by step.
Where are we going to put the code
I don’t like to have the same stuff, code-wise, in two locations... so I did some investigation... up on the top of the class definitions for the two weapons is a line of beauty...
Inside weapon_smg1.cpp
class CWeaponSMG1 : public CHL2MPMachineGun { ... }
Inside weapon_ar2.h
class CWeaponAR2 : public CHL2MPMachineGun { ... }
In case you aren’t familiar with C++ code... this says that the sub machine gun and pulse rifle are children of the machine gun! And what better way to change kids than to kick their parents into shape! What we are looking at is this: CHL2MPMachineGun Woah... lets go find find that class!
Okay, there are two files. You are going to have to open both. One is the class declaration and definition of some constants; lets go there first.
Inside weapon_hl2mpbase_machinegun.h:
//====== Copyright © 1996-2005, Valve Corporation, All rights reserved. ======//
//
// Purpose:
//
//============================================================================//
#include "weapon_hl2mpbase.h"
#ifndef BASEHLCOMBATWEAPON_H
#define BASEHLCOMBATWEAPON_H
#ifdef _WIN32
#pragma once
#endif
#if defined( CLIENT_DLL )
#define CHL2MPMachineGun C_HL2MPMachineGun
#endif
//=========================
// Machine gun base class
//=========================
class CHL2MPMachineGun : public CWeaponHL2MPBase
{
public:
DECLARE_CLASS( CHL2MPMachineGun, CWeaponHL2MPBase );
DECLARE_DATADESC();
CHL2MPMachineGun();
DECLARE_NETWORKCLASS();
DECLARE_PREDICTABLE();
void PrimaryAttack( void ); // Primary attack function (mouse1)
// Default calls through to m_hOwner, but plasma weapons can override and shoot projectiles here.
virtual void ItemPostFrame( void ); // Called after each frame of animation
virtual void FireBullets( const FireBulletsInfo_t &info ); // Self-explanatory
virtual bool Deploy( void ); // What happens when the gun is chosen from inventory
virtual const Vector &GetBulletSpread( void ); // Where the bullets go
int WeaponSoundRealtime( WeaponSound_t shoot_type ); // Sound for the weapon when shot
// Utility function
static void DoMachineGunKick( CBasePlayer *pPlayer, float dampEasy, float maxVerticleKickAngle, float fireDurationTime, float slideLimitTime ); // this is the recoil function
private:
CHL2MPMachineGun( const CHL2MPMachineGun & ); // this is the constructor, it creates the class and loads variables as we choose
protected:
int m_nShotsFired; // Number of consecutive shots fired
float m_flNextSoundTime; // real-time clock of when to make next sound
};
#endif // BASEHLCOMBATWEAPON_H
Well… hmm… not too confusing eh? I added some comments to try to clear up some of it. I think its time to delve into this code. In order for my implementation to work, you will have to add a few variables,
protected:
bool m_bFMReady; // Firemode Ready switch
bool m_bFMAutomatic; // Firemode Automatic switch
int m_nBurstRate; // Variable burst rate
int m_nFireMode; // Firemode value (0 safety,1 single fire,2 burst,3 auto)
int m_nShotsLeft; // Firemode remaining shots
and one function
void FireMode( void ); // Firemode implementer
Where to put them and why
In my code I added the firemode function to the public section and dropped it in right below primaryattack. The variables are member. This in short means they are only able to be accessed by the class itself. They are put into the private section. When you finish that, it should look like this :
//=========================
// Machine gun base class
//=========================
class CHL2MPMachineGun : public CWeaponHL2MPBase
{
public:
DECLARE_CLASS( CHL2MPMachineGun, CWeaponHL2MPBase );
DECLARE_DATADESC();
CHL2MPMachineGun();
DECLARE_NETWORKCLASS();
DECLARE_PREDICTABLE();
void PrimaryAttack( void );
void FireMode( void );
// Default calls through to m_hOwner, but plasma weapons can override and shoot projectiles here.
virtual void ItemPostFrame( void );
virtual void FireBullets( const FireBulletsInfo_t &info );
virtual bool Deploy( void );
virtual bool Holster( CBaseCombatWeapon *pSwitchingTo );
virtual const Vector &GetBulletSpread( void );
int WeaponSoundRealtime( WeaponSound_t shoot_type );
// Utility function
static void DoMachineGunKick( CBasePlayer *pPlayer, float dampEasy, float maxVerticleKickAngle, float fireDurationTime, float slideLimitTime );
private:
CHL2MPMachineGun( const CHL2MPMachineGun & );
protected:
bool m_bFMReady; // Firemode Ready switch
bool m_bFMAutomatic; // Firemode Automatic switch
int m_nBurstRate; // Variable burst rate
int m_nFireMode; // Firemode value (0 safety,1 single fire,2 burst,3 auto)
int m_nShotsLeft; // Firemode remaining shots
int m_nShotsFired; // Number of consecutive shots fired
float m_flNextSoundTime; // real-time clock of when to make next sound
};
They are placed there so that:
- variables are not able to be accessed outside of this class and
- so our firing mode function can be.
That is quite simply all its going to take to get our mod to have firing modes in this file. All we have to do now is add the appropriate function and actions to cause our mod to work how we want it to.
Open up weapon_hl2mpbase_machinegun.cpp. If you go down to line 20 or so, you should see some macro driven variable definitions, my best guess is that there is something behind the scenes that hl2 needs in order to communicate with the server variables. Lets add ours to this list…
BEGIN_DATADESC( CHL2MPMachineGun )
DEFINE_FIELD( m_nShotsFired, FIELD_INTEGER ),
DEFINE_FIELD( m_nBurstRate, FIELD_INTEGER ), // Burst fires this many bullets
DEFINE_FIELD( m_bFMReady, FIELD_BOOLEAN ), // firing mode change readiness
DEFINE_FIELD( m_bFMAutomatic, FIELD_BOOLEAN ), // firing mode change readiness
DEFINE_FIELD( m_nShotsLeft, FIELD_INTEGER ), // keeps track of shots left in this firing mode
DEFINE_FIELD( m_nFireMode, FIELD_INTEGER ), // keeps track of firing mode
DEFINE_FIELD( m_flNextSoundTime, FIELD_TIME ),
END_DATADESC()
That was easy enough. You might be wondering where I pulled those FIELD variables from. I guessed. But it was an educated one. And later I remembered this page:
hmm, next we should start small. Lets implement the FireMode function. This is what mine looks like:
//----------------------------------------------------------
// Purpose: Implementation of firemode in all machine guns
//----------------------------------------------------------
void CHL2MPMachineGun::FireMode( void ){
// Grab the current player
CBasePlayer *pPlayer = ToBasePlayer( GetOwner() );
if (!pPlayer) // if there isn't one, return...
return;
// If player is pushing the firemode button and is okay to change mode,
if ( ( ( pPlayer->m_nButtons & IN_FIREMODE ) && ( m_bFMReady == true ) ) && !( pPlayer->m_nButtons & IN_ATTACK ) ){
if ( m_nFireMode == 1 ) // if mode was 0
m_nFireMode = 2; // set it to 1
else if ( m_nFireMode == 2 && m_bFMAutomatic) // if mode was 1
m_nFireMode = 3; // set it to 3
else // otherwise
m_nFireMode = 1; // set it to automatic
// play weapon sound so you know the button has been pressed
WeaponSound(EMPTY);
if (m_nFireMode == 2)// sets the counter to the correct amount (depending on firing mode
m_nShotsLeft = m_nBurstRate;
else
m_nShotsLeft = m_nFireMode;
// and set ready to false to keep from runnign through the list over and over
m_bFMReady = false;
DevMsg ("There are %d bullets being fired at a time...\\n", m_nFireMode);
}
return;
}
Drop that code into the weapon_hl2mpbase_machinegun.cpp file right after the PrimaryAttack() function. Now for some fixing and changing; we need our variables to have values when the game starts, so lets put that in the constructor. One way of doing this is as follows…
CHL2MPMachineGun::CHL2MPMachineGun()
{
m_nFireMode = 0;
m_nShotsLeft = m_nFireMode;
m_nBurstRate = 3;
m_bFMAutomatic = true;
}
which works absolutely fine, but im a fan of short, to the point code, and I was let in on a constructor secret...
CHL2MPMachineGun::CHL2MPMachineGun( void ): m_nFireMode(0), m_nShotsLeft(m_nFireMode), m_nBurstRate(3), m_bFMAutomatic(true)
{
}
It's just a question of code style.
We are almost done! Home Stretch time...
Next we have to ask ourselves, functionality wise, when will it be okay to change weapon mode? We could do it after a shot is taken or any other goofy time. The only thing that made sense to me is to only have it able to change when the player wasn’t changing it currently. Oh, side note, we also have to update our variables.
Down on the very bottom, is a function called: ItemPostFrame which is called after each frame is drawn, and thereby the best place, in my opinion, to keep our update ... well, up to date... (excuse the pun).
We are going to reset our m_nShotsLeft, and m_bFMReady variables so our gun works properly.
- m_nShotsLeft
- This one is going to be reset to the firing mode after each release of the primary trigger. so the burst is going to burst, wait till we release the trigger, and then repeat when we depress it again.
- m_bFMReady
- This one is going to be reset to true after we release the firing mode and primary button. this way we can make sure the firing mode only changes once per key press, and doesn't change while firing the weapon.
This is the finished function:
//----------------------------------
// Purpose: every frame, do things
//----------------------------------
void CHL2MPMachineGun::ItemPostFrame( void )
{
CBasePlayer *pOwner = ToBasePlayer( GetOwner() );
if ( pOwner == NULL )
return;
// Debounce the recoiling counter
if (!( pOwner->m_nButtons & IN_ATTACK )){ //when not using primary fire
if (m_nFireMode == 2)// sets the counter to the correct amount (depending on firing mode
m_nShotsLeft = m_nBurstRate;
else
m_nShotsLeft = m_nFireMode;
m_nShotsFired = 0;
}
if (!(pOwner->m_nButtons & IN_FIREMODE)) // when not pressing firemode button
m_bFMReady = true; // set ready to true
BaseClass::ItemPostFrame();
}
For general purposes, I also overloaded two functions so that my mod makes sense.
//--------------------------------
// Purpose: Reset our shots fired
//--------------------------------
bool CHL2MPMachineGun::Deploy( void )
{
m_nShotsFired = 0;
m_bFMReady = true;
return BaseClass::Deploy();
}
bool CHL2MPMachineGun::Holster( CBaseCombatWeapon *pSwitchingTo )
{
m_nShotsLeft = 0;
m_nFireMode = 0;
return BaseClass::Holster(pSwitchingTo);
}
Since my mod focuses on realism, we thought it wouldn't make sense to say that if we didn't do things for safety, such as safety a weapon when holstering it.
After that, all that’s left is the implementation of the firing mode change in the primary attack function. Lets go up there and have a look at what needs to happen…
We have this variable, m_nShotsLeft, which keeps track of how many shots are left before this firing mode is in need of a reset. It should be simple enough, lets give it a try.
Two simple if statements and a -- in the right spot, and a return statement should do the trick.
// If there is less than 1 shot left in this firing mode... return
if (m_nShotsLeft < 1)
return;
// If the firing mode is less than four, remove one from the shots left counter...
if (m_nFireMode < 3)
m_nShotsLeft--;
this code does the following in order of appearance:
- compare m_nShotsLeft to 1 (m_nShotsLeft <1)
- skip the rest of the function if shots left is less than 1
- compare m_nFiremode to 3 (m_nFiremode <3)
- if the firing mode is not 3 subtract 1 from m_nShotsLeft (m_nShotsLeft--)
This is the finished primary attack function.
void CHL2MPMachineGun::PrimaryAttack( void )
{
// If there is less than 1 shot left in this firing mode... return
if (m_nShotsLeft < 1)
return;
// If the firing mode is less than four, remove one from the shots left counter...
if (m_nFireMode < 3)
m_nShotsLeft--;
// Only the player fires this way so we can cast
CBasePlayer *pPlayer = ToBasePlayer( GetOwner() );
if (!pPlayer)
return;
// Abort here to handle burst and auto fire modes
if ( (UsesClipsForAmmo1() && m_iClip1 == 0) || ( !UsesClipsForAmmo1() && !pPlayer->GetAmmoCount(m_iPrimaryAmmoType) ) )
return;
m_nShotsFired++;
if (pPlayer->m_nButtons & IN_ATTACK)
DevMsg ("There are %d shots left...\\n", m_nShotsLeft);
// MUST call sound before removing a round from the clip of a CHLMachineGun
// FIXME: only called once, will miss multiple sound events per frame if needed
// FIXME: m_flNextPrimaryAttack is always in the past, it's not clear what'll happen with sounds
WeaponSound(SINGLE, m_flNextPrimaryAttack);
// Msg("%.3f\\n", m_flNextPrimaryAttack.Get() );
pPlayer->DoMuzzleFlash();
// To make the firing framerate independent, we may have to fire more than one bullet here on low-framerate systems,
// especially if the weapon we're firing has a really fast rate of fire.
int iBulletsToFire = 0;
float fireRate = GetFireRate();
while ( m_flNextPrimaryAttack <= gpGlobals->curtime )
{
m_flNextPrimaryAttack = m_flNextPrimaryAttack + fireRate;
iBulletsToFire++;
}
// Make sure we don't fire more than the amount in the clip, if this weapon uses clips
if ( UsesClipsForAmmo1() )
{
if ( iBulletsToFire > m_iClip1 )
iBulletsToFire = m_iClip1;
m_iClip1 -= iBulletsToFire;
}
CHL2MP_Player *pHL2MPPlayer = ToHL2MPPlayer( pPlayer );
// Fire the bullets
FireBulletsInfo_t info;
info.m_iShots = iBulletsToFire;
info.m_vecSrc = pHL2MPPlayer->Weapon_ShootPosition( );
info.m_vecDirShooting = pHL2MPPlayer->GetAutoaimVector( AUTOAIM_5DEGREES );
info.m_vecSpread = pHL2MPPlayer->GetAttackSpread( this );
info.m_flDistance = MAX_TRACE_LENGTH;
info.m_iAmmoType = m_iPrimaryAmmoType;
info.m_iTracerFreq = 2;
FireBullets( info );
// Factor in the view kick
AddViewKick();
if (!m_iClip1 && pPlayer->GetAmmoCount(m_iPrimaryAmmoType) <= 0)
{
// HEV suit - indicate out of ammo condition
pPlayer->SetSuitUpdate("!HEV_AMO0", FALSE, 0);
}
SendWeaponAnim( GetPrimaryAttackActivity() );
pPlayer->SetAnimation( PLAYER_ATTACK1 );
}
And that does it. As soon as your code looks like that, you are done.
Notes
- The guns both start out with safety on.
- This implements a rotating firing mode, it will actually go back to single fire after automatic, not burst. If enough people ask, I will add the safety as a secondary option otherwise its up to you.