The Place to Start


TFC Grenade Throwing 
Prefect
Intermediate-Advanced


TFC Grenade Throwing

 

This is the second tutorial in the series "TFC Style Grenades", which contains the parts:

 

I split this tutorial into several chapters to keep things structured:

1. Adding the throwing code
2. Displaying the status on the HUD
3. Creating an entity to pick up
A. Things you should know

You should get a compilable and playable dll at the end of each chapter.

 

Old code is in Red, new code is in Yellow. Changed code and reference info is Green.

I gave up telling you the line numbers of the code, because they would be completely wrong (they're offset by more than 500 in player.cpp). You will see where you have to add new code by looking at the surrounding lines of old code, and I will give you the name of the function you have to put it in if necessary. If you try to understand what's going on instead of simply cut'n'paste the code, you'll know where to put it anyway.

 

NOTE:

I added some references to hallucination gas grenades in this code. You needn't add them if you don't have hallucination gas grenades in your code, I simply left them in to give you an example on how to add more different types of grenades.

 

 


1. Adding the throwing code

 

This chapter holds more or less all of the important code. You will be able to throw the grenades once you have them (but there won't be an entity to pick up yet!), but you won't see anything on the HUD.

First of all, we need some declarations in player.h:

	PLAYER_ATTACK1,
} PLAYER_ANIM;

// TFC Grenade Throwing
// this is forced by the client dll
#define MAX_GRENADE_TYPES			4

// Define the different grenade types
// IMPORTANT: Update gszGrenadeTypeIcons in player.cpp when you
// change this!!!
enum {
	GRENADE_NULL = 0,
	GRENADE_NORMAL,
	GRENADE_HALLUC
};

#define GRENADE_NORMAL_MAXCARRY			4
#define GRENADE_HALLUC_MAXCARRY			4
// TFC Grenade Throwing

A player can't carry more than 4 different grenade types at a time. This is only a limitation for one player. You can create more than 4 different grenade types globally, but one player can only carry 4 of them at a time. This is due to a limitation in the responsible HUD element in the client dll, which should be easy to bypass with a little client dll coding. Figure out your way through CHudAmmoSecondary, but I don't think you'll need more than 4 grenade types at a time, so I won't go into detail.

Some constants follow, which will be used several times in the code. Add this directly afterwards:

class CBasePlayer : public CBaseMonster
{
public:
// TFC Grenade Throwing
	int		m_iGrenadeTypes[MAX_GRENADE_TYPES];
	int		m_iGrenadeMaxCarry[MAX_GRENADE_TYPES];
	int		m_iGrenadeCarry[MAX_GRENADE_TYPES];
	int		m_iInThrow;
	float		m_flExplodeTime;
	float		m_flNextThrow;

	void		ThrowGrenade(int iType);
// TFC Grenade Throwing

m_iGrenadeTypes[] stores the grenade types the player carries. So if the player has normal grenades in grenade slot 1, m_iGrenadeTypes[0] would be GRENADE_NORMAL. If there are hallucination gas grenades in slot 1, it would be GRENADE_HALLUC. Note that you can even give a player one type of grenades in two or more slots.

m_iGrenadeMaxCarry[] and m_iGrenadeCarry[] store the maximum of grenades a player can carry in a slot and how many he's actually carrying, respectively.

m_iInThrow is 0 when the player doesn't attempt to throw a grenade, 1 if he's going to throw a grenade of slot 1, etc...

m_flExplodeTime holds the time when the grenade the player is currently holding will explode.

m_flNextThrow tells us when the player is allowed to throw another grenade. Otherwise the player could throw tons of grenades at once at an enemy.

ThrowGrenade() actually throws a grenade of the given type.

 

If you want to use the hallucination gas grenades, you have to move the declaration of CHallucGrenade from hallucgren.cpp to weapons.h.

 

Of course we have to initialize the new member variables, so add the following to CBasePlayer::Spawn() in player.cpp:

	m_lastx = m_lasty = 0;

// TFC Grenade Throwing
	for(i = 0; i < MAX_GRENADE_TYPES; i++) {
		m_iGrenadeTypes[i] = GRENADE_NULL;
		m_iGrenadeMaxCarry[i] = -1;
		m_iGrenadeCarry[i] = 0;
	}

	m_iGrenadeTypes[0] = GRENADE_NORMAL;
	m_iGrenadeMaxCarry[0] = GRENADE_NORMAL_MAXCARRY;
	
	m_iGrenadeTypes[1] = GRENADE_HALLUC;
	m_iGrenadeMaxCarry[1] = GRENADE_HALLUC_MAXCARRY;

	m_iInThrow = 0;
// TFC Grenade Throwing

	g_pGameRules->PlayerSpawn( this );
}

You may also give the player a starting amount of grenades here, and you'll probably want to modify it if you have different types of grenades for different classes.

The player has to be able to throw a grenade, right? We'll add keybindings for "+gren0" to "+gren3". This will create a "+grenX" client command when the player presses a key, and a "-grenX" client command when he/she releases the key. Client commands are processed by ClientCommand() (what a descriptive name ;-) in client.cpp, so add some code there:

	else if (FStrEq(pcmd, "lastinv" ))
	{
		GetClassPtr((CBasePlayer *)pev)->SelectLastItem();
	}
// TFC Grenade Throwing
	else if (!strncmp(pcmd, "+gren", 5))
	{
		CBasePlayer *pPlayer = GetClassPtr((CBasePlayer *)pev);
		int iType = pcmd[5] - '0';

		if (!pPlayer->m_iInThrow && gpGlobals->time > pPlayer->m_flNextThrow && 
							iType < MAX_GRENADE_TYPES) {
			if (pPlayer->m_iGrenadeCarry[iType]) {
				pPlayer->m_iInThrow = iType + 1;
				pPlayer->m_flExplodeTime = gpGlobals->time + 3.0;
			}
		}
	}
	else if (!strncmp(pcmd, "-gren", 5))
	{
		CBasePlayer *pPlayer = GetClassPtr((CBasePlayer *)pev);
		int iType = pcmd[5] - '0';

		if (pPlayer->m_iInThrow == iType + 1) {
			pPlayer->ThrowGrenade(iType);
			pPlayer->m_iInThrow = 0;
		}
	}
// TFC Grenade Throwing
	else if ( g_pGameRules->ClientCommand( GetClassPtr((CBasePlayer *)pev), pcmd ) )
	{

When the player presses one of the grenade keys, we're not yet throwing one and we're allowed to throw one again, initiate the throwing sequence.

When the player releases one ot the grenade keys, we check if the player is currently throwing this type of grenade, and call ThrowGrenade() to actually throw it.

To make keybindings more comfortable to players by adding them to the Controls menu, you should add the following lines to the file gfx/shell/kb_act.lst in your mod directory. If you don't have this file, copy it over from the valve folder.

"+gren0"				"Throw grenade type 1"
"+gren1"				"Throw grenade type 2"
"+gren2"				"Throw grenade type 3"
"+gren3"				"Throw grenade type 4"

Note that the engine will automatically create "+grenX" when the key is pressed and "-grenX" when the key is released. If you left out the plus sign, it would simply send a "grenX" client command on keypress, and nothing when the key is released.

 

Now a grenade should explode in the player's hands if they're waiting for too long. So we should check for the explosion timeout in CBasePlayer::PreThink() (in player.cpp):

	if (pev->deadflag >= DEAD_DYING)
	{
		PlayerDeathThink();
		return;
	}

// TFC Grenade Throwing
	// Make sure a grenade ALWAYS explodes after 3 seconds
	if (m_iInThrow) {
		if (m_flExplodeTime < gpGlobals->time) {
			ThrowGrenade(m_iInThrow-1);
			m_iInThrow = 0;
		}
	}
// TFC Grenade Throwing

	// So the correct flags get sent to client asap.
	//

Ok, now it's time to add two the new function ThrowGrenade(). Add them anywhere in player.cpp:

// TFC Grenade Throwing
void CBasePlayer::ThrowGrenade(int iType)
{
	float time;

	if (m_iGrenadeTypes[iType] == GRENADE_NULL || !m_iGrenadeCarry[iType]) {
		ALERT(at_console, "!!! Internal error in grenade throwing code !!!\n");

		m_iInThrow = -1;

		return;
	}

	time = m_flExplodeTime - gpGlobals->time;
	if (time < 0)
		time = 0;

	// ripped from handgrenade.cpp
	Vector angThrow = pev->v_angle + pev->punchangle;

	float flVel = (90 - angThrow.x) * 4;
	if (flVel > 500)
		flVel = 500;

	UTIL_MakeVectors( angThrow );

	Vector vecSrc = pev->origin + gpGlobals->v_forward * 16;

	Vector vecThrow = gpGlobals->v_forward * flVel + pev->velocity;
	vecThrow = vecThrow + (gpGlobals->v_up * flVel/2);

	switch(m_iGrenadeTypes[iType]) {
	case GRENADE_NORMAL:
		CGrenade::ShootTimed( pev, vecSrc, vecThrow, time );
		break;
	case GRENADE_HALLUC:
		CHallucGrenade::ShootHalluc( pev, vecSrc, vecThrow, time );
		break;
	}

	m_iGrenadeCarry[iType]--;

	// so the player can't throw too many grenades at a time
	m_flNextThrow = gpGlobals->time + 0.5;
}
// TFC Grenade Throwing

ThrowGrenade() is more or less a ripoff of the handgrenade throwing code. After some sanity checking it calculates the time that is left until the grenade explodes, and the velocity and vector to throw the grenade with. I modified this code somewhat so the grenade is thrown out of the player's center and not from his hands as for the handgrenade. After this, the function actually creates the grenade in a switch()-statement. When you add new grenade types, you have to add a case-branch here. Note that the next throw is delayed by 0.5 seconds. Without this, a player could throw tons of grenades within a few moments!

 

 


2. Displaying the status on the HUD

 

In this chapter you'll implement the grenade ammo display on the HUD. We'll also show the player if he's currently throwing a grenade. The necessary HUD elements are already there, so you needn't do any client dll coding. But the server-client messages - which are used in TFC - aren't registered in the SDK. These messages are

a) SecAmmoIcon - this tells the HUD what item to use right beside the amount of grenades you carry

b) SecAmmoVal - this tells the HUD how many grenades you carry

Add the following to player.cpp (near the top):

int gmsgSetFOV = 0;
int gmsgShowMenu = 0;

// TFC Grenade Throwing
int gmsgSecAmmoIcon = 0;
int gmsgSecAmmoVal = 0;
// TFC Grenade Throwing

LINK_ENTITY_TO_CLASS( player, CBasePlayer );

These have to be registered, so add the following to CBasePlayer::Precache():

	gmsgFade = REG_USER_MSG("ScreenFade", sizeof(ScreenFade));
	gmsgAmmoX = REG_USER_MSG("AmmoX", 2);

// TFC Grenade Throwing
	gmsgSecAmmoIcon = REG_USER_MSG( "SecAmmoIcon", -1 );
	gmsgSecAmmoVal = REG_USER_MSG( "SecAmmoVal", 2 );
// TFC Grenade Throwing

	m_iUpdateTime = 5;  // won't update for 1/2 a second

Now we need some additional member functions and variables in CBasePlayer. Add the following in player.h:

// TFC Grenade Throwing
	int		m_iGrenadeTypes[MAX_GRENADE_TYPES];
	int		m_iGrenadeMaxCarry[MAX_GRENADE_TYPES];
	int		m_iGrenadeCarry[MAX_GRENADE_TYPES];
	int		m_iInThrow;
	float		m_flExplodeTime;
	float		m_flNextThrow;

	int		m_iClientInThrow;
	int		m_iClientGrenadeCarry[MAX_GRENADE_TYPES];

	void		ThrowGrenade(int iType);
	void		UpdateGrenades();
// TFC Grenade Throwing

The first thing to do is tell the HUD what icon we want to use. We'll do this once per game at the top of CBasePlayer::UpdateClientData() (in player.cpp):

			if ( g_pGameRules->IsMultiplayer() )
			{
				FireTargets( "game_playerjoin", this, this, USE_TOGGLE, 0 );
			}

// TFC Grenade Throwing
			// tell the client what sprite to use for the grenade display
			MESSAGE_BEGIN( MSG_ONE, gmsgSecAmmoIcon, NULL, pev );
				WRITE_STRING( "grenade" );
			MESSAGE_END( );

			// fix problem with grenade ammo staying in the HUD over level changes
			// Would be better to fix it in the client dll... oh well
			for(int i = 0; i < MAX_GRENADE_TYPES; i++) {
				if (m_iGrenadeTypes[i]) {
					MESSAGE_BEGIN( MSG_ONE, gmsgSecAmmoVal, NULL, pev );
						WRITE_BYTE( i );
						WRITE_BYTE( 0 );
					MESSAGE_END();
				}
			}
// TFC Grenade Throwing
		}
		FireTargets( "game_playerspawn", this, this, USE_TOGGLE, 0 );
	}

Then we call UpdateGrenades() from UpdateClientData() to update the grenade HUD:

		// Clear off non-time-based damage indicators
		m_bitsDamageType &= DMG_TIMEBASED;
	}

// TFC Grenade Throwing
	UpdateGrenades();
// TFC Grenade Throwing

	// Update Flashlight
	if ((m_flFlashLightTime) && (m_flFlashLightTime <= gpGlobals->time))

Now we finally have to add CBasePlayer::UpdateGrenades(). I put it where I put ThrowGrenade(), but that doesn't matter. Here it is:

// don't forget to update this when you add new grenade types!!!
static char *gszGrenadeTypeIcons[] = {
	NULL,
	"d_normalgrenade",
	"d_gasgrenade"
};

// this function is responsible for updating the HUD
void CBasePlayer::UpdateGrenades()
{
	// update grenade throwing icons
	if (m_iClientInThrow != m_iInThrow) 
	{
		char *szIcon;

		// deactivate the old icon first
		if (m_iClientInThrow) {
			szIcon = gszGrenadeTypeIcons[m_iGrenadeTypes[m_iClientInThrow-1]];
		} else {
			szIcon = NULL;
		}

		if (szIcon) {
			MESSAGE_BEGIN( MSG_ONE, gmsgStatusIcon, NULL, pev );
				WRITE_BYTE( FALSE );		// deactivate
				WRITE_STRING( szIcon );
			MESSAGE_END();
		}

		// now activate the new icon
		if (m_iInThrow) {
			szIcon = gszGrenadeTypeIcons[m_iGrenadeTypes[m_iInThrow-1]];
		} else {
			szIcon = NULL;
		}

		if (szIcon) {
			MESSAGE_BEGIN( MSG_ONE, gmsgStatusIcon, NULL, pev );
				WRITE_BYTE( TRUE );				// activate
				WRITE_STRING( szIcon );
				WRITE_BYTE(255);	// rgb color values
				WRITE_BYTE(160);
				WRITE_BYTE(0);
			MESSAGE_END();
		}

		m_iClientInThrow = m_iInThrow;
	}

	for(int i = 0; i < MAX_GRENADE_TYPES; i++) {
		if (m_iClientGrenadeCarry[i] != m_iGrenadeCarry[i]) {
			MESSAGE_BEGIN( MSG_ONE, gmsgSecAmmoVal, NULL, pev );
				WRITE_BYTE( i );
				WRITE_BYTE( m_iGrenadeCarry[i] );
			MESSAGE_END();

			m_iClientGrenadeCarry[i] = m_iGrenadeCarry[i];
		}
	}
}

This checks if the m_iInThrow state has changed, and updates the icons on the left side of the HUD appropriately. After this, it checks if any of the grenade amounts have changed, and updates them if necessary.

It is important that the order of strings in gszGrenadeTypeIcons matches the order of grenade types in the enum in player.cpp.

Note that I use two new HUD icons: d_normalgrenade and d_hallucgrenade to display the throwing state. To be able to use the TFC icons, you have to extract the file sprites/tfc_dmsg.spr from the TFC pak into your mod directory, and change your sprites/hud.txt accordingly (if you don't have this file yet, copy it from the valve/sprites directory):

1. Add 4 to the value in the first line of the file, as we add 4 sprites (two for low resolution and two for high resolution)

2. Add the low-res sprites:

d_skull			320	320hud1	192	240	32	16
d_tracktrain		320	320hud1	224	208	32	16

// TFC Grenade Throwing
d_normalgrenade		320	320hud1	144	224	32	16
d_gasgrenade		320	tfc_dmsg 84	96	18	18
// TFC Grenade Throwing

3. And then the high-res sprites:

d_skull			640	640hud1	192	224	32	16
d_tracktrain		640	640hud1	192	240	32	16

// TFC Grenade Throwing
d_normalgrenade		640	640hud1	192	160	32	16
d_gasgrenade		640	tfc_dmsg 60	96	24	24
// TFC Grenade Throwing

 


3. Creating an entity to pick up

 

In this chapter, we'll create an ammo_grenades item that the player can pick up. But first of all, a function has to be added to CBasePlayer (in player.h):

// TFC Grenade Throwing
	int		m_iGrenadeTypes[MAX_GRENADE_TYPES];
	int		m_iGrenadeMaxCarry[MAX_GRENADE_TYPES];
	int		m_iGrenadeCarry[MAX_GRENADE_TYPES];
	int		m_iInThrow;
	float		m_flExplodeTime;
	float		m_flNextThrow;

	int		m_iClientInThrow;
	int		m_iClientGrenadeCarry[MAX_GRENADE_TYPES];

	void		ThrowGrenade(int iType);
	void		UpdateGrenades();
	virtual int	GiveGrenades(int iType, int iAmount);
// TFC Grenade Throwing

I made this function virtual for a reason: due to how inheritance and ammo code work, GiveGrenades() has to be in all entities, so the following has to be added to CBaseEntity, in cbase.h:

class CBaseEntity 
{
public:
// TFC Grenade Throwing
	virtual int		GiveGrenades(int iType, int iAmount) { return 0; }
// TFC Grenade Throwing
	// Constructor.  Set engine to use C/C++ callback functions
	// pointers to engine data
	entvars_t *pev;		// Don't need to save/restore this pointer, the engine resets it

This makes sure all entities have a GiveGrenades() function. This function won't do anything at all for normal entities.

Now let's add the code for the newly declared function. I put it where I put the other new member functions of CBasePlayer, but it really doesn't matter. Here it is:

// give the player some grenades; return the number of grenades actually
// given to the player
int CBasePlayer::GiveGrenades(int iType, int iAmount)
{
	if (m_iGrenadeTypes[iType] == GRENADE_NULL)
		return 0;

	int maxgive = m_iGrenadeMaxCarry[iType] - m_iGrenadeCarry[iType];

	iAmount = min(maxgive, iAmount);
	if (!iAmount)
		return 0;

	m_iGrenadeCarry[iType] += iAmount;

	// with a little client dll coding one could add an icon to the
	// pickup history; I'll just do it the TFC way
	ClientPrint( pev, HUD_PRINTCENTER, "Restocking grenades..." );

	return iAmount;
}

This should be really self-explanatory.

The last thing to create is CAmmoGrenades. I didn't really know where to put it, and I didn't want to create a new .cpp-file, so I added it at the end of weapons.cpp:

// TFC Grenade Throwing
class CAmmoGrenades : public CBasePlayerAmmo
{
public:
	int		m_iGrenades[MAX_GRENADE_TYPES];

	void KeyValue( KeyValueData *pkvd )
	{
		if (!strncmp(pkvd->szKeyName, "grenades", 8))
		{
			int iType = pkvd->szKeyName[8] - '0';

			if (iType >= 0 && iType < MAX_GRENADE_TYPES) {
				m_iGrenades[iType] = atoi(pkvd->szValue);
				pkvd->fHandled = TRUE;
			}
		}
		else
			CBasePlayerAmmo::KeyValue( pkvd );
	}
	void Spawn( void )
	{ 
		Precache( );
		SET_MODEL(ENT(pev), "models/w_grenade.mdl");
		CBasePlayerAmmo::Spawn( );

		for(int i = 0; i < MAX_GRENADE_TYPES; i++) {
			if (!m_iGrenades[i])
				m_iGrenades[i] = 1;

			if (m_iGrenades[i] < 0)
				m_iGrenades[i] = 0;
		}
	}
	void Precache( void )
	{
		PRECACHE_MODEL ("models/w_grenade.mdl");
		PRECACHE_SOUND("items/9mmclip1.wav");
	}
	BOOL AddAmmo( CBaseEntity *pOther ) 
	{ 
		BOOL fSuccess = FALSE;

		for(int i = 0; i < MAX_GRENADE_TYPES; i++) {
			if (pOther->GiveGrenades( i, m_iGrenades[i] ))
			{
				fSuccess = TRUE;
			}
		}

		if (fSuccess) {
			EMIT_SOUND(ENT(pev), CHAN_ITEM, "items/9mmclip1.wav", 1, ATTN_NORM);
			return TRUE;
		}

		return FALSE;
	}
};
LINK_ENTITY_TO_CLASS( ammo_grenades, CAmmoGrenades );
// TFC Grenade Throwing

The neat thing about this entity is that you can specify the amount of grenades given to a player through keyvalues. If you want the player to get two grenades of type 1, add the keyvalue "grenades0 2", and if you don't want the player to get any grenades of this type, add "grenades0 -1". If you don't specify any keyvalue for one type, the default of 1 will be used for this type.

We have to precache ammo_grenades in W_Precache() (still in weapons.cpp):

	// hand grenade
	UTIL_PrecacheOtherWeapon("weapon_handgrenade");

// TFC Grenade Throwing
	UTIL_PrecacheOther("ammo_grenades");
// TFC Grenade Throwing

#if !defined( OEM_BUILD ) && !defined( HLDEMO_BUILD )

That's it!

 

 


A. Things you should know

 

1. How do I add new types of grenades?

This is easy. First of all, you have to add your grenade type to the enum-list in player.h, and you should define a GRENADE_XXX_MAXCARRY constant.

Second, you must add a sprite name to the list gszGrenadeTypeIcons[] in player.cpp. If you don't have/want an icon, you can still say NULL there, but it has to be in there, or you'll get crashes. Note that the strings in gszGrenadeTypeIcons[] have to be in the same order as the grenade type definitions in the enum in player.h.

To make any use of the new grenade type, you should assign it to one of the grenade slots. This is done in CBasePlayer::Spawn(). Note that you can't have more than 4 slots currently (array indexes 0 to 3). See below for more information.

Finally, you have to add a new case-branch to the switch()-instruction of CBasePlayer::ThrowGrenade(), or else no grenade will come when you press the launch key.

 

2. I want more than four grenade slots!

You can do this with a little client dll coding. Look into the declaration of CHudAmmoSecondary, there's a constant defining the maximum number of secondary ammo values there. It should be enough to adjust it up. Of course you have to change MAX_GRENADE_TYPES in the entity dll as well!

 

3. Problem with different numbers of grenade types

If you implement a class system where one class has more grenade types than the others you'll still see the grenade type that is no longer available on the HUD when you switch from this class to one of the other classes. The amount will be zero, but it'll look weird.

Again, you have to do some client dll coding to fix this. Make sure the CHudAmmoSecondary::Reset() function resets all the secondary ammo values to -1. Then you have to reset all the used secondary ammo slots to 0 on every respawn. To do this, move the for()-loop you added to UpdateClientData() (the one which sends the SecAmmoVal messages) directly below the ResetHUD-message, and not into the if (!m_fGameHUDInitialized)-statement. I think this should work, but I haven't tried it.

 

 


If you have any questions, suggestions, bugfixes, creative critizism or you want to give me donations for this great tutorial, mail me or post on the Wavelength forums. Go to Wavelength, click on Forums and then on Coding. You have to register yourself to post there, but visiting it regularly is good anyway. There is a forum on HL Programming Planet that I regularly visit, too, but it has less traffic and less people visit it.


You may use this tutorial for any non-commercial mod development, as long as you give me some credit for it.

(c) 2000 by Nicolai Haehnle aka Prefect