Interactive Ingame VGUI Panels

From Valve Developer Community
Jump to: navigation, search

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.