Over the Shoulder View: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
m (Fixed little error)
No edit summary
Line 1: Line 1:
{{cleanup}}
This tutorial will teach you how to create an over-the-shoulder camera view with collision detection. It has been created for the template SDK on the source 2007 engine branch, therefore additional modifications might be necessary to allow functionality with a different SDK.


== Overview ==


This tutorial will teach you how to create an over-the-shoulder camera view with collision detection.
== Seperating view and aiming angle ==


Code is also included for variable cross hair screen position that will adjust so the player can have true aim with
To allow correct aiming in this mode, the actual viewangle and the angle the player will shoot along have to be split up first. The new viewangle will also be used to calculate the movement vectors, that's why it has to be networked to the server as well.
his/her cross hairs since going third person breaks it.


== Steps ==
=== client.dll: iinput.h ===


=== Step 1 - Default the game to third-person view ===
Expand the input interface with these functions:


The first step is to make the game default to third person view. This is an easy step. Find and open the file <code>in_camera.cpp</code>, it will be in the client .dll files.
<source lang=cpp> virtual void const GetCamViewangles( QAngle &view ) = 0;
virtual void SetCamViewangles( QAngle const &view ) = 0; // in case you need to update the viewangles from the server
// you will require this alongside of, for example, a new user message</source>


Now, find every instance of <code>sv_cheats</code> and either delete it or comment it out (even the <code>if</code> conditions with it). This will get rid of the game checking to see if the server has sv_cheats enabled, and
=== client.dll: input.h ===
if it is not, kicking them out of third person view.


Go to the last function of the file at the very end, which is: <code>void CInput::Init_Camera( void )</code>
Add these to the public area of the actual input implementation:
Inside this function add: <code>m_fCameraInThirdPerson = true;</code> :


void CInput::Init_Camera( void )
<source lang=cpp> virtual void const GetCamViewangles( QAngle &view ){ view = m_angViewAngle; };
{
virtual void SetCamViewangles( QAngle const &view ){ m_angViewAngle = view; };</source>
m_CameraIsOrthographic = false;
'''m_fCameraInThirdPerson = true;'''
}


This will have the game defaulting to third person view at the very get go with no need for the player to do anything. This file receives no further changes and may be closed.
And these as private members:


=== Step 2 - Creating the over-the-shoulder view ===
<source lang=cpp> QAngle m_angViewAngle;
void CalcPlayerAngle( CUserCmd *cmd );</source>


This is the step that will create the necessary changes to make the camera be over-the-shoulder. Find and open the file <code>clientmode_shared.cpp</code>. It is also in the client dll files.
=== client.dll: in_main.cpp ===


Now, find the function <code>void ClientModeShared::OverrideView( CViewSetup *pSetup )</code>. This is where all the changes will be made.
Add this include:
<source lang=cpp>#include "view.h"</source>


Replace everything inside the function with:
Initialize the new QAngle in the constructor of CInput:
<pre>
<source lang=cpp>m_angViewAngle = vec3_angle;</source>
// Let the player override the view.
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
if(!pPlayer)
      return;
pPlayer->OverrideView( pSetup );
if( ::input->CAM_IsThirdPerson() )
{
const float Z = 96, X = 16, Y = 16;


Vector camForward, camRight, camUp;
Define the function that you have just declared:
<source lang=cpp>void CInput::CalcPlayerAngle( CUserCmd *cmd )
        AngleVectors( pPlayer->EyeAngles(), &camForward, &camRight, &camUp );
{
 
C_BasePlayer *pl = C_BasePlayer::GetLocalPlayer();
trace_t tr, tr2;
if ( !pl->AllowOvertheShoulderView() )
Vector vecStart, vecStop, vecDirection, vecSetDirection;
{
static float camCurrentY;
engine->SetViewAngles( m_angViewAngle );
static float camCurrentX=X; //used for fluid camera transfers
cmd->viewangles_cam = m_angViewAngle;
float camDelta=0.5f;
return;
vecStart=pSetup->origin;
}
AngleVectors(pPlayer->EyeAngles(), &vecDirection);
vecSetDirection.Init(0,0,1.0f);
vecDirection=vecDirection.Cross(vecSetDirection);
vecStop = vecStart + vecDirection*52.0f;
UTIL_TraceLine( vecStart, vecStop, MASK_ALL, pPlayer, COLLISION_GROUP_NONE, &tr );
if (tr.fraction == 1) //are we far enough away to not be hugging a wall with the camera?
{
if(camCurrentX < X)
camCurrentX +=camDelta;
if(camCurrentX >X)
camCurrentX=X;
VectorMA( pSetup->origin, camCurrentX, camRight, pSetup->origin); //set the right offset
VectorMA( pSetup->origin, Y, camUp, pSetup->origin);
vecStart=tr.endpos;
}
else
{
//we weren't clear on the right, lets check the left
vecStop = vecStart + vecDirection * -52.0f;
UTIL_TraceLine( vecStart, vecStop, MASK_ALL, pPlayer, COLLISION_GROUP_NONE, &tr );
if (tr.fraction == 1) //are we clear on the left?
{
if(camCurrentX > -X)
camCurrentX -=camDelta;
if(camCurrentX < -X)
camCurrentX=-X;
VectorMA( pSetup->origin, camCurrentX, camRight, pSetup->origin);
VectorMA( pSetup->origin, Y, camUp, pSetup->origin);
vecStart=tr.endpos;
}
else //not clear, so set the camera behind the player and raise it more than normal to maintain clear view
{
//set camera behind player because left and right are not clear
VectorMA( pSetup->origin, 0.0f, camRight, pSetup->origin);
//check to see if there is enough room above
AngleVectors(pPlayer->EyeAngles(), &vecDirection);
vecSetDirection.Init(1.0f,0,0);
vecDirection=vecDirection.Cross(vecSetDirection);
vecStop = vecStart +vecDirection*32.0f;
UTIL_TraceLine( vecStart, vecStop, MASK_ALL, pPlayer, COLLISION_GROUP_NONE, &tr);
if(tr.fraction == 1)
{
VectorMA( pSetup->origin, 32.0f, camUp, pSetup->origin);
vecStart=tr.endpos;
}
else //not enough room on left, right, or above, so move the camera eye level
//TODO: Add code to make player translucent as well so the player can see better
{
VectorMA( pSetup->origin, 0.0f, camUp, pSetup->origin);
}
}
}
AngleVectors(pPlayer->EyeAngles(), &vecDirection);
vecStop = vecStart + vecDirection * -Z;
UTIL_TraceLine( vecStart, vecStop, MASK_ALL, pPlayer, COLLISION_GROUP_NONE, &tr );
vecStart=pSetup->origin;
vecStop = vecStart+vecDirection*-Z;
UTIL_TraceLine( vecStart, vecStop, MASK_ALL, pPlayer, COLLISION_GROUP_NONE, &tr );
//multiply the default distance by the percentage the traceline traveled, should put the camera infront of the object
 
if(tr.fraction != 1)
{
camCurrentY = -Z * tr.fraction + 10.0f;
}
else
        {
camCurrentY=-Z;
VectorMA( pSetup->origin, camCurrentY, camForward, pSetup->origin);
}
}
</pre>


This will create an over-the-shoulder view that is defaulted to be over the right shoulder and it is offset 16 units right, 16 units up, and 96 units back. At time of creation this function is still undergoing tweaking and bug fixing and will be updated as it is fixed, but for most it will be more than enough to get someone started with their mod and they can make changes as they see fit. The camera also has collision detection built in by way of tracelines. The process has the camera checking if there is enough room on the right, and if there is it will set the x and z location to be +16/+16 of the player's origin. It then goes to the end where the distance behind the player is checked, where another two tracelines are done, one going back at the +16/+16 location, and one going straight back from the player. This is to prevent the camera from going into objects.
trace_t tr;
const Vector eyePos = pl->EyePosition();
UTIL_TraceLine( MainViewOrigin(), MainViewOrigin() + MainViewForward() * MAX_TRACE_LENGTH, MASK_SHOT, pl, COLLISION_GROUP_NONE, &tr );


If there is no room on the right, it will check the left and if there is room it will set x and z to -16/+16 and then check distance behind. If there is no room on the left or the right, z is made +32 and x 0. This will put the camera above the player's model's head so the player can still see. If there is not enough room above, the camera will be placed at eye level and whatever distance is determined. This is one area of tweaking needed still as it should make the model become around 75% transparent so the player can still see forward. This would only occur in crawl spaces such as air ducts or very small hallways.
// ensure that the player entity does not shoot towards the camera, get dist to plane where the player is on and add a constant
float flMinForward = abs( DotProduct( MainViewForward(), eyePos - MainViewOrigin() ) ) + 32.0f;
Vector vecTrace = tr.endpos - tr.startpos;
float flLenOld = vecTrace.NormalizeInPlace();
float flLen = max( flMinForward, flLenOld );
vecTrace *= flLen;


=== Step 3 - Fixing a Small Bug ===
Vector vecFinalDir = MainViewOrigin() + vecTrace - eyePos; //eyePos;
There is one small bug that must be fixed and that is jittery animations when you move the camera up and down in the game with the mouse while looking around.


Find and open the file <code>c_sdk_player.cpp</code>. This should be in the client/sdk folder.
QAngle playerangles;
VectorAngles( vecFinalDir, playerangles );
engine->SetViewAngles( playerangles );
cmd->viewangles_cam = m_angViewAngle;
}</source>
{{Note|At this point you are actually not supposed to access the view data, it still works fine, though.}}


Find the function <code>void C_SDKPlayer::UpdateClientSideAnimation()</code> and replace its body with:
Search for the functions
<source lang=cpp>void CInput::ExtraMouseSample( float frametime, bool active )</source>
<source lang=cpp>void CInput::CreateMove ( int sequence_number, float input_sample_frametime, bool active )</source>


// Update the animation data. It does the local check here so this works when using
In '''both''' of them add the following line after the <code>if ( active )</code> right before <code>engine->GetViewAngles( viewangles );</code>
// a third-person camera (and we don't have valid player angles).
<source lang=cpp>CalcPlayerAngle( cmd );</source>
if ( this == C_SDKPlayer::GetLocalSDKPlayer() )
m_PlayerAnimState->Update( EyeAngles()[YAW], EyeAngles()[PITCH] );
else
m_PlayerAnimState->Update( m_angEyeAngles[YAW], EyeAngles()[PITCH] );
BaseClass::UpdateClientSideAnimation();


Now animations should be perfectly smooth in scratch built mods.
=== client.dll: in_mouse.cpp ===


For HL2MP SDK, the steps are a bit different.
Search for the function:
<source lang=cpp>void CInput::MouseMove( CUserCmd *cmd )</source>


Find and open the file <code>hl2mp_player.cpp</code>. This should be in the server/hl2mp folder.
Now ''comment'' or ''remove'' these lines:


Find the function <code>void CHL2MP_Player::PostThink( void )</code> and replace its body with:
<source lang=cpp>QAngle viewangles;
engine->GetViewAngles( viewangles );
engine->SetViewAngles( viewangles );</source>


BaseClass::PostThink();
And change the call to <code>ApplyMouse(...)</code> to read this:
<source lang=cpp>ApplyMouse( m_angViewAngle, cmd, mouse_x, mouse_y );</source>
if ( GetFlags() & FL_DUCKING )
{
SetCollisionBounds( VEC_CROUCH_TRACE_MIN, VEC_CROUCH_TRACE_MAX );
}
QAngle angles = GetLocalAngles();
// We need to see if this is the client that we're dealing with.
CBasePlayer *pPlayer = dynamic_cast<CBasePlayer*>(this);
if ( pPlayer )
{
angles[PITCH] = EyeAngles()[PITCH];
angles[YAW] = EyeAngles()[YAW];
}
else
{
angles[PITCH] = EyeAngles()[PITCH];
angles[YAW] = m_angEyeAngles[YAW];
}


m_PlayerAnimState->Update(angles[YAW], angles[PITCH]);
=== shared: usercmd.h ===
SetLocalAngles( angles );


Now there should be no jitterness at all when moving your mouse in the thirdperson view.
Search for all occurances of <code>viewangles</code> in this file; duplicate each and rename the var to <code>viewangles_cam</code>. You should end up adding these lines at their respective places:


=== Step 4 - Self-adjusting crosshair ===
<source lang=cpp>viewangles_cam.Init();
viewangles_cam = src.viewangles_cam;
CRC32_ProcessBuffer( &crc, &viewangles_cam, sizeof( viewangles_cam ) );
QAngle viewangles_cam;</source>


This step will initialize self-adjusting cross hairs that will be drawn on the spot the player will actually shoot since the change of camera breaks the center-screen default of the cross hairs.
=== shared: usercmd.cpp ===


Find and open the file <code>hud_crosshair.cpp</code>. This is in the client files.
To properly network the new QAngle through the command stream, you need to make sure that you write and read it in the exact same order.


Now, find the fucntion <code>void CHudCrosshair::Paint( void )</code>.
Look for this snippet:


Replace the inside of this function with:
<source lang=cpp> if ( to->viewangles[ 2 ] != from->viewangles[ 2 ] )
{
buf->WriteOneBit( 1 );
buf->WriteFloat( to->viewangles[ 2 ] );
}
else
{
buf->WriteOneBit( 0 );
}</source>


if ( !m_pCrosshair )
Add this below:
return;
if ( !IsCurrentViewAccessAllowed() )
return;
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
Vector vecStart, vecStop, vecDirection, vecCrossPos;
trace_t tr;
AngleVectors(pPlayer->EyeAngles(), &vecDirection);
vecStart= pPlayer->EyePosition();
vecStop = vecStart + vecDirection * MAX_TRACE_LENGTH;
UTIL_TraceLine( vecStart, vecStop, MASK_ALL, pPlayer , COLLISION_GROUP_NONE, &tr );
ScreenTransform(tr.endpos, vecCrossPos);
m_pCrosshair->DrawSelf( 0.5f*ScreenWidth()+0.5f*ScreenWidth()*vecCrossPos[0]-0.5f*m_pCrosshair->Width(),
0.5f*ScreenHeight()+(0.5f*ScreenHeight()*-vecCrossPos[1])-0.5f*m_pCrosshair->Height(),
m_clrCrosshair );


<source lang=cpp> if ( to->viewangles_cam[ 0 ] != from->viewangles_cam[ 0 ] )
{
buf->WriteOneBit( 1 );
buf->WriteFloat( to->viewangles_cam[ 0 ] );
}
else
{
buf->WriteOneBit( 0 );
}


This function will now have the cross hairs be drawn on the exact spot the player is actually going to hit, minus cone of fire spread. What it does is draw a traceline out pretty much exactly as it would if the player were to fire a weapon, get the exact world coordinates of the spot it hits, turn those coordinates into screen coordinates (not exactly though, read below for the full explanation), and then adjust those values to be the REAL screen coordinates of where the crosshair should be while painting it on the screen.
if ( to->viewangles_cam[ 1 ] != from->viewangles_cam[ 1 ] )
{
buf->WriteOneBit( 1 );
buf->WriteFloat( to->viewangles_cam[ 1 ] );
}
else
{
buf->WriteOneBit( 0 );
}


== Conclusion ==
if ( to->viewangles_cam[ 2 ] != from->viewangles_cam[ 2 ] )
Now you should have an overview-the-shoulder view that will adjust based on the player's surroundings and adjusting cross hairs so the players can aim properly.
{
buf->WriteOneBit( 1 );
buf->WriteFloat( to->viewangles_cam[ 2 ] );
}
else
{
buf->WriteOneBit( 0 );
}</source>
 
Afer this snippet:
 
<source lang=cpp> if ( buf->ReadOneBit() )
{
move->viewangles[2] = buf->ReadFloat();
}</source>
 
Add these lines:
 
<source lang=cpp> if ( buf->ReadOneBit() )
{
move->viewangles_cam[0] = buf->ReadFloat();
}
 
if ( buf->ReadOneBit() )
{
move->viewangles_cam[1] = buf->ReadFloat();
}
 
if ( buf->ReadOneBit() )
{
move->viewangles_cam[2] = buf->ReadFloat();
}</source>
 
=== client.dll: prediction.cpp ===
 
Find the line:
 
<source lang=cpp>move->m_vecViewAngles = ucmd->viewangles;</source>
 
and replace it with:
 
<source lang=cpp>move->m_vecViewAngles = ucmd->viewangles_cam;</source>
 
This allows us to move relative to the view, not the aiming direction.
 
=== server.dll: player_command.cpp ===
 
Find the line:
 
<source lang=cpp>move->m_vecViewAngles = ucmd->viewangles;</source>
 
and replace it with:
 
<source lang=cpp>move->m_vecViewAngles = ucmd->viewangles_cam;</source>
 
The same as above, just for the server.
 
== Setting up the new view mode ==
 
The camera will be made to default to thirdperson mode and code will be added to calculate the view transformations.
 
=== client.dll: in_camera.cpp ===
 
Comment or remove this snippet:
 
<source lang=cpp>
// If cheats have been disabled, pull us back out of third-person view.
if ( sv_cheats && !sv_cheats->GetBool() )
{
CAM_ToFirstPerson();
return;
}</source>
 
In the function:
 
<source lang=cpp>void CInput::Init_Camera( void )</source>
 
add this line to the end:
 
<source lang=cpp> m_fCameraInThirdPerson = true;</source>
 
=== client.dll: clientmode_shared.cpp ===
 
Define these cvars near the top:
 
<source lang=cpp>static ConVar cam_ots_offset( "cam_ots_offset", "20 -75 20" );
static ConVar cam_ots_offsetlag( "cam_ots_offset_lag", "64.0" );
static ConVar cam_ots_originlag( "cam_ots_origin_lag", "38.0" );
static ConVar cam_ots_translucencythreshold( "cam_ots_translucencyThreshold", "32.0" );</source>
 
Find the function named:
 
<source lang=cpp>void ClientModeShared::OverrideView( CViewSetup *pSetup )</source>
 
Replace its content with this:
 
<source lang=cpp>{
QAngle camAngles;
 
// Let the player override the view.
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
if(!pPlayer)
return;
 
pPlayer->OverrideView( pSetup );
float flPlayerTranslucency = 0;
 
if( ::input->CAM_IsThirdPerson() )
{
if ( pPlayer->AllowOvertheShoulderView() )
{
// hack to hide weird interpolation issue for the origin of the listenserver host
static ConVarRef fakelag( "net_fakelag" );
if ( fakelag.GetInt() != 1 )
fakelag.SetValue( 1 );
 
enum // for readability
{
CAM_RIGHT = 0,
CAM_FORWRAD,
CAM_UP
};
const Vector camHull( 10, 10, 10 ); // collision test hull
float idealcamShoulderOffset[3] = { 20, -75, 20 }; // ideal local offset; right, fwd, up
float idealcamShoulderOffset_ColTest[3] = { 30, -75, 20 }; // ideal local offset; right, fwd, up
const float camLag = cam_ots_offsetlag.GetFloat(); // smoothing speed
const float camOriginLag = cam_ots_originlag.GetFloat();
 
if ( Q_strlen( cam_ots_offset.GetString() ) > 1 )
{
CCommand cmd;
cmd.Tokenize( cam_ots_offset.GetString() );
if ( cmd.ArgC() >= 3 )
{
for ( int i = 0; i < 3; i++ )
idealcamShoulderOffset_ColTest[ i ] = idealcamShoulderOffset[ i ] = atoi( cmd[ i ] );
idealcamShoulderOffset_ColTest[ CAM_RIGHT ] += 10 * Sign( idealcamShoulderOffset_ColTest[ CAM_RIGHT ] );
}
}
 
const float eyeposlag_snap_threshold = 128;
static Vector eyepos_lag = vec3_origin;
const Vector eyepos = pPlayer->EyePosition();
float eyeposDist = (eyepos - eyepos_lag).Length();
if ( eyeposDist > eyeposlag_snap_threshold )
eyepos_lag = eyepos;
 
// Approach eyeorigin
float speedVariety = eyeposDist / eyeposlag_snap_threshold;
if ( speedVariety )
{
Vector delta = eyepos - eyepos_lag;
float maxLength = delta.NormalizeInPlace();
delta *= min( maxLength, gpGlobals->frametime * (camOriginLag + camOriginLag * camOriginLag * speedVariety) );
eyepos_lag += delta;
}
 
QAngle viewAng;
Vector directions[ 3 ];
Vector idealCamPos;
static Vector lastLocalCamPos = vec3_origin;
trace_t tr;
 
::input->GetCamViewangles( viewAng );
AngleVectors( viewAng, &directions[CAM_FORWRAD], &directions[CAM_RIGHT], &directions[CAM_UP] );
 
idealCamPos = eyepos_lag;
 
// set up possible cam positions to test for
Vector camPositions[3] = { idealCamPos, idealCamPos, idealCamPos };
const float idealPos_Dir[3][3] = { 1, 1, 1,
-1, 1, 1,
0, 1, 1 }; // three possible offsets
for ( int x = 0; x < 3; x++ )
for ( int y = 0; y < 3; y++ )
{
UTIL_TraceHull( camPositions[x], camPositions[x] + idealcamShoulderOffset_ColTest[ y ] * directions[y] * idealPos_Dir[x][y],
-camHull, camHull, MASK_SOLID, pPlayer, COLLISION_GROUP_DEBRIS, &tr );
camPositions[x] = tr.endpos;
}
 
// choose the camoffsets that give us the furthest distance
int bestDirection = 0;
float maxBack = (camPositions[ 0 ] - eyepos_lag).Length();
for ( int i = 1; i < 3; i++ )
{
float curBack = abs( (camPositions[ i ] - eyepos_lag).Length() ) - 5.0f * i;
if ( maxBack < curBack )
{
maxBack = curBack;
bestDirection = i;
}
}
 
const float sortFinalCollisionTest[3] = { CAM_FORWRAD, CAM_UP, CAM_RIGHT }; // do collisiontest to the side at the end
// get the final cam position
Vector tmpidealCamPos = idealCamPos;
for ( int i = 0; i < 3; i++ )
{
int colTest = sortFinalCollisionTest[ i ];
 
// first check how far we can go actually
float maxShoulderOffset = idealcamShoulderOffset[colTest] * idealPos_Dir[ bestDirection ][ colTest ];
UTIL_TraceHull( tmpidealCamPos, tmpidealCamPos + maxShoulderOffset * directions[colTest],
-camHull, camHull, MASK_SOLID, pPlayer, COLLISION_GROUP_DEBRIS, &tr );
maxShoulderOffset = Sign( maxShoulderOffset ) * ( tr.endpos - tr.startpos ).Length();
tmpidealCamPos = tr.endpos;
 
// approach this position
float idealOffset = maxShoulderOffset;
if ( idealOffset != lastLocalCamPos[colTest] )
lastLocalCamPos[ colTest ] = Approach( idealOffset, lastLocalCamPos[ colTest ],
gpGlobals->frametime * camLag * abs(idealcamShoulderOffset[colTest] / idealcamShoulderOffset[CAM_RIGHT]) );
 
// don't punch through walls due to interpolation
UTIL_TraceHull( idealCamPos, idealCamPos + lastLocalCamPos[ colTest ] * directions[colTest],
-camHull, camHull, MASK_SOLID, pPlayer, COLLISION_GROUP_DEBRIS, &tr );
 
idealCamPos = tr.endpos;
}
 
// get rid of other unintended cam shaking
Vector localCamOffset = idealCamPos - eyepos_lag;
for ( int i = 0; i < 3; i++ )
{
float dot = DotProduct( directions[i], localCamOffset );
lastLocalCamPos[ i ] = ( min( idealPos_Dir[ bestDirection ][ i ], idealcamShoulderOffset[ i ] ) < 0) ?
max( lastLocalCamPos[i], dot ) : min( lastLocalCamPos[i], dot );
}
 
pSetup->origin = idealCamPos;
pSetup->angles = viewAng;
 
const float minOpaqueDistSquared = cam_ots_translucencythreshold.GetFloat() * cam_ots_translucencythreshold.GetFloat();
float distSqr = (idealCamPos - eyepos_lag).LengthSqr();
flPlayerTranslucency = 1.0f - min( 1, distSqr / minOpaqueDistSquared );
}
else
{
Vector cam_ofs;
 
::input->CAM_GetCameraOffset( cam_ofs );
 
camAngles[ PITCH ] = cam_ofs[ PITCH ];
camAngles[ YAW ] = cam_ofs[ YAW ];
camAngles[ ROLL ] = 0;
 
Vector camForward, camRight, camUp;
AngleVectors( camAngles, &camForward, &camRight, &camUp );
 
VectorMA( pSetup->origin, -cam_ofs[ ROLL ], camForward, pSetup->origin );
 
// Override angles from third person camera
VectorCopy( camAngles, pSetup->angles );
}
}
else if (::input->CAM_IsOrthographic())
{
pSetup->m_bOrtho = true;
float w, h;
::input->CAM_OrthographicSize( w, h );
w *= 0.5f;
h *= 0.5f;
pSetup->m_OrthoLeft  = -w;
pSetup->m_OrthoTop    = -h;
pSetup->m_OrthoRight  = w;
pSetup->m_OrthoBottom = h;
}
 
// translucency will not work flawlessly on player models that use one of the eyeshaders
// since those shaders do not support alpha blending by default
bool bWasTransulcent = pPlayer->GetRenderMode() != kRenderNormal ||
( pPlayer->GetActiveWeapon() && pPlayer->GetActiveWeapon()->GetRenderMode() != kRenderNormal );
bool bShouldBeTranslucent = !!flPlayerTranslucency;
if ( bWasTransulcent != bShouldBeTranslucent )
{
if ( bShouldBeTranslucent )
{
unsigned char alpha = ( 1.0f - flPlayerTranslucency ) * 255;
pPlayer->SetRenderMode( kRenderTransTexture, true );
pPlayer->SetRenderColorA( alpha );
if ( pPlayer->GetActiveWeapon() )
{
pPlayer->GetActiveWeapon()->SetRenderMode( kRenderTransTexture );
pPlayer->GetActiveWeapon()->SetRenderColorA( alpha );
}
}
else // not really required because this will be reset due to networking anyway
// disabling networking for those may cause other issues though
{
pPlayer->SetRenderMode( kRenderNormal, true );
pPlayer->SetRenderColorA( 255 );
if ( pPlayer->GetActiveWeapon() )
{
pPlayer->GetActiveWeapon()->SetRenderMode( kRenderNormal );
pPlayer->GetActiveWeapon()->SetRenderColorA( 255 );
}
}
}
}</source>
 
=== client.dll: c_baseplayer.h ===
 
Only allow this mode when we're alive and spawned; declare this function as public:
 
<source lang=cpp>virtual bool AllowOvertheShoulderView();</source>
 
=== client.dll: c_baseplayer.cpp ===
 
Implement the function above:
 
<source lang=cpp>bool C_BasePlayer::AllowOvertheShoulderView()
{
if ( !IsAlive() )
return false;
if ( GetTeamNumber() == TEAM_SPECTATOR )
return false;
return true;
}</source>
 
=== client.dll c_baseanimating.cpp ===
 
Muzzleflash particles are broken for thirdperson; to disable them find this function:
 
<source lang=cpp>void C_BaseAnimating::FireObsoleteEvent( const Vector& origin, const QAngle& angles, int event, const char *options )</source>
 
Right after this snippet:
 
<source lang=cpp> case CL_EVENT_MUZZLEFLASH0:
case CL_EVENT_MUZZLEFLASH1:
case CL_EVENT_MUZZLEFLASH2:
case CL_EVENT_MUZZLEFLASH3:
case CL_EVENT_NPC_MUZZLEFLASH0:
case CL_EVENT_NPC_MUZZLEFLASH1:
case CL_EVENT_NPC_MUZZLEFLASH2:
case CL_EVENT_NPC_MUZZLEFLASH3:
{</source>
 
add these lines:
 
<source lang=cpp> C_BaseEntity *follow = GetFollowedEntity();
if ( follow && follow->IsPlayer() && ::input->CAM_IsThirdPerson() )
break;</source>


== See also ==
== See also ==

Revision as of 14:54, 29 January 2011

This tutorial will teach you how to create an over-the-shoulder camera view with collision detection. It has been created for the template SDK on the source 2007 engine branch, therefore additional modifications might be necessary to allow functionality with a different SDK.


Seperating view and aiming angle

To allow correct aiming in this mode, the actual viewangle and the angle the player will shoot along have to be split up first. The new viewangle will also be used to calculate the movement vectors, that's why it has to be networked to the server as well.

client.dll: iinput.h

Expand the input interface with these functions:

	virtual void		const GetCamViewangles( QAngle &view ) = 0;
	virtual void		SetCamViewangles( QAngle const &view ) = 0; 	// in case you need to update the viewangles from the server
										// you will require this alongside of, for example, a new user message

client.dll: input.h

Add these to the public area of the actual input implementation:

	virtual		void		const GetCamViewangles( QAngle &view ){ view = m_angViewAngle; };
	virtual		void		SetCamViewangles( QAngle const &view ){ m_angViewAngle = view; };

And these as private members:

	QAngle		m_angViewAngle;
	void		CalcPlayerAngle( CUserCmd *cmd );

client.dll: in_main.cpp

Add this include:

#include "view.h"

Initialize the new QAngle in the constructor of CInput:

m_angViewAngle = vec3_angle;

Define the function that you have just declared:

void CInput::CalcPlayerAngle( CUserCmd *cmd )
{
	C_BasePlayer *pl = C_BasePlayer::GetLocalPlayer();
	if ( !pl->AllowOvertheShoulderView() )
	{
		engine->SetViewAngles( m_angViewAngle );
		cmd->viewangles_cam = m_angViewAngle;
		return;
	}

	trace_t tr;
	const Vector eyePos = pl->EyePosition();
	UTIL_TraceLine( MainViewOrigin(), MainViewOrigin() + MainViewForward() * MAX_TRACE_LENGTH, MASK_SHOT, pl, COLLISION_GROUP_NONE, &tr );

	// ensure that the player entity does not shoot towards the camera, get dist to plane where the player is on and add a constant
	float flMinForward = abs( DotProduct( MainViewForward(), eyePos - MainViewOrigin() ) ) + 32.0f;
	Vector vecTrace = tr.endpos - tr.startpos;
	float flLenOld = vecTrace.NormalizeInPlace();
	float flLen = max( flMinForward, flLenOld );
	vecTrace *= flLen;

	Vector vecFinalDir = MainViewOrigin() + vecTrace - eyePos; //eyePos;

	QAngle playerangles;
	VectorAngles( vecFinalDir, playerangles );
	engine->SetViewAngles( playerangles );
	cmd->viewangles_cam = m_angViewAngle;
}
Note.pngNote:At this point you are actually not supposed to access the view data, it still works fine, though.

Search for the functions

void CInput::ExtraMouseSample( float frametime, bool active )
void CInput::CreateMove ( int sequence_number, float input_sample_frametime, bool active )

In both of them add the following line after the if ( active ) right before engine->GetViewAngles( viewangles );

CalcPlayerAngle( cmd );

client.dll: in_mouse.cpp

Search for the function:

void CInput::MouseMove( CUserCmd *cmd )

Now comment or remove these lines:

QAngle	viewangles;
engine->GetViewAngles( viewangles );
engine->SetViewAngles( viewangles );

And change the call to ApplyMouse(...) to read this:

ApplyMouse( m_angViewAngle, cmd, mouse_x, mouse_y );

shared: usercmd.h

Search for all occurances of viewangles in this file; duplicate each and rename the var to viewangles_cam. You should end up adding these lines at their respective places:

viewangles_cam.Init();
viewangles_cam		= src.viewangles_cam;
CRC32_ProcessBuffer( &crc, &viewangles_cam, sizeof( viewangles_cam ) );
QAngle	viewangles_cam;

shared: usercmd.cpp

To properly network the new QAngle through the command stream, you need to make sure that you write and read it in the exact same order.

Look for this snippet:

	if ( to->viewangles[ 2 ] != from->viewangles[ 2 ] )
	{
		buf->WriteOneBit( 1 );
		buf->WriteFloat( to->viewangles[ 2 ] );
	}
	else
	{
		buf->WriteOneBit( 0 );
	}

Add this below:

	if ( to->viewangles_cam[ 0 ] != from->viewangles_cam[ 0 ] )
	{
		buf->WriteOneBit( 1 );
		buf->WriteFloat( to->viewangles_cam[ 0 ] );
	}
	else
	{
		buf->WriteOneBit( 0 );
	}

	if ( to->viewangles_cam[ 1 ] != from->viewangles_cam[ 1 ] )
	{
		buf->WriteOneBit( 1 );
		buf->WriteFloat( to->viewangles_cam[ 1 ] );
	}
	else
	{
		buf->WriteOneBit( 0 );
	}

	if ( to->viewangles_cam[ 2 ] != from->viewangles_cam[ 2 ] )
	{
		buf->WriteOneBit( 1 );
		buf->WriteFloat( to->viewangles_cam[ 2 ] );
	}
	else
	{
		buf->WriteOneBit( 0 );
	}

Afer this snippet:

	if ( buf->ReadOneBit() )
	{
		move->viewangles[2] = buf->ReadFloat();
	}

Add these lines:

	if ( buf->ReadOneBit() )
	{
		move->viewangles_cam[0] = buf->ReadFloat();
	}

	if ( buf->ReadOneBit() )
	{
		move->viewangles_cam[1] = buf->ReadFloat();
	}

	if ( buf->ReadOneBit() )
	{
		move->viewangles_cam[2] = buf->ReadFloat();
	}

client.dll: prediction.cpp

Find the line:

move->m_vecViewAngles	= ucmd->viewangles;

and replace it with:

move->m_vecViewAngles	= ucmd->viewangles_cam;

This allows us to move relative to the view, not the aiming direction.

server.dll: player_command.cpp

Find the line:

move->m_vecViewAngles		= ucmd->viewangles;

and replace it with:

move->m_vecViewAngles		= ucmd->viewangles_cam;

The same as above, just for the server.

Setting up the new view mode

The camera will be made to default to thirdperson mode and code will be added to calculate the view transformations.

client.dll: in_camera.cpp

Comment or remove this snippet:

	// If cheats have been disabled, pull us back out of third-person view.
	if ( sv_cheats && !sv_cheats->GetBool() )
	{
		CAM_ToFirstPerson();
		return;
	}

In the function:

void CInput::Init_Camera( void )

add this line to the end:

	m_fCameraInThirdPerson = true;

client.dll: clientmode_shared.cpp

Define these cvars near the top:

static ConVar cam_ots_offset( "cam_ots_offset", "20 -75 20" );
static ConVar cam_ots_offsetlag( "cam_ots_offset_lag", "64.0" );
static ConVar cam_ots_originlag( "cam_ots_origin_lag", "38.0" );
static ConVar cam_ots_translucencythreshold( "cam_ots_translucencyThreshold", "32.0" );

Find the function named:

void ClientModeShared::OverrideView( CViewSetup *pSetup )

Replace its content with this:

{
	QAngle camAngles;

	// Let the player override the view.
	C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
	if(!pPlayer)
		return;

	pPlayer->OverrideView( pSetup );
	float flPlayerTranslucency = 0;

	if( ::input->CAM_IsThirdPerson() )
	{
		if ( pPlayer->AllowOvertheShoulderView() )
		{
			// hack to hide weird interpolation issue for the origin of the listenserver host
			static ConVarRef fakelag( "net_fakelag" );
			if ( fakelag.GetInt() != 1 )
				fakelag.SetValue( 1 );

			enum // for readability
			{
				CAM_RIGHT = 0,
				CAM_FORWRAD,
				CAM_UP
			};
			const Vector camHull( 10, 10, 10 ); // collision test hull
			float idealcamShoulderOffset[3] = { 20, -75, 20 }; // ideal local offset; right, fwd, up
			float idealcamShoulderOffset_ColTest[3] = { 30, -75, 20 }; // ideal local offset; right, fwd, up
			const float camLag = cam_ots_offsetlag.GetFloat(); // smoothing speed
			const float camOriginLag = cam_ots_originlag.GetFloat();

			if ( Q_strlen( cam_ots_offset.GetString() ) > 1 )
			{
				CCommand cmd;
				cmd.Tokenize( cam_ots_offset.GetString() );
				if ( cmd.ArgC() >= 3 )
				{
					for ( int i = 0; i < 3; i++ )
						idealcamShoulderOffset_ColTest[ i ] = idealcamShoulderOffset[ i ] = atoi( cmd[ i ] );
					idealcamShoulderOffset_ColTest[ CAM_RIGHT ] += 10 * Sign( idealcamShoulderOffset_ColTest[ CAM_RIGHT ] );
				}
			}

			const float eyeposlag_snap_threshold = 128;
			static Vector eyepos_lag = vec3_origin;
			const Vector eyepos = pPlayer->EyePosition();
			float eyeposDist = (eyepos - eyepos_lag).Length();
			if ( eyeposDist > eyeposlag_snap_threshold )
				eyepos_lag = eyepos;

			// Approach eyeorigin
			float speedVariety = eyeposDist / eyeposlag_snap_threshold;
			if ( speedVariety )
			{
				Vector delta = eyepos - eyepos_lag;
				float maxLength = delta.NormalizeInPlace();
				delta *= min( maxLength, gpGlobals->frametime * (camOriginLag + camOriginLag * camOriginLag * speedVariety) );
				eyepos_lag += delta;
			}

			QAngle viewAng;
			Vector directions[ 3 ];
			Vector idealCamPos;
			static Vector lastLocalCamPos = vec3_origin;
			trace_t tr;

			::input->GetCamViewangles( viewAng );
			AngleVectors( viewAng, &directions[CAM_FORWRAD], &directions[CAM_RIGHT], &directions[CAM_UP] );

			idealCamPos = eyepos_lag;

			// set up possible cam positions to test for
			Vector camPositions[3] = { idealCamPos, idealCamPos, idealCamPos };
			const float idealPos_Dir[3][3] =	{	1, 1, 1,
									-1, 1, 1,
									0, 1, 1		}; // three possible offsets
			for ( int x = 0; x < 3; x++ )
				for ( int y = 0; y < 3; y++ )
				{
					UTIL_TraceHull( camPositions[x], camPositions[x] + idealcamShoulderOffset_ColTest[ y ] * directions[y] * idealPos_Dir[x][y],
						-camHull, camHull, MASK_SOLID, pPlayer, COLLISION_GROUP_DEBRIS, &tr );
					camPositions[x] = tr.endpos;
				}

			// choose the camoffsets that give us the furthest distance
			int bestDirection = 0;
			float maxBack = (camPositions[ 0 ] - eyepos_lag).Length();
			for ( int i = 1; i < 3; i++ )
			{
				float curBack = abs( (camPositions[ i ] - eyepos_lag).Length() ) - 5.0f * i;
				if ( maxBack < curBack )
				{
					maxBack = curBack;
					bestDirection = i;
				}
			}

			const float sortFinalCollisionTest[3] = { CAM_FORWRAD, CAM_UP, CAM_RIGHT }; // do collisiontest to the side at the end
			// get the final cam position
			Vector tmpidealCamPos = idealCamPos;
			for ( int i = 0; i < 3; i++ )
			{
				int colTest = sortFinalCollisionTest[ i ];

				// first check how far we can go actually
				float maxShoulderOffset = idealcamShoulderOffset[colTest] * idealPos_Dir[ bestDirection ][ colTest ];
				UTIL_TraceHull( tmpidealCamPos, tmpidealCamPos + maxShoulderOffset * directions[colTest],
					-camHull, camHull, MASK_SOLID, pPlayer, COLLISION_GROUP_DEBRIS, &tr );
				maxShoulderOffset = Sign( maxShoulderOffset ) * ( tr.endpos - tr.startpos ).Length();
				tmpidealCamPos = tr.endpos;

				// approach this position
				float idealOffset = maxShoulderOffset;
				if ( idealOffset != lastLocalCamPos[colTest] )
					lastLocalCamPos[ colTest ] = Approach( idealOffset, lastLocalCamPos[ colTest ],
					gpGlobals->frametime * camLag * abs(idealcamShoulderOffset[colTest] / idealcamShoulderOffset[CAM_RIGHT]) );

				// don't punch through walls due to interpolation
				UTIL_TraceHull( idealCamPos, idealCamPos + lastLocalCamPos[ colTest ] * directions[colTest],
					-camHull, camHull, MASK_SOLID, pPlayer, COLLISION_GROUP_DEBRIS, &tr );

				idealCamPos = tr.endpos;
			}

			// get rid of other unintended cam shaking
			Vector localCamOffset = idealCamPos - eyepos_lag;
			for ( int i = 0; i < 3; i++ )
			{
				float dot = DotProduct( directions[i], localCamOffset );
				lastLocalCamPos[ i ] = ( min( idealPos_Dir[ bestDirection ][ i ], idealcamShoulderOffset[ i ] ) < 0) ?
					max( lastLocalCamPos[i], dot ) : min( lastLocalCamPos[i], dot );
			}

			pSetup->origin = idealCamPos;
			pSetup->angles = viewAng;

			const float minOpaqueDistSquared = cam_ots_translucencythreshold.GetFloat() * cam_ots_translucencythreshold.GetFloat();
			float distSqr = (idealCamPos - eyepos_lag).LengthSqr();
			flPlayerTranslucency = 1.0f - min( 1, distSqr / minOpaqueDistSquared );
		}
		else
		{
			Vector cam_ofs;

			::input->CAM_GetCameraOffset( cam_ofs );

			camAngles[ PITCH ] = cam_ofs[ PITCH ];
			camAngles[ YAW ] = cam_ofs[ YAW ];
			camAngles[ ROLL ] = 0;

			Vector camForward, camRight, camUp;
			AngleVectors( camAngles, &camForward, &camRight, &camUp );

			VectorMA( pSetup->origin, -cam_ofs[ ROLL ], camForward, pSetup->origin );

			// Override angles from third person camera
			VectorCopy( camAngles, pSetup->angles );
		}
	}
	else if (::input->CAM_IsOrthographic())
	{
		pSetup->m_bOrtho = true;
		float w, h;
		::input->CAM_OrthographicSize( w, h );
		w *= 0.5f;
		h *= 0.5f;
		pSetup->m_OrthoLeft   = -w;
		pSetup->m_OrthoTop    = -h;
		pSetup->m_OrthoRight  = w;
		pSetup->m_OrthoBottom = h;
	}

	// translucency will not work flawlessly on player models that use one of the eyeshaders
	// since those shaders do not support alpha blending by default
	bool bWasTransulcent = pPlayer->GetRenderMode() != kRenderNormal || 
		( pPlayer->GetActiveWeapon() && pPlayer->GetActiveWeapon()->GetRenderMode() != kRenderNormal );
	bool bShouldBeTranslucent = !!flPlayerTranslucency;
	if ( bWasTransulcent != bShouldBeTranslucent )
	{
		if ( bShouldBeTranslucent )
		{
			unsigned char alpha = ( 1.0f - flPlayerTranslucency ) * 255;
			pPlayer->SetRenderMode( kRenderTransTexture, true );
			pPlayer->SetRenderColorA( alpha );
			if ( pPlayer->GetActiveWeapon() )
			{
				pPlayer->GetActiveWeapon()->SetRenderMode( kRenderTransTexture );
				pPlayer->GetActiveWeapon()->SetRenderColorA( alpha );
			}
		}
		else	// not really required because this will be reset due to networking anyway
				// disabling networking for those may cause other issues though
		{
			pPlayer->SetRenderMode( kRenderNormal, true );
			pPlayer->SetRenderColorA( 255 );
			if ( pPlayer->GetActiveWeapon() )
			{
				pPlayer->GetActiveWeapon()->SetRenderMode( kRenderNormal );
				pPlayer->GetActiveWeapon()->SetRenderColorA( 255 );
			}
		}
	}
}

client.dll: c_baseplayer.h

Only allow this mode when we're alive and spawned; declare this function as public:

virtual bool			AllowOvertheShoulderView();

client.dll: c_baseplayer.cpp

Implement the function above:

bool C_BasePlayer::AllowOvertheShoulderView()
{
	if ( !IsAlive() )
		return false;
	if ( GetTeamNumber() == TEAM_SPECTATOR )
		return false;
	return true;
}

client.dll c_baseanimating.cpp

Muzzleflash particles are broken for thirdperson; to disable them find this function:

void C_BaseAnimating::FireObsoleteEvent( const Vector& origin, const QAngle& angles, int event, const char *options )

Right after this snippet:

	case CL_EVENT_MUZZLEFLASH0:
	case CL_EVENT_MUZZLEFLASH1:
	case CL_EVENT_MUZZLEFLASH2:
	case CL_EVENT_MUZZLEFLASH3:
	case CL_EVENT_NPC_MUZZLEFLASH0:
	case CL_EVENT_NPC_MUZZLEFLASH1:
	case CL_EVENT_NPC_MUZZLEFLASH2:
	case CL_EVENT_NPC_MUZZLEFLASH3:
		{

add these lines:

			C_BaseEntity *follow = GetFollowedEntity();
			if ( follow && follow->IsPlayer() && ::input->CAM_IsThirdPerson() )
				break;

See also

Template:Otherlang:en Template:Otherlang:en:ru