Player and NPC Cloak

From Valve Developer Community
Revision as of 15:14, 22 August 2014 by Chainmanner (talk | contribs) (Created page with "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...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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.

basecombatweapon_shared.cpp

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.