Player and NPC Cloak
In this tutorial, we will be making a basic cloak that players and NPCs can use to hide from enemy NPCs and other players.
Contents
basecombatcharacter.h
To start, define the following network variables in CBaseCombatCharacter, in a public section and preferably under the definition for m_flNextAttack:
//Cloak variables CNetworkVar( int, m_intCloakStatus ); CNetworkVar( float, m_floatCloakFactor );
The m_intCloakStatus network variable will be used to determine the status of the combat character's cloaking; 0 means fully visible, 1 means uncloaking, 2 means fully invisible, and 3 means cloaking. m_floatCloakFactor will be used to determine the entity's cloaking progress, where between 0.1 and 0.9, the requested models will be partially visible, 0 will be fully visible, and 1 will be fully invisible. Also in a public area, add the following function definitions:
//Cloak functions int GetCloakStatus( void ) { return m_intCloakStatus; } void SetCloakStatus( int cloakstatus ) { SetTransmitState( FL_EDICT_ALWAYS ); m_intCloakStatus.Set( cloakstatus ); } float GetCloakFactor( void ) { return m_floatCloakFactor; } void SetCloakFactor( float cloakfactor ) { SetTransmitState( FL_EDICT_ALWAYS ); m_floatCloakFactor.Set( cloakfactor ); }
These will be used as setters and getters for the cloak variables.
basecombatcharacter.cpp
If you want to save the game with the cloak status and factor in it, add these field definitions, right after BEGIN_DATADESC( CBaseCombatCharacter ):
//Cloak variables DEFINE_FIELD( m_intCloakStatus, FIELD_INTEGER ), DEFINE_FIELD( m_floatCloakFactor, FIELD_FLOAT ),
It's not mandatory, but will come in useful so that these specific entities will not lose their cloak data when saving the game. Since we will need the client to process the cloak effect, we must send the variable values from the server to the client. Under IMPLEMENT_SERVERCLASS_ST(CBaseCombatCharacter, DT_BaseCombatCharacter), add the following:
//Cloak data SendPropInt( SENDINFO( m_intCloakStatus ) ), SendPropFloat( SENDINFO ( m_floatCloakFactor ) ),
We'll need receivers on the client as well, but we'll take care of it later. Now, we must initialize some fields when a combat character is created or destroyed. Under the CBaseCombatCharacter constructor and destructor, add these variables on line 715:
m_intCloakStatus.Set( 0 ); m_floatCloakFactor.Set( 0.0f );
This won't change anything if there was data in a saved game. Since the cloak variables will be constantly updating any time there's action, we need to put its cloaking logic inside a constantly updating function. The only one found in basecombatcharacter.cpp is Weapon_FrameUpdate. First, though, to test the cloaking effects, define two new ConVars somewhere in basecombatcharacter.cpp's ConVar definitions:
ConVar player_cloak_custom( "player_cloak_custom", "0", FCVAR_CHEAT, "Enable cloak factor modification" ); ConVar player_cloak_factor( "player_cloak_factor", "0.0", FCVAR_CHEAT, "Cloak factor" );
This will allow you to see the cloak factors, for testing purposes. On to the logical function in Weapon_FrameUpdate, be sure to encase it all in an engine->IsPaused() loop, as this function updates every frame.
//Cloak effects if ( !engine->IsPaused() ) { if ( player_cloak_custom.GetInt() == 0 && !dynamic_cast< CBasePlayer *>(this) ) { if ( GetCloakStatus() == 1 && m_floatCloakFactor.Get() == 0.0f || GetCloakStatus() == 1 && m_floatCloakFactor.Get()) SetCloakStatus( 0 ); if ( GetCloakStatus() == 3 && m_floatCloakFactor.Get() == 1.0f || GetCloakStatus() == 3 && m_floatCloakFactor.Get() >= 1.0f ) SetCloakStatus( 2 ); if ( GetCloakStatus() == 0 ) { m_floatCloakFactor.Set( 0.0f ); RemoveEffects( EF_NOSHADOW ); } if ( GetCloakStatus() == 2 ) { m_floatCloakFactor.Set( 1.0f ); AddEffects( EF_NOSHADOW ); } if ( GetCloakStatus() == 1 && m_floatCloakFactor.Get() != 0.0f || GetCloakStatus() == 1 && m_floatCloakFactor.Get() >= 0.0f ) { m_floatCloakFactor.Set( m_floatCloakFactor.Get() - 0.005f ); } if ( GetCloakStatus() == 3 && m_floatCloakFactor.Get() != 1.0f || GetCloakStatus() == 3 && m_floatCloakFactor.Get()) { m_floatCloakFactor.Set( m_floatCloakFactor.Get() + 0.005f ); } } if ( player_cloak_custom.GetInt() == 0 && dynamic_cast< CBasePlayer *>(this) ) { if ( GetCloakStatus() == 1 && m_floatCloakFactor.Get() == 0.0f || GetCloakStatus() == 1 && m_floatCloakFactor.Get()) SetCloakStatus( 0 ); if ( GetCloakStatus() == 3 && m_floatCloakFactor.Get() == 0.75f || GetCloakStatus() == 3 && m_floatCloakFactor.Get() >= 0.75f ) SetCloakStatus( 2 ); if ( GetCloakStatus() == 0 ) m_floatCloakFactor.Set( 0.0f ); if ( GetCloakStatus() == 2 ) m_floatCloakFactor.Set( 0.75f ); if ( GetCloakStatus() == 1 && m_floatCloakFactor.Get() != 0.0f || GetCloakStatus() == 1 && m_floatCloakFactor.Get() >= 0.0f ) { m_floatCloakFactor.Set( m_floatCloakFactor.Get() - 0.005f ); } if ( GetCloakStatus() == 3 && m_floatCloakFactor.Get() != 0.75f || GetCloakStatus() == 3 && m_floatCloakFactor.Get()) { m_floatCloakFactor.Set( m_floatCloakFactor.Get() + 0.005f ); } } if ( player_cloak_custom.GetInt() == 1 ) { m_floatCloakFactor = player_cloak_factor.GetFloat(); } }
Unless the player_cloak_custom ConVar is set to true, the server will constantly update the cloak effect. If it's set to 1, then it slowly decreases the cloak factor until it reaches status 0, and if the status is 3, then it will increase the cloak factor until it's high enough to become status 2.
You can also see that we've got different cloak factor maximum values for the player and for NPCs. The player will be using their own cloak factor for an effect on the viewmodel, which will be partially visible since the cloak factor isn't what will determine the visibility to NPCs, but NPCs will be easy to spot when cloaked at around 90% and near a wall, as the game will display a strange artifact (similar to when sometimes, you'd look through a nodraw texture into the void, where the image copied onto the leak effect constantly shifts in one direction) where the NPC silhouette should be.
Now, let's balance it out so that players and NPCs can't shoot while invisible. Paste the following into ItemPreFrame (not ItemPostFrame, or else some weapons like weapon_crowbar or weapon_physcannon will not work as intended):
CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); if (!pOwner) return; #ifndef CLIENT_DLL //Only done on the server, since the client won't be transmitting anything if ( pOwner->GetCloakStatus() == 1 || pOwner->GetCloakStatus() == 2 || pOwner->GetCloakStatus() == 3 ) { pOwner->DisableButtons( IN_ATTACK ); pOwner->DisableButtons( IN_ATTACK2 ); } else { pOwner->EnableButtons( IN_ATTACK ); pOwner->EnableButtons( IN_ATTACK2 ); } #endif
This way, the player can't attack enemies while they'll never be able to find him until he uncloaks. Our last modifications will be the most important: NPC AI.
ai_basenpc.cpp
The code here is still a work in progress and might not work the way you'd want, but something is better than nothing. In the function FCanCheckAttacks, add right at the top:
//Cannot check attacks while not fully uncloaked if ( GetCloakStatus() != 0 ) return false;
Search for the function QuerySeeEntity and add:
CBaseCombatCharacter *pCC = dynamic_cast<CBaseCombatCharacter *>(pEntity); if ( pCC && pCC->GetCloakStatus() == 2 ) return false;
Go to the OnLooked function, and in the "while( pSightEnt )" loop, replace the top of it with:
if ( pSightEnt->IsPlayer() ) { CBasePlayer *pPlayer = ToBasePlayer( pSightEnt ); if ( pPlayer && pPlayer->GetCloakStatus() != 2 ) { // if we see a client, remember that (mostly for scripted AI) SetCondition(COND_SEE_PLAYER); m_flLastSawPlayerTime = gpGlobals->curtime; } }
In the same function, not too far below, add some new factors in the if definitions:
// the looker will want to consider this entity // don't check anything else about an entity that can't be seen, or an entity that you don't care about. if ( relation != D_NU ) { if ( pSightEnt == GetEnemy() ) { CBaseCombatCharacter *pEnemy = GetEnemy()->MyCombatCharacterPointer(); if (/*pEnemy &&*/ pEnemy->GetCloakStatus() != 2) { // we know this ent is visible, so if it also happens to be our enemy, store that now. SetCondition(COND_SEE_ENEMY); } } }
In the GatherEnemyConditions function, right under the "Have LOS but may not be in view cone" comment, replace those lines with:
// Have LOS but may not be in view cone SetCondition( COND_HAVE_ENEMY_LOS ); //START CBaseCombatCharacter *pCC = dynamic_cast<CBaseCombatCharacter *>( pEnemy ); if ( bSensesDidSee && pCC && pCC->GetCloakStatus() != 2 ) { // Have LOS and in view cone SetCondition( COND_SEE_ENEMY ); } //END
Right before the comment "If I haven't seen the enemy in a while he may have eluded me", add this line:
CBaseCombatCharacter *pNPC = dynamic_cast< CBaseCombatCharacter *>( pEnemy );
Now finally, in the if section after that comment, add this right at the end:
if ( pNPC ) { if ( /*!HasCondition( COND_SEE_ENEMY ) && HasCondition( COND_ENEMY_OCCLUDED ) &&*/ pNPC->GetCloakStatus() == 2 ) //Memory on the cloaked enemy needs to be wiped out or the NPC will find said enemy immediately, regardless of position or occlusion ClearCondition( COND_SEE_ENEMY ); GetEnemies()->ClearMemory( pEnemy ); MarkEnemyAsEluded(); }
That's it. Now the NPCs should not be able to detect the player, or any other NPC for that matter, if they're fully invisible, and if the player cloaks while in the middle of combat, they can escape.
c_basecombatcharacter.h
Now that we're done with the logical processing on the server, now it's time to move on to the rendering on the client. That said, we will be doing things here somewhat similarly to that of the servers, but this time only the getters for the cloak variables will be added, and those variables will not go under the CNetworkVar macro. Place said variables and functions in a public definition list:
//Cloak functions int GetCloakStatus( void ) { return m_intCloakStatus; } float GetCloakFactor( void ) { return m_floatCloakFactor; } //Cloak variables int m_intCloakStatus; float m_floatCloakFactor;
c_basecombatcharacter.cpp
All that must be done here is to add receivers for variables. Search for the receiving data table under the name BEGIN_RECV_TABLE( C_BaseCombatCharacter, DT_BaseCombatCharacter ) and add the following:
RecvPropInt( RECVINFO( m_intCloakStatus ) ), RecvPropFloat( RECVINFO( m_floatCloakFactor ) ),
Add some prediction fields under BEGIN_PREDICTION_DATA( C_BaseCombatCharacter ):
//Cloak variables DEFINE_PRED_FIELD( m_intCloakStatus, FIELD_INTEGER, FTYPEDESC_INSENDTABLE ), DEFINE_PRED_FIELD( m_floatCloakFactor, FIELD_FLOAT, FTYPEDESC_INSENDTABLE ),
Cloak effect material proxy (cloakproxy.cpp)
Finally, it's time to add a material proxy for our cloaking effect. Since there are no modifications needed to base classes, the whole file code will be pasted right below.
#include "cbase.h"
#include "materialsystem/imaterialvar.h"
#include "materialsystem/imaterialproxy.h"
#include "baseviewmodel_shared.h"
class C_CloakProxy : public IMaterialProxy
{
public:
C_CloakProxy();
virtual ~C_CloakProxy();
virtual bool Init( IMaterial *pMaterial, KeyValues *pKeyValues );
C_BaseEntity *BindArgToEntity( void *pArg );
virtual void OnBind( void* C_BaseEntity );
virtual void Release( void ) { delete this; }
IMaterial *GetMaterial( void );
private:
IMaterialVar* cloakFactorVar;
};
C_CloakProxy::C_CloakProxy()
{
cloakFactorVar = NULL;
}
C_CloakProxy::~C_CloakProxy()
{
}
bool C_CloakProxy::Init( IMaterial *pMaterial, KeyValues *pKeyValues )
{
bool found;
pMaterial->FindVar( "$cloakpassenabled", &found, false );
if ( !found )
return false;
cloakFactorVar = pMaterial->FindVar( "$cloakfactor", &found, false );
if ( !found )
return false;
return true;
}
C_BaseEntity *C_CloakProxy::BindArgToEntity( void *pArg )
{
IClientRenderable *pRend = (IClientRenderable *)pArg;
return pRend ? pRend->GetIClientUnknown()->GetBaseEntity() : NULL;
}
void C_CloakProxy::OnBind( void* pC_BaseEntity )
{
if ( !pC_BaseEntity )
return;
C_BaseEntity *pEntity = BindArgToEntity( pC_BaseEntity );
//If this is a player's viewmodel...
if ( C_BaseViewModel *pViewModel = dynamic_cast< C_BaseViewModel *>(pEntity) )
{
C_BasePlayer *pPlayer = ToBasePlayer( pViewModel->GetOwner() );
cloakFactorVar->SetFloatValue( pPlayer->GetCloakFactor() );
}
//If this is a non-player character...
else if ( C_BaseCombatCharacter *pNPC = dynamic_cast< C_BaseCombatCharacter *>(pEntity) )
{
cloakFactorVar->SetFloatValue( pNPC->GetCloakFactor() );
}
//If this is a weapon's worldmodel (under the assumption it's in something's possesion)...
else if ( C_BaseCombatWeapon *pWeapon = dynamic_cast< C_BaseCombatWeapon *>(pEntity) )
{
C_BaseCombatCharacter *pOwner = ToBaseCombatCharacter( pWeapon->GetOwner() );
if ( !pOwner )
return;
cloakFactorVar->SetFloatValue( pOwner->GetCloakFactor() );
}
else
return;
}
IMaterial *C_CloakProxy::GetMaterial()
{
return cloakFactorVar->GetOwningMaterial();
}
EXPOSE_INTERFACE( C_CloakProxy, IMaterialProxy, "Invisibility" IMATERIAL_PROXY_INTERFACE_VERSION );
Confused? Here's an explanation of what's happening in this file:
- In our initialization function (Init), we're searching to see if the material has cloaking effects enabled and it initializes the cloakFactorVar variable, which we will be using to increase or decrease the cloaking effect.
- The OnBind function is called every time the material is about to be rendered on an entity. First, we must identify the pC_BaseEntity as an entity pointer, after which we determine what type of entity it is.
- C_BaseViewModel: The player's viewmodel. Since CBasePlayer inherits from CBaseCombatCharacter, we get its owner, determine the player's cloak factor, and write it to $cloakfactor.
- C_BaseCombatCharacter: An NPC (like npc_citizen or npc_metropolice).
- C_BaseCombatWeapon: A weapon in worldmodel form. We get its owner and write its cloak factor to $cloakfactor, but if there is no owner then we ignore this step. Neglecting this part will crash the game upon loading a map.
- If it's none of the above, then it was never meant to be invisible and we return.
- EXPOSE_INTERFACE allows materials to use our new proxy, which will be named "Invisibility".
Now the coding is done! Now to finalize this part, all that's left is to edit the VMT files.
VMT file modification
VertexLitGeneric, the shader used by models, supports the cloaking effect used by the Spy in Team Fortress 2. There are a few variables that you will need to know when modifying the material files:
$cloakpassenabled <boolean> *Enables cloaking effects $cloakfactor<normal> *0.0 = Fully visible, 1.0 = Fully invisible $cloakcolortint <RGB matrix> *Colors the refraction effect. White if undefined. $refractamount <float> *How strong the refraction effect should be when partially invisible.
You will also have to make sure to include the "Invisibility" proxy in the VMT file.
Here's an example of a modified VMT file of a combine soldier:
"VertexLitGeneric" { "$bumpmap" "models/combine_soldier/combinesoldier_normal" "$surfaceprop" "flesh" "$selfillum" 1 "$model" 1 "$phong" 1 "$phongboost" "5" "$halflambert" "1" "$phongexponenttexture" "models/combine_soldier/combinesoldier_phong" "$phongalbedotint" "1" "$phongfresnelranges" "[.1 .5 1.0]" // Use separate self-illum mask on Pre DX9 hardware ">=dx90_20b" { "$baseTexture" "Models/Combine_soldier/combinesoldier_noalpha" "$selfillummask" "Models/Combine_soldier/combinesoldierselfillummask" } // Use redundant self-illum in base alpha on Pre DX9 hardware "<dx90_20b" { "$baseTexture" "Models/Combine_soldier/combinesoldiersheet" } "$cloakpassenabled" "1" "$alphatest" "1" "$cloakfactor" "0" "$cloakcolortint" "[.1 0.5 1.0]" "$refractamount" "1" "Proxies" { "Invisibility" { } } }
OPTIONAL: Moment of truth (cloaktest.cpp)
Now to test the cloak so that you can improve it, let's make a new CPP file in the server called cloaktest.cpp
. This is the source code:
#include "cbase.h"
void CC_TestCloak( void )
{
CBasePlayer *pPlayer = UTIL_GetLocalPlayer();
if ( pPlayer->GetCloakStatus() == 0 )
{
pPlayer->SetCloakStatus( 3 );
Msg( "Cloaking...\n" );
}
else if ( pPlayer->GetCloakStatus() == 2 )
{
pPlayer->SetCloakStatus( 1 );
Msg( "Uncloaking...\n" );
}
else
return;
}
static ConCommand testcloak( "testcloak", CC_TestCloak );
CBasePlayer derives from CBaseCombatCharacter. When the player fires the testcloak
command, they will cloak or uncloak depending on their cloak status.
That's it! Now bind testcloak
to a key and watch the invisibility in action!
Notes
- Touching a barnacle tongue while cloaked will crash the game.