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. This has been tested in Source 2013 HL2 singleplayer engine, so there may be some editing needed to port it to other versions.
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:
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 a variant of a nodraw tool brush in some cases, 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 the player 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 && pEnemy && pEnemy->GetCloakStatus() != 2 )
{
if ( pSightEnt == GetEnemy() )
{
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. Beware that some NPCs such as npc_manhack and npc_turret_floor will need you to be occluded, and some AIs such as an npc_metropolice chasing you with a stunstick will not be affected at all.