Weapon Accuracy and Vector Projectiles

From Valve Developer Community
Jump to: navigation, search
Wikipedia - Letter.png
This article has multiple issues. Please help improve it or discuss these issues on the talk page. (Learn how and when to remove these template messages)
Dead End - Icon.png
This article has no links to other VDC articles. Please help improve this article by adding links that are relevant to the context within the existing text.
January 2024


Weapon Spread Customization

In this tutorial we will discuss some basics of how to effect the spread of projectiles from weapons such as the AR2, SMG1, Pistol, and Shotgun. This will include the effects of recoil parameters on simulating weapon spread as well as shot penalty timers and their effect on base combat weapons throughout your mod. This tutorial is a general overview as many different classes of weapons utilize camera and projectile vector generation differently. To start the tutorial, we need to discuss how the game determines shot angles.

Vector Cones & FOV

The bulk of the combat weapons in HL2 render the "spread" of projectiles based on a fixed "cone" or preset angle defined within a handful of places. These cones are based on the origin or center anchor point of the player's field of view or camera angle in first person. Below is an example from "weapon_smg1.cpp" which is typical for CHLMachineGun and CHLSelectFireMachineGun type weapons.

 virtual const Vector& GetBulletSpread( void )
 {
	 static const Vector cone = VECTOR_CONE_5DEGREES;
 	 return cone;
 }

One way most beginner mods will get the desired spread pattern or cone is to simply change the vector cone value to the closest desired degree value (Ex. "5DEGREES" is replaced with "1DEGREES" etc.) However this can have it's own issues, especially if we want to have several similar classes of weapons in the mod. To optimize certain weapons, we have to define vector cones specifically.

Note.pngNote:For weapons such as the AR2, some data will be contained in header files.

Basecombatweapon_shared.h'

We can find the declaration for "VECTOR_CONE_5DEGREES" as an example in 'basecombatweapon_shared.h':

// NOTE: The way these are calculated is that each component == sin (degrees/2)
#define VECTOR_CONE_PRECALCULATED	vec3_origin
#define VECTOR_CONE_1DEGREES		Vector( 0.00873, 0.00873, 0.00873 )
#define VECTOR_CONE_LRIFLE          Vector( 0.00980, 0.00980, 0.00980 )
#define VECTOR_CONE_SRIFLE    		Vector( 0.01261, 0.01261, 0.01261 )
#define VECTOR_CONE_LPISTOL    		Vector( 0.01474, 0.01474, 0.01474 )
#define VECTOR_CONE_SPISTOL    		Vector( 0.01648, 0.01658, 0.01658 )
#define VECTOR_CONE_2DEGREES		Vector( 0.01745, 0.01745, 0.01745 )
#define VECTOR_CONE_SUBMACHG		Vector( 0.02530, 0.02530, 0.02530 )
#define VECTOR_CONE_3DEGREES		Vector( 0.02618, 0.02618, 0.02618 )
#define VECTOR_CONE_PDW		        Vector( 0.03228, 0.03228, 0.03228 )
#define VECTOR_CONE_4DEGREES		Vector( 0.03490, 0.03490, 0.03490 )
#define VECTOR_CONE_5DEGREES		Vector( 0.04362, 0.04362, 0.04362 )
#define VECTOR_CONE_6DEGREES		Vector( 0.05234, 0.05234, 0.05234 )
#define VECTOR_CONE_7DEGREES		Vector( 0.06105, 0.06105, 0.06105 )
#define VECTOR_CONE_8DEGREES		Vector( 0.06976, 0.06976, 0.06976 )
#define VECTOR_CONE_9DEGREES		Vector( 0.07846, 0.07846, 0.07846 )
#define VECTOR_CONE_10DEGREES		Vector( 0.08716, 0.08716, 0.08716 )
#define VECTOR_CONE_11DEGREES		Vector( 0.09589, 0.09589, 0.09589 )
#define VECTOR_CONE_15DEGREES		Vector( 0.13053, 0.13053, 0.13053 )
#define VECTOR_CONE_20DEGREES		Vector( 0.17365, 0.17365, 0.17365 )

In this example, the data table has been customized to allow for multiple rifles and submachine guns to be used, each with a slightly different base spread pattern.

Movement and Recoil Penalty

Vanilla Half-Life 2 features some advanced recoil effects in first person view which effect the rumble and pitch of the player camera angle. For this example we will discuss how this is accomplished and how to manipulate these effects in-game to produce unique weapon characteristics for a mod using weapon_smg1.cpp. Below is the local recoil definitions specific to just the SMG1

void CWeaponSMG1::AddViewKick( void )
{
	#define	EASY_DAMPEN			1.5f
	#define	MAX_VERTICAL_KICK	17.5f	//Degrees
	#define	SLIDE_LIMIT			0.11f	//Seconds
	
	//Get the view kick
	CBasePlayer *pPlayer = ToBasePlayer( GetOwner() );

	if ( pPlayer == NULL )
		return;

	DoMachineGunKick( pPlayer, EASY_DAMPEN, MAX_VERTICAL_KICK, m_fFireDuration, SLIDE_LIMIT );
}

However the 'DoMachineGunKick' function is what controls the "rumble" effect specically. In this case 'SLIDE_LIMIT' refers to a time variable, expressed in seconds. To understand how the first person rumble effect is carried out, we need to look at 'basehlcombatweapon.cpp'

Basehlcombatweapon.cpp

The method that HL2 uses to build the player camera rumble or "view punch" effect for weapons like the SMG1, is defined in this file. Let's take a closer look at how this is done.

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CHLMachineGun::DoMachineGunKick( CBasePlayer *pPlayer, float dampEasy, float maxVerticleKickAngle, float fireDurationTime, float slideLimitTime )
{
	#define	KICK_MIN_X			0.2f	//Degrees
	#define	KICK_MIN_Y			0.2f	//Degrees
	#define	KICK_MIN_Z			0.1f	//Degrees

	QAngle vecScratch;
	
	//Find how far into our accuracy degradation we are
	float duration	= ( fireDurationTime > slideLimitTime ) ? slideLimitTime : fireDurationTime;
	float kickPerc = duration / slideLimitTime;

	// do this to get a hard discontinuity, clear out anything under 10 degrees punch
	pPlayer->ViewPunchReset( 10 );

	//Apply this to the view angles as well
	vecScratch.x = -( KICK_MIN_X + ( maxVerticleKickAngle * kickPerc ) );
	vecScratch.y = -( KICK_MIN_Y + ( maxVerticleKickAngle * kickPerc ) ) / 3;
	vecScratch.z = KICK_MIN_Z + ( maxVerticleKickAngle * kickPerc ) / 8;

	//Wibble left and right
	if ( random->RandomInt( -1, 1 ) >= 0 )
		vecScratch.y *= -1;

	//Wobble up and down
	if ( random->RandomInt( -1, 1 ) >= 0 )
		vecScratch.z *= -1;

	//If we're in easy, dampen the effect a bit
	if ( g_pGameRules->IsSkillLevel( SKILL_EASY ) )
	{
		for ( int i = 0; i < 3; i++ )
		{
			vecScratch[i] *= dampEasy;
		}
	}

	//Clip this to our desired min/max
	UTIL_ClipPunchAngleOffset( vecScratch, pPlayer->m_Local.m_vecPunchAngle, QAngle( 24.0f, 3.0f, 1.0f ) );

	//Add it to the view punch
	// NOTE: 0.5 is just tuned to match the old effect before the punch became simulated
	pPlayer->ViewPunch( vecScratch * 0.5 );
}

In this example the code used is unmodified. If we examine the expression for 'KickPerc' we notice that the fire duration time or time with the trigger pressed, is continually counted against the player during the rumble process. At a glance this is perfectly logical, however if the player does not release the trigger for several seconds, the gun will begin to bounce awkwardly in the air. To adjust this we can modify the code like this:

void CHLMachineGun::DoMachineGunKick( CBasePlayer *pPlayer, float dampEasy, float maxVerticleKickAngle, float fireDurationTime, float slideLimitTime )
{
	#define	KICK_MIN_X			0.2f	//Degrees
	#define	KICK_MIN_Y			0.2f	//Degrees
	#define	KICK_MIN_Z			0.1f	//Degrees

	QAngle vecScratch;
	
	//Find how far into our accuracy degradation we are
	float duration	= ( fireDurationTime > slideLimitTime ) ? slideLimitTime : fireDurationTime;
	float kickPerc = duration / slideLimitTime; //old implementation
	float kickPercy = slideLimitTime;   //new system. delete this line and switch kickpercy back to kickperc in  vecscratch lines below for old system

	// do this to get a hard discontinuity, clear out anything under 10 degrees punch
	pPlayer->ViewPunchReset( 30 );  // formerly 10

	//Apply this to the view angles as well
	vecScratch.x = -( KICK_MIN_X + ( maxVerticleKickAngle * kickPercy ) ) / ( 1 * dampEasy );    // is y axis or vertical. 
	vecScratch.y = -( KICK_MIN_Y + ( maxVerticleKickAngle * kickPercy ) ) / ( 2 * dampEasy );   // is really X axis or horizontal. remove / followed by dampeasy multiplier to return to old value
	vecScratch.z = KICK_MIN_Z + ( maxVerticleKickAngle * kickPercy ) / 8;

	//Wibble left and right
	if ( random->RandomInt( -1, 1 ) >= 0 )
		vecScratch.y *= -1;

	//Wobble up and down
	if ( random->RandomInt( -1, 1 ) >= 0 )
		vecScratch.z *= -1;

	//If we're in easy, dampen the effect a bit
	if ( fireDurationTime >= .3 )      // not going to worry about this being switched back. dampeasy was only for easy difficulty in default hl2
	{
		for ( int i = 0; i < 3; i++ )
		{
			vecScratch[i] *= dampEasy;
		}
	}

	//Clip this to our desired min/max
	UTIL_ClipPunchAngleOffset( vecScratch, pPlayer->m_Local.m_vecPunchAngle, QAngle( 24.0f, 3.0f, 1.0f ) );

	//Add it to the view punch
	// NOTE: 0.5 is just tuned to match the old effect before the punch became simulated
	pPlayer->ViewPunch( vecScratch * 0.5 );
}

Weapon_smg1.cpp

We have now defined both a custom spread vector, and made changes to the recoil parameters for our CHLMachineGun and CHLSelectFireMachineGun weapons. We can make additional parameters locally at weapon_smg1.cpp. The changes will locally define how much recoil our weapon has, and how much to adjust for player movement. First, let's start with the recoil from before.

void CWeaponMP5::AddViewKick( void )
{
	#define	EASY_DAMPEN			2.3f
	#define	MAX_VERTICAL_KICK	17.0f	//Degrees
	#define	SLIDE_LIMIT			0.11f	//Seconds

	//Get the view kick
	CBasePlayer *pPlayer = ToBasePlayer( GetOwner() );

	if ( pPlayer == NULL )
		return;

	DoMachineGunKick( pPlayer, EASY_DAMPEN, MAX_VERTICAL_KICK, m_fFireDuration, SLIDE_LIMIT ); //---Fire duration is now an empty value
}

We can manipulate how much overall muzzle climb we get by increasing or decreasing 'SLIDE_LIMIT'. We can do the same for our climb per shot through changes to 'MAX_VERTICAL_KICK'. Also "EASY_DAMPEN" is now a damping effect which will end after the first few shots. This allows for a smoother burst, before transitioning into a "sawtooth" pattern recoil full automatic fire. Lastly, if we want to make player movement effect our spread, we must look at the vector cone from earlier. Here is its again:

 virtual const Vector& GetBulletSpread( void )
 {
	static const Vector cone = VECTOR_CONE_5DEGREES;
   	return cone;
 }

We want to tell the game that if certain conditions are met, affect the accuracy, and only if the "owner" is the player. This is one way:

//----New Values which allow our "spread" to change from button input from player SP Only
	virtual const Vector& GetBulletSpread( void )
	{
		// Define "spread" parameters based on the "owner" and what they are doing
		static Vector p_duckCone = VECTOR_CONE_2DEGREES;
		static Vector npcCone = VECTOR_CONE_5DEGREES;
		static Vector p_standCone = VECTOR_CONE_3DEGREES;
		static Vector p_moveCone = VECTOR_CONE_4DEGREES;
		static Vector p_runCone = VECTOR_CONE_6DEGREES;
		static Vector p_jumpCone = VECTOR_CONE_9DEGREES;
		if ( GetOwner() && GetOwner()->IsNPC() )
			return npcCone;
		static Vector cone;
		// We must know the player "owns" the weapon before different cones may be used
		CBasePlayer *pPlayer = ToBasePlayer( GetOwnerEntity() );
		if ( pPlayer->m_nButtons & IN_DUCK )
           return p_duckCone;
		if ( pPlayer->m_nButtons & IN_FORWARD )
           return p_moveCone;
		if ( pPlayer->m_nButtons & IN_BACK )
           return p_moveCone;
		if ( pPlayer->m_nButtons & IN_MOVERIGHT )
           return p_moveCone;
		if ( pPlayer->m_nButtons & IN_MOVELEFT )
           return p_moveCone;
		if ( pPlayer->m_nButtons & IN_JUMP )
           return p_jumpCone;
		if ( pPlayer->m_nButtons & IN_SPEED )
           return p_runCone;
		if ( pPlayer->m_nButtons & IN_RUN )
           return p_runCone;
		else
           return p_standCone;

	}

We have established the parameters for how the vector cones are gathered. First, we told the game engine that NPCs must use a generic static cone. Second, we defined who "pPlayer" is by determining if the hl2 base player owns the gun. Third, we defined the parameters saying: "If the 'pPlayer' owns this gun, and they are pressing a button, and that button is assigned to 'duck', then use 'x' vector cone". Finally if none of the buttons are being pressed, but the owner is still the player, assume they are standing and use that vector cone.

Conclusion

This is one of many methods to better customize the behavior of weapons in game. There are other variables to consider depending on the core design of the mod itself. The use of dynamic accuracy and realistic recoil may not be ideal for faster paced games, or may be perfect for a tactical or stealth based shooter. Similar methods can be used for zooming or scope use as well as health based accuracy penalty for both NPCs and players.