Player and NPC Cloak

From Valve Developer Community
Jump to: navigation, search
Cloak effect at 33%
Ditto, 50%
Ditto, 95%. Notice how you can barely make out a silhouette of the player's viewmodel while the soldier can still be seen from half a mile away.
In this tutorial, we will be making a basic cloak that players and NPCs can use to hide from enemy NPCs and other players.
Note:This has been tested in Source 2013 HL2 singleplayer engine, so there may be some editing needed to port it to other versions.
Note:Materials with cloak effects applied are still drawn in the frame buffer, so if a material's cloak factor is between 0 and 1 there will likely be a hall-of-mirrors effect. For a material to refract without a hall-of-mirrors effect, its active skin must have $translucent set to 1. Since this will probably generate depth-test problems, it's probably best to switch to a skin with $translucent after the cloak factor exceeds 0.5.

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();
 }
}

Warning: This may cause a good deal of network traffic on multiplayer. 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.

basecombatweapon_shared.cpp

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. Warning: Some NPCs such as npc_manhack and npc_turret_floor will need you to be occluded, and some AIs such as an npc_citizen in the player's squad and an npc_metropolice chasing you with a stunstick will not be affected at all.

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.