|
|
|
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.
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!
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
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!
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