Interactive Ingame VGUI Panels
January 2024
Introduction
This tutorial will detail how to add the necessary functionality to create a fully working ingame VGUI panel. There are a few small flaws with the workings of the original VGUI panel presented in the source code that must be rectified before these panels will operate.
Fixing the VGUI Screen errors
This first problem is that CInput::ExtraMouseSample
calls g_pClientMode->CreateMove()
with a faked CUserCmd
which does not have the correct button flags. When DetermineVguiInputMode
is called from within CreateMove
with this bad input problems occur. This can be fixed if a boolean is added that will prevent the function running during the extra mouse samples.
First in client/in_main.cpp replace:
// Let the move manager override anything it wants to.
if ( g_pClientMode->CreateMove( frametime, cmd ) )
{
// Get current view angles after the client mode tweaks with it
engine->SetViewAngles( cmd->viewangles );
prediction->SetLocalViewAngles( cmd->viewangles );
}
With
// Let the move manager override anything it wants to.
if ( g_pClientMode->CreateMove( frametime, cmd, true ) )
{
// Get current view angles after the client mode tweaks with it
engine->SetViewAngles( cmd->viewangles );
prediction->SetLocalViewAngles( cmd->viewangles );
}
So now when CreateMove
is called it passed a Boolean which will be used to show this is a valid call to the VGUI screen as well as frametime
and cmd
. In order for this to work the CreateMove
function must be modified to accommodate this extra Boolean. To do this, add:
virtual bool CreateMove( float flInputSampleTime, CUserCmd *cmd, bool bVguiUpdate ) = 0;
above
virtual bool CreateMove( float flInputSampleTime, CUserCmd *cmd ) = 0;
in client/iclientmode.h, client/hl2/c_basehlplayer.h and client/clientmode_shared.h
Next functionality has to be provided for these headers, so in client/clientmode_shared.cpp change the section
bool ClientModeShared::CreateMove( float flInputSampleTime, CUserCmd *cmd )
{
// Let the player override the view.
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
if(!pPlayer)
return true;
// Let the player at it
return pPlayer->CreateMove( flInputSampleTime, cmd );
}
To
bool ClientModeShared::CreateMove( float flInputSampleTime, CUserCmd *cmd, bool bVguiUpdate )
{
// Let the player override the view.
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
if(!pPlayer)
return true;
// Let the player at it
return pPlayer->CreateMove( flInputSampleTime, cmd, bVguiUpdate );
}
bool ClientModeShared::CreateMove( float flInputSampleTime, CUserCmd *cmd )
{
return CreateMove( flInputSampleTime, cmd, false );
}
And in client/hl2/c_basehlplayer.cpp replace
bool C_BaseHLPlayer::CreateMove( float flInputSampleTime, CUserCmd *pCmd )
{
bool bResult = BaseClass::CreateMove( flInputSampleTime, pCmd );
with
bool C_BaseHLPlayer::CreateMove( float flInputSampleTime, CUserCmd *pCmd , bool bVguiUpdate )
{
bool bResult = BaseClass::CreateMove( flInputSampleTime, pCmd, bVguiUpdate );
Finally in client/c_baseplayer.cpp find:
bool C_BasePlayer::CreateMove( float flInputSampleTime, CUserCmd *pCmd )
and replace it with:
bool C_BasePlayer::CreateMove( float flInputSampleTime, CUserCmd *pCmd, bool bVguiUpdate )
Then inside the CreateMove
function find:
DetermineVguiInputMode( pCmd );
And replace it with:
if (bVguiUpdate)
{
bool tempvguimode = IsInVGuiInputMode();
// Check to see if we're in vgui input mode...
DetermineVguiInputMode( pCmd );
if (tempvguimode == !IsInVGuiInputMode())
{
if (IsInVGuiInputMode())
{
engine->ClientCmd( "vguimode_true" );
}
else
{
engine->ClientCmd( "vguimode_false" );
}
}
}
This function witll now only call the DetermineVguiInputMode function when a valid call is being made to the CreateMove function. Furthermore, when the VguiMode changes as a player interacts with a screen it will send messages from the client to the server.
Fixing the Interactive Mode
By performing the previous fix, vguiscreens will be initialised with proper data, however due to a flaw in DetermineVguiInputMode
the vgui screens are not detected and so the player never enters vguiinput mode. If the vgui screens are simply for display purposes this is fine, to get the screens interactive this must change however. The problem lies with the FindNearbyVguiScreen
function in client/c_vguiscreen.cpp. In this function a sphere enumerater is used to detect the screens, but this fails to give a result. To change this a vector can be added to contain all vguiscreens present within the level. This is done by adding:
CUtlVector<C_VGuiScreen*> g_pVGUIScreens;
Above
CUtlDict<KeyValues*, int> g_KeyValuesCache;
In client/c_vguiscreen.cpp.
In order to fill this vector the function
g_pVGUIScreens.AddToTail( this );
is added to the constructor of C_VGuiScreen and
g_pVGUIScreens.FindAndRemove( this );
is added to the destructor.
Next find the FindNearbyVguiScreen
and replace it with:
C_BaseEntity *FindNearbyVguiScreen( const Vector &viewPosition, const QAngle &viewAngle, int nTeam )
{
if ( IsX360() )
{
// X360TBD: Turn this on if feature actually used
return NULL;
}
C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer();
Assert( pLocalPlayer );
if ( !pLocalPlayer )
return NULL;
// Get the view direction...
Vector lookDir;
AngleVectors( viewAngle, &lookDir );
// Create a ray used for raytracing
Vector lookEnd;
VectorMA( viewPosition, 2.0f * VGUI_SCREEN_MODE_RADIUS, lookDir, lookEnd );
Ray_t lookRay;
lookRay.Init( viewPosition, lookEnd );
Vector vecOut, vecViewDelta;
float flBestDist = 2.0f;
C_VGuiScreen *pBestScreen = NULL;
for (int i = 0; i < g_pVGUIScreens.Count(); i++)
{
if (g_pVGUIScreens.IsValidIndex(i))
{
C_VGuiScreen *pScreen = g_pVGUIScreens[i];
if ( pScreen->IsAttachedToViewModel() )
continue;
// Don't bother with screens I'm behind...
// Hax - don't cancel backfacing with viewmodel attached screens.
// we can get prediction bugs that make us backfacing for one frame and
// it resets the mouse position if we lose focus.
if ( pScreen->IsBackfacing(viewPosition) )
continue;
// Don't bother with screens that are turned off
if (!pScreen->IsActive())
continue;
// FIXME: Should this maybe go into a derived class of some sort?
// Don't bother with screens on the wrong team
if (!pScreen->IsVisibleToTeam(nTeam))
continue;
if ( !pScreen->AcceptsInput() )
continue;
if ( pScreen->IsInputOnlyToOwner() && pScreen->GetPlayerOwner() != pLocalPlayer )
continue;
// Test perpendicular distance from the screen...
pScreen->GetVectors( NULL, NULL, &vecOut );
VectorSubtract( viewPosition, pScreen->GetAbsOrigin(), vecViewDelta );
float flPerpDist = DotProduct(vecViewDelta, vecOut);
if ( (flPerpDist < 0) || (flPerpDist > VGUI_SCREEN_MODE_RADIUS) )
continue;
// Perform a raycast to see where in barycentric coordinates the ray hits
// the viewscreen; if it doesn't hit it, you're not in the mode
float u, v, t;
if (!pScreen->IntersectWithRay( lookRay, &u, &v, &t ))
continue;
// Barycentric test
if ((u < 0) || (v < 0) || (u > 1) || (v > 1))
continue;
if ( t < flBestDist )
{
flBestDist = t;
pBestScreen = pScreen;
}
}
}
return pBestScreen;
}
Preventing Accidental Deaths
At this point the VGUI screens will not only work, but will be fully interactive. Buttons can be clicked by pressing the fire button whilst pointing at them on the VGUI screen. Try this with a rocket launcher though and a problem arises. The player’s gun still fires when the buttons are pressed, which in the example case will prove fatal and annoy the player. To prevent this, the simplest way is to make the player lower their gun and remove the fire button flags. This stops the firing and gives the player a cue to known when they can interact with the screen.
Start by looking at the CBasePlayer
function in server/player.h and adding the following lines:
public:
bool m_pVGUImode;
bool GetVGUIMode(void) {return m_pVGUImode;}
void SetVGUImode(bool newmode) { m_pVGUImode = newmode;}
Then in server/player.cpp at the end of the Spawn
function add
SetVGUImode(false);
And at the end of the ClientCommand
function, after the existing else ifs add
else if ( stricmp( cmd, "vguimode_true" ) == 0 )
{
SetVGUImode( true );
return true;
}
else if ( stricmp( cmd, "vguimode_false" ) == 0 )
{
SetVGUImode( false );
return true;
}
These functions will capture the commands we sent earlier when the player enters VGUI mode on the client and enable the server to know when the weapons should be lowered. Finally, in server/hl2/hl2_player.cpp beneath
UpdateWeaponPosture();
Add
// If we're in VGUI mode we should avoid shooting
if ( GetVGUIMode() )
{
m_nButtons &= ~(IN_ATTACK|IN_ATTACK2);
}
And inside the UpdateWeaponPosture
function add
if ( GetVGUIMode() )
{
//We're over a friendly, drop our weapon
if ( Weapon_Lower() == false )
{
//FIXME: We couldn't lower our weapon!
}
return;
}
Beneath
CBaseEntity *aimTarget = tr.m_pEnt;
These functions will stop the player firing, and lower the weapon respectively when the player is looking at a VGUI screen.