Fixing the player animation state (Single Player)
The main goal of this tutorial is to solve the issues and implement a working system in order to enable the pose parameters check. Valve's Source Engine 2007 "Single Player" source code will be required for this tutorial.
The first section of this tutorial covers the implementation of the main files to enable the player animation state system. Several parts of the source code come from Valve, so giving them credits would not be a bad idea.
Requirements
- Valve's Source Engine code (2007) Single Player.
- Nem's tools ( GCFScape & VTFEdit )
Animation state system implementation.
In the first place you shall create two files (.cpp / .h) that are "shared" between the Client and Server. Open up the two solutions and add two files named 'singleplayer_animstate.cpp', 'singleplayer_animstate.h' Once this step is completed, open up 'singleplayer_animstate.h' and add the following code:
//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose: Single Player animation state 'handler'. This utility is used
// to evaluate the pose parameter value based on the direction
// and speed of the player.
//
//====================================================================================//
#ifndef SINGLEPLAYER_ANIMSTATE_H
#define SINGLEPLAYER_ANIMSTATE_H
#ifdef _WIN32
#pragma once
#endif
#include "cbase.h"
#ifdef CLIENT_DLL
#include "c_baseplayer.h"
#else
#include "player.h"
#endif
class CSinglePlayerAnimState
{
public:
enum
{
TURN_NONE = 0,
TURN_LEFT,
TURN_RIGHT
};
CSinglePlayerAnimState( CBasePlayer *pPlayer );
void Init( CBasePlayer *pPlayer );
Activity BodyYawTranslateActivity( Activity activity );
void Update();
const QAngle& GetRenderAngles();
void GetPoseParameters( CStudioHdr *pStudioHdr, float poseParameter[MAXSTUDIOPOSEPARAM] );
CBasePlayer *GetBasePlayer();
void Release();
private:
void GetOuterAbsVelocity( Vector& vel );
int ConvergeAngles( float goal,float maxrate, float dt, float& current );
void EstimateYaw( void );
void ComputePoseParam_BodyYaw( void );
void ComputePoseParam_BodyPitch( CStudioHdr *pStudioHdr );
void ComputePoseParam_BodyLookYaw( void );
void ComputePoseParam_HeadPitch( CStudioHdr *pStudioHdr );
void ComputePlaybackRate();
CBasePlayer *m_pPlayer;
float m_flGaitYaw;
float m_flStoredCycle;
float m_flGoalFeetYaw;
float m_flCurrentFeetYaw;
float m_flCurrentTorsoYaw;
float m_flLastYaw;
float m_flLastTurnTime;
int m_nTurningInPlace;
QAngle m_angRender;
float m_flTurnCorrectionTime;
};
CSinglePlayerAnimState *CreatePlayerAnimationState( CBasePlayer *pPlayer );
#endif // SINGLEPLAYER_ANIMSTATE_H
Once this is completed, you will open up 'singleplayer_animstate.cpp' and add the following code:
//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose: Single Player animation state 'handler'. This utility is used
// to evaluate the pose parameter value based on the direction
// and speed of the player.
//
//====================================================================================//
#include "cbase.h"
#include "singleplayer_animstate.h"
#include "tier0/vprof.h"
#include "animation.h"
#include "studio.h"
#include "apparent_velocity_helper.h"
#include "utldict.h"
#include "filesystem.h"
extern ConVar mp_facefronttime, mp_feetyawrate, mp_ik;
#define MIN_TURN_ANGLE_REQUIRING_TURN_ANIMATION 15.0f
CSinglePlayerAnimState *CreatePlayerAnimationState( CBasePlayer *pPlayer )
{
MDLCACHE_CRITICAL_SECTION();
CSinglePlayerAnimState *pState = new CSinglePlayerAnimState( pPlayer );
pState->Init(pPlayer);
return pState;
}
// Below this many degrees, slow down turning rate linearly
#define FADE_TURN_DEGREES 45.0f
// After this, need to start turning feet
#define MAX_TORSO_ANGLE 90.0f
// Below this amount, don't play a turning animation/perform IK
#define MIN_TURN_ANGLE_REQUIRING_TURN_ANIMATION 15.0f
//static ConVar tf2_feetyawrunscale( "tf2_feetyawrunscale", "2", FCVAR_REPLICATED, "Multiplier on tf2_feetyawrate to allow turning faster when running." );
extern ConVar sv_backspeed;
extern ConVar mp_feetyawrate;
extern ConVar mp_facefronttime;
extern ConVar mp_ik;
CSinglePlayerAnimState::CSinglePlayerAnimState( CBasePlayer *pPlayer ): m_pPlayer( pPlayer )
{
m_flGaitYaw = 0.0f;
m_flGoalFeetYaw = 0.0f;
m_flCurrentFeetYaw = 0.0f;
m_flCurrentTorsoYaw = 0.0f;
m_flLastYaw = 0.0f;
m_flLastTurnTime = 0.0f;
m_flTurnCorrectionTime = 0.0f;
m_pPlayer = NULL;
};
void CSinglePlayerAnimState::Init( CBasePlayer *pPlayer )
{
m_pPlayer = pPlayer;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CSinglePlayerAnimState::Update()
{
m_angRender = GetBasePlayer()->GetLocalAngles();
ComputePoseParam_BodyYaw();
ComputePoseParam_BodyPitch(GetBasePlayer()->GetModelPtr());
ComputePoseParam_BodyLookYaw();
ComputePoseParam_HeadPitch(GetBasePlayer()->GetModelPtr());
ComputePlaybackRate();
}
void CSinglePlayerAnimState::Release()
{
delete this;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CSinglePlayerAnimState::ComputePlaybackRate()
{
// Determine ideal playback rate
Vector vel;
GetOuterAbsVelocity( vel );
float speed = vel.Length2D();
bool isMoving = ( speed > 0.5f ) ? true : false;
float maxspeed = GetBasePlayer()->GetSequenceGroundSpeed( GetBasePlayer()->GetSequence() );
if ( isMoving && ( maxspeed > 0.0f ) )
{
float flFactor = 1.0f;
// Note this gets set back to 1.0 if sequence changes due to ResetSequenceInfo below
GetBasePlayer()->SetPlaybackRate( ( speed * flFactor ) / maxspeed );
// BUG BUG:
// This stuff really should be m_flPlaybackRate = speed / m_flGroundSpeed
}
else
{
GetBasePlayer()->SetPlaybackRate( 1.0f );
}
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : CBasePlayer
//-----------------------------------------------------------------------------
CBasePlayer *CSinglePlayerAnimState::GetBasePlayer()
{
return m_pPlayer;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : dt -
//-----------------------------------------------------------------------------
void CSinglePlayerAnimState::EstimateYaw( void )
{
float dt = gpGlobals->frametime;
if ( !dt )
{
return;
}
Vector est_velocity;
QAngle angles;
GetOuterAbsVelocity( est_velocity );
angles = GetBasePlayer()->GetLocalAngles();
if ( est_velocity[1] == 0 && est_velocity[0] == 0 )
{
float flYawDiff = angles[YAW] - m_flGaitYaw;
flYawDiff = flYawDiff - (int)(flYawDiff / 360) * 360;
if (flYawDiff > 180)
flYawDiff -= 360;
if (flYawDiff < -180)
flYawDiff += 360;
if (dt < 0.25)
flYawDiff *= dt * 4;
else
flYawDiff *= dt;
m_flGaitYaw += flYawDiff;
m_flGaitYaw = m_flGaitYaw - (int)(m_flGaitYaw / 360) * 360;
}
else
{
m_flGaitYaw = (atan2(est_velocity[1], est_velocity[0]) * 180 / M_PI);
if (m_flGaitYaw > 180)
m_flGaitYaw = 180;
else if (m_flGaitYaw < -180)
m_flGaitYaw = -180;
}
}
//-----------------------------------------------------------------------------
// Purpose: Override for backpeddling
// Input : dt -
//-----------------------------------------------------------------------------
void CSinglePlayerAnimState::ComputePoseParam_BodyYaw( void )
{
int iYaw = GetBasePlayer()->LookupPoseParameter( "move_yaw" );
if ( iYaw < 0 )
return;
// view direction relative to movement
float flYaw;
EstimateYaw();
QAngle angles = GetBasePlayer()->GetLocalAngles();
float ang = angles[ YAW ];
if ( ang > 180.0f )
{
ang -= 360.0f;
}
else if ( ang < -180.0f )
{
ang += 360.0f;
}
// calc side to side turning
flYaw = ang - m_flGaitYaw;
// Invert for mapping into 8way blend
flYaw = -flYaw;
flYaw = flYaw - (int)(flYaw / 360) * 360;
if (flYaw < -180)
{
flYaw = flYaw + 360;
}
else if (flYaw > 180)
{
flYaw = flYaw - 360;
}
GetBasePlayer()->SetPoseParameter( iYaw, flYaw );
#ifndef CLIENT_DLL
//Adrian: Make the model's angle match the legs so the hitboxes match on both sides.
GetBasePlayer()->SetLocalAngles( QAngle( GetBasePlayer()->EyeAngles().x, m_flCurrentFeetYaw, 0 ) );
#endif
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CSinglePlayerAnimState::ComputePoseParam_BodyPitch( CStudioHdr *pStudioHdr )
{
// Get pitch from v_angle
float flPitch = GetBasePlayer()->GetLocalAngles()[ PITCH ];
if ( flPitch > 180.0f )
{
flPitch -= 360.0f;
}
flPitch = clamp( flPitch, -90, 90 );
QAngle absangles = GetBasePlayer()->GetAbsAngles();
absangles.x = 0.0f;
m_angRender = absangles;
// See if we have a blender for pitch
GetBasePlayer()->SetPoseParameter( pStudioHdr, "aim_pitch", flPitch );
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : goal -
// maxrate -
// dt -
// current -
// Output : int
//-----------------------------------------------------------------------------
int CSinglePlayerAnimState::ConvergeAngles( float goal,float maxrate, float dt, float& current )
{
int direction = TURN_NONE;
float anglediff = goal - current;
float anglediffabs = fabs( anglediff );
anglediff = AngleNormalize( anglediff );
float scale = 1.0f;
if ( anglediffabs <= FADE_TURN_DEGREES )
{
scale = anglediffabs / FADE_TURN_DEGREES;
// Always do at least a bit of the turn ( 1% )
scale = clamp( scale, 0.01f, 1.0f );
}
float maxmove = maxrate * dt * scale;
if ( fabs( anglediff ) < maxmove )
{
current = goal;
}
else
{
if ( anglediff > 0 )
{
current += maxmove;
direction = TURN_LEFT;
}
else
{
current -= maxmove;
direction = TURN_RIGHT;
}
}
current = AngleNormalize( current );
return direction;
}
void CSinglePlayerAnimState::ComputePoseParam_BodyLookYaw( void )
{
QAngle absangles = GetBasePlayer()->GetAbsAngles();
absangles.y = AngleNormalize( absangles.y );
m_angRender = absangles;
// See if we even have a blender for pitch
int upper_body_yaw = GetBasePlayer()->LookupPoseParameter( "aim_yaw" );
if ( upper_body_yaw < 0 )
{
return;
}
// Assume upper and lower bodies are aligned and that we're not turning
float flGoalTorsoYaw = 0.0f;
int turning = TURN_NONE;
float turnrate = 360.0f;
Vector vel;
GetOuterAbsVelocity( vel );
bool isMoving = ( vel.Length() > 1.0f ) ? true : false;
if ( !isMoving )
{
// Just stopped moving, try and clamp feet
if ( m_flLastTurnTime <= 0.0f )
{
m_flLastTurnTime = gpGlobals->curtime;
m_flLastYaw = GetBasePlayer()->EyeAngles().y;
// Snap feet to be perfectly aligned with torso/eyes
m_flGoalFeetYaw = GetBasePlayer()->EyeAngles().y;
m_flCurrentFeetYaw = m_flGoalFeetYaw;
m_nTurningInPlace = TURN_NONE;
}
// If rotating in place, update stasis timer
if ( m_flLastYaw != GetBasePlayer()->EyeAngles().y )
{
m_flLastTurnTime = gpGlobals->curtime;
m_flLastYaw = GetBasePlayer()->EyeAngles().y;
}
if ( m_flGoalFeetYaw != m_flCurrentFeetYaw )
{
m_flLastTurnTime = gpGlobals->curtime;
}
turning = ConvergeAngles( m_flGoalFeetYaw, turnrate, gpGlobals->frametime, m_flCurrentFeetYaw );
QAngle eyeAngles = GetBasePlayer()->EyeAngles();
QAngle vAngle = GetBasePlayer()->GetLocalAngles();
// See how far off current feetyaw is from true yaw
float yawdelta = GetBasePlayer()->EyeAngles().y - m_flCurrentFeetYaw;
yawdelta = AngleNormalize( yawdelta );
bool rotated_too_far = false;
float yawmagnitude = fabs( yawdelta );
// If too far, then need to turn in place
if ( yawmagnitude > 45 )
{
rotated_too_far = true;
}
// Standing still for a while, rotate feet around to face forward
// Or rotated too far
// FIXME: Play an in place turning animation
if ( rotated_too_far ||
( gpGlobals->curtime > m_flLastTurnTime + mp_facefronttime.GetFloat() ) )
{
m_flGoalFeetYaw = GetBasePlayer()->EyeAngles().y;
m_flLastTurnTime = gpGlobals->curtime;
/* float yd = m_flCurrentFeetYaw - m_flGoalFeetYaw;
if ( yd > 0 )
{
m_nTurningInPlace = TURN_RIGHT;
}
else if ( yd < 0 )
{
m_nTurningInPlace = TURN_LEFT;
}
else
{
m_nTurningInPlace = TURN_NONE;
}
turning = ConvergeAngles( m_flGoalFeetYaw, turnrate, gpGlobals->frametime, m_flCurrentFeetYaw );
yawdelta = GetBasePlayer()->EyeAngles().y - m_flCurrentFeetYaw;*/
}
// Snap upper body into position since the delta is already smoothed for the feet
flGoalTorsoYaw = yawdelta;
m_flCurrentTorsoYaw = flGoalTorsoYaw;
}
else
{
m_flLastTurnTime = 0.0f;
m_nTurningInPlace = TURN_NONE;
m_flCurrentFeetYaw = m_flGoalFeetYaw = GetBasePlayer()->EyeAngles().y;
flGoalTorsoYaw = 0.0f;
m_flCurrentTorsoYaw = GetBasePlayer()->EyeAngles().y - m_flCurrentFeetYaw;
}
if ( turning == TURN_NONE )
{
m_nTurningInPlace = turning;
}
if ( m_nTurningInPlace != TURN_NONE )
{
// If we're close to finishing the turn, then turn off the turning animation
if ( fabs( m_flCurrentFeetYaw - m_flGoalFeetYaw ) < MIN_TURN_ANGLE_REQUIRING_TURN_ANIMATION )
{
m_nTurningInPlace = TURN_NONE;
}
}
// Rotate entire body into position
absangles = GetBasePlayer()->GetAbsAngles();
absangles.y = m_flCurrentFeetYaw;
m_angRender = absangles;
GetBasePlayer()->SetPoseParameter( upper_body_yaw, clamp( m_flCurrentTorsoYaw, -60.0f, 60.0f ) );
/*
// FIXME: Adrian, what is this?
int body_yaw = GetBasePlayer()->LookupPoseParameter( "body_yaw" );
if ( body_yaw >= 0 )
{
GetBasePlayer()->SetPoseParameter( body_yaw, 30 );
}
*/
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CSinglePlayerAnimState::ComputePoseParam_HeadPitch( CStudioHdr *pStudioHdr )
{
// Get pitch from v_angle
int iHeadPitch = GetBasePlayer()->LookupPoseParameter("head_pitch");
float flPitch = GetBasePlayer()->EyeAngles()[PITCH];
if ( flPitch > 180.0f )
{
flPitch -= 360.0f;
}
flPitch = clamp( flPitch, -90, 90 );
QAngle absangles = GetBasePlayer()->GetAbsAngles();
absangles.x = 0.0f;
m_angRender = absangles;
GetBasePlayer()->SetPoseParameter( pStudioHdr, iHeadPitch, flPitch );
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : activity -
// Output : Activity
//-----------------------------------------------------------------------------
Activity CSinglePlayerAnimState::BodyYawTranslateActivity( Activity activity )
{
// Not even standing still, sigh
if ( activity != ACT_IDLE )
return activity;
// Not turning
switch ( m_nTurningInPlace )
{
default:
case TURN_NONE:
return activity;
/*
case TURN_RIGHT:
return ACT_TURNRIGHT45;
case TURN_LEFT:
return ACT_TURNLEFT45;
*/
case TURN_RIGHT:
case TURN_LEFT:
return mp_ik.GetBool() ? ACT_TURN : activity;
}
Assert( 0 );
return activity;
}
const QAngle& CSinglePlayerAnimState::GetRenderAngles()
{
return m_angRender;
}
void CSinglePlayerAnimState::GetOuterAbsVelocity( Vector& vel )
{
#if defined( CLIENT_DLL )
GetBasePlayer()->EstimateAbsVelocity( vel );
#else
vel = GetBasePlayer()->GetAbsVelocity();
#endif
}
After completing this section, you will be required to call these functions in order to be active. You shall open up 'hl2_player.h' and 'hl2_player.cpp'
In 'hl2_player.h', add the following code to the top ( in the list of #include definitions. )
#include "singleplayer_animstate.h"
At the bottom, in the 'private' section, add these two lines:
CSinglePlayerAnimState *m_pPlayerAnimState;
QAngle m_angEyeAngles;
After this, open up 'hl2_player.cpp' and direct yourself toward the constructor of the player ( chl2_player::chl2_player() ) around line 391 and this piece of code:
CHL2_Player::CHL2_Player()
{
// Here we create and init the player animation state.
m_pPlayerAnimState = CreatePlayerAnimationState(this);
m_angEyeAngles.Init();
// Code originally written by Valve.
m_nNumMissPositions = 0;
m_pPlayerAISquad = 0;
m_bSprintEnabled = true;
m_flArmorReductionTime = 0.0f;
m_iArmorReductionFrom = 0;
}
As soon as this task is completed, make your way to the 'postthink' function around line 909 and add this part:
m_angEyeAngles = EyeAngles();
QAngle angles = GetLocalAngles();
angles[PITCH] = 0;
SetLocalAngles( angles );
m_pPlayerAnimState->Update();
After this, proceed to the empty destructor ( chl2_player::~chl2_player() ) around line 1411 and add this code that will help releasing the memory of the "Player Animation State" :
CHL2_Player::~CHL2_Player()
{
// Clears the animation state.
if ( m_pPlayerAnimState != NULL )
{
m_pPlayerAnimState->Release();
m_pPlayerAnimState = NULL;
}
}
The next step is really important. You will be required to go to the client solution and open up 'c_baseplayer.cpp'.
Find a function around line 1078 named: void C_BasePlayer::AddEntity( void )
At the end of the function, under the snippet of code:
// Add in lighting effects
CreateLightEffects();
add this code:
SetLocalAnglesDim(X_INDEX, 0 );
This line will prevent the model of the player from rotating when looking up & firing.
The "Player Animation State" system is now set up and all functions will be called correctly according to the player direction. The requirements you will probably need now are a working player model and a thirdperson animation code.
If you already possess such code then, I do believe you should be alright and this tutorial will end this way. Otherwise you are allowed to proceed deeper and follow a few more explanations on how to set up a working 'Third Person' animation system.
Player blending sequences & animation code.
In order to be able to see the "Blending sequences" animation technics, the player model you are bearing will need to have working animations as seen in this tutorial originally found on the Valve Developer community: Developer.valvesoftware.com
If you do not have valid animations for the moment, there are a few animations that are available in Valve's ( Half-Life 2: Deathmatch ). If you do own this game, you will need a few programs: Nem's tools ( GCFScape & VTFEdit ) and CannonFodder's decompiler / Studio compiler.
Once you have them installed, you will be required to decompile a human model of your choice from the ( Source Models ) 'GCF extension'. As soon as you made your point, extract from ( Source Materials ) the head texture according to the model you have chose to decompile. These head materials can be found in around this folder: materials/models/humans/gender/group/head texture.vtf. The head material should be extracted to the ( modification folder ) ex: modification/materials/models/humans/gender/group/.
Once this done, you will be required to extract the 'models/player/male_anims.mdl' from ( Half-Life 2: Deathmatch ) 'GCF extension' and put them in your current modification ) folder ex: modification/models/player/male_anims.mdl.
You shall open up the '.qc' file extension of the decompiled human model and scroll through the lines until you manage to arrive at a few nested $includemodel descriptions such as:
$includemodel "humans/male_shared.mdl"
$includemodel "humans/male_ss.mdl"
$includemodel "humans/male_gestures.mdl"
$includemodel "humans/male_postures.mdl"
Above these lines, add this one:
$includemodel "Player/male_anims.mdl"
Recompile the model and open up the head texture of your new model using Nem's VTFEdit and uncheck the following parameters in the left box: 'Clamp S', 'Clamp T'.
Save the material and the new model should now have all animations necessary to support thirdperson movement with and without weapons.
The time for writing the new thirdperson animation is here. You will need to open up 'hl2_player.h' and 'hl2_player.cpp'
In 'hl2_player.h', add this declaration in the 'public' section:
void SetAnimation( PLAYER_ANIM playerAnim );
Once this done, in 'hl2_player.cpp', add this code:
// Set the activity based on an event or current state
void CHL2_Player::SetAnimation( PLAYER_ANIM playerAnim )
{
int animDesired;
float speed;
speed = GetAbsVelocity().Length2D();
if ( GetFlags() & ( FL_FROZEN | FL_ATCONTROLS ) )
{
speed = 0;
playerAnim = PLAYER_IDLE;
}
Activity idealActivity = ACT_HL2MP_RUN;
if ( playerAnim == PLAYER_JUMP )
{
if ( HasWeapons() )
idealActivity = ACT_HL2MP_JUMP;
else
idealActivity = ACT_JUMP;
}
else if ( playerAnim == PLAYER_DIE )
{
if ( m_lifeState == LIFE_ALIVE )
{
return;
}
}
else if ( playerAnim == PLAYER_ATTACK1 )
{
if ( GetActivity( ) == ACT_HOVER ||
GetActivity( ) == ACT_SWIM ||
GetActivity( ) == ACT_HOP ||
GetActivity( ) == ACT_LEAP ||
GetActivity( ) == ACT_DIESIMPLE )
{
idealActivity = GetActivity( );
}
else
{
idealActivity = ACT_HL2MP_GESTURE_RANGE_ATTACK;
}
}
else if ( playerAnim == PLAYER_RELOAD )
{
idealActivity = ACT_HL2MP_GESTURE_RELOAD;
}
else if ( playerAnim == PLAYER_IDLE || playerAnim == PLAYER_WALK )
{
if ( !( GetFlags() & FL_ONGROUND ) && ( GetActivity( ) == ACT_HL2MP_JUMP || GetActivity( ) == ACT_JUMP ) ) // Still jumping
{
idealActivity = GetActivity( );
}
else if ( GetWaterLevel() > 1 )
{
if ( speed == 0 )
{
if ( HasWeapons() )
idealActivity = ACT_HL2MP_IDLE;
else
idealActivity = ACT_IDLE;
}
else
{
if ( HasWeapons() )
idealActivity = ACT_HL2MP_RUN;
else
idealActivity = ACT_RUN;
}
}
else
{
if ( GetFlags() & FL_DUCKING )
{
if ( speed > 0 )
{
if ( HasWeapons() )
idealActivity = ACT_HL2MP_WALK_CROUCH;
else
idealActivity = ACT_WALK_CROUCH;
}
else
{
if ( HasWeapons() )
idealActivity = ACT_HL2MP_IDLE_CROUCH;
else
idealActivity = ACT_COVER_LOW;
}
}
else
{
if ( speed > 0 )
{
{
if ( HasWeapons() )
idealActivity = ACT_HL2MP_RUN;
else
{
if ( speed > HL2_WALK_SPEED + 20.0f )
idealActivity = ACT_RUN;
else
idealActivity = ACT_WALK;
}
}
}
else
{
if ( HasWeapons() )
idealActivity = ACT_HL2MP_IDLE;
else
idealActivity = ACT_IDLE;
}
}
}
//idealActivity = TranslateTeamActivity( idealActivity );
}
if ( IsInAVehicle() )
{
idealActivity = ACT_COVER_LOW;
}
if ( idealActivity == ACT_HL2MP_GESTURE_RANGE_ATTACK )
{
RestartGesture( Weapon_TranslateActivity( idealActivity ) );
// FIXME: this seems a bit wacked
Weapon_SetActivity( Weapon_TranslateActivity( ACT_RANGE_ATTACK1 ), 0 );
return;
}
else if ( idealActivity == ACT_HL2MP_GESTURE_RELOAD )
{
RestartGesture( Weapon_TranslateActivity( idealActivity ) );
return;
}
else
{
SetActivity( idealActivity );
animDesired = SelectWeightedSequence( Weapon_TranslateActivity ( idealActivity ) );
if (animDesired == -1)
{
animDesired = SelectWeightedSequence( idealActivity );
if ( animDesired == -1 )
{
animDesired = 0;
}
}
// Already using the desired animation?
if ( GetSequence() == animDesired )
return;
m_flPlaybackRate = 1.0;
ResetSequence( animDesired );
SetCycle( 0 );
return;
}
// Already using the desired animation?
if ( GetSequence() == animDesired )
return;
//Msg( "Set animation to %d\n", animDesired );
// Reset to first frame of desired animation
ResetSequence( animDesired );
SetCycle( 0 );
}
In order to be able to attribute a valid player model to the main player other than the default one ("player.mdl"), you will need to precache it and assign it.
At the top of 'hl2_player.cpp' near the end of the definition macros around line 116, add this code that will define the main model of the player:
#define PLAYER_MODEL "models/humans/group01/male_09.mdl"
The model between the quotes can be replaced with the character of your choice.
As soon as you are over typing this, the main model will need to be precached. Go on and find a function around line 443 called: 'void CHL2_Player::Precache( void )'
At the end of this function, add this precache code:
PrecacheModel(PLAYER_MODEL);
Finally, you will need to assign the desired player model in the function around line 1134 named 'void CHL2_Player::Spawn(void)'
Find the following line:
SetModel( "models/player.mdl" );
Replace this one with:
SetModel( PLAYER_MODEL );
The player should be able to play all correct animations when no weapons are actually held but if the player is bound on carrying weapons, since the activity list of all weapons is not completed, the player will probably enter in a 'Ragdoll' animation state. In order to correct this mistake, you shall open up all weapon files and add or complete the main activity list.
Weapon activity list tables.
In weapon_357.cpp:
In the class description, under the macro "DECLARE_DATADESC()", add this line:
DECLARE_ACTTABLE();
Under the class definition around line 50, add this activity table list:
acttable_t CWeapon357::m_acttable[] =
{
{ ACT_HL2MP_IDLE, ACT_HL2MP_IDLE_PISTOL, false },
{ ACT_HL2MP_RUN, ACT_HL2MP_RUN_PISTOL, false },
{ ACT_HL2MP_IDLE_CROUCH, ACT_HL2MP_IDLE_CROUCH_PISTOL, false },
{ ACT_HL2MP_WALK_CROUCH, ACT_HL2MP_WALK_CROUCH_PISTOL, false },
{ ACT_HL2MP_GESTURE_RANGE_ATTACK, ACT_HL2MP_GESTURE_RANGE_ATTACK_PISTOL, false },
{ ACT_HL2MP_GESTURE_RELOAD, ACT_HL2MP_GESTURE_RELOAD_PISTOL, false },
{ ACT_HL2MP_JUMP, ACT_HL2MP_JUMP_PISTOL, false },
{ ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_PISTOL, false },
};
IMPLEMENT_ACTTABLE( CWeapon357 );
In weapon_ar2.cpp:
There is already an activity list. At it's foot, after // { ACT_RANGE_ATTACK2, ACT_RANGE_ATTACK_AR2_GRENADE, true },
add the following list:
{ ACT_HL2MP_IDLE, ACT_HL2MP_IDLE_AR2, false },
{ ACT_HL2MP_RUN, ACT_HL2MP_RUN_AR2, false },
{ ACT_HL2MP_IDLE_CROUCH, ACT_HL2MP_IDLE_CROUCH_AR2, false },
{ ACT_HL2MP_WALK_CROUCH, ACT_HL2MP_WALK_CROUCH_AR2, false },
{ ACT_HL2MP_GESTURE_RANGE_ATTACK, ACT_HL2MP_GESTURE_RANGE_ATTACK_AR2, false },
{ ACT_HL2MP_GESTURE_RELOAD, ACT_GESTURE_RELOAD_SMG1, false },
{ ACT_HL2MP_JUMP, ACT_HL2MP_JUMP_AR2, false },
{ ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_AR2, false },
In weapon_crossbow.cpp:
In the class description, around line 450, under the macro "DECLARE_DATADESC()", add this line:
DECLARE_ACTTABLE();
Under the class definition around line 489, add this activity table list:
acttable_t CWeaponCrossbow::m_acttable[] =
{
{ ACT_HL2MP_IDLE, ACT_HL2MP_IDLE_CROSSBOW, false },
{ ACT_HL2MP_RUN, ACT_HL2MP_RUN_CROSSBOW, false },
{ ACT_HL2MP_IDLE_CROUCH, ACT_HL2MP_IDLE_CROUCH_CROSSBOW, false },
{ ACT_HL2MP_WALK_CROUCH, ACT_HL2MP_WALK_CROUCH_CROSSBOW, false },
{ ACT_HL2MP_GESTURE_RANGE_ATTACK, ACT_HL2MP_GESTURE_RANGE_ATTACK_CROSSBOW, false },
{ ACT_HL2MP_GESTURE_RELOAD, ACT_HL2MP_GESTURE_RELOAD_CROSSBOW, false },
{ ACT_HL2MP_JUMP, ACT_HL2MP_JUMP_CROSSBOW, false },
{ ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_SHOTGUN, false },
};
IMPLEMENT_ACTTABLE( CWeaponCrossbow );
In weapon_crowbar.cpp:
There is already an activity list. At it's foot, after { ACT_IDLE_ANGRY, ACT_IDLE_ANGRY_MELEE, false },
add the following list:
{ ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_SLAM, true },
{ ACT_HL2MP_IDLE, ACT_HL2MP_IDLE_MELEE, false },
{ ACT_HL2MP_RUN, ACT_HL2MP_RUN_MELEE, false },
{ ACT_HL2MP_IDLE_CROUCH, ACT_HL2MP_IDLE_CROUCH_MELEE, false },
{ ACT_HL2MP_WALK_CROUCH, ACT_HL2MP_WALK_CROUCH_MELEE, false },
{ ACT_HL2MP_GESTURE_RANGE_ATTACK, ACT_HL2MP_GESTURE_RANGE_ATTACK_MELEE, false },
{ ACT_HL2MP_GESTURE_RELOAD, ACT_HL2MP_GESTURE_RELOAD_MELEE, false },
{ ACT_HL2MP_JUMP, ACT_HL2MP_JUMP_MELEE, false },
In weapon_frag.cpp:
There is already an activity list. At it's foot, after { ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_SLAM, true },
add the following list:
{ ACT_HL2MP_IDLE, ACT_HL2MP_IDLE_GRENADE, false },
{ ACT_HL2MP_RUN, ACT_HL2MP_RUN_GRENADE, false },
{ ACT_HL2MP_IDLE_CROUCH, ACT_HL2MP_IDLE_CROUCH_GRENADE, false },
{ ACT_HL2MP_WALK_CROUCH, ACT_HL2MP_WALK_CROUCH_GRENADE, false },
{ ACT_HL2MP_GESTURE_RANGE_ATTACK, ACT_HL2MP_GESTURE_RANGE_ATTACK_GRENADE, false },
{ ACT_HL2MP_GESTURE_RELOAD, ACT_HL2MP_GESTURE_RELOAD_GRENADE, false },
{ ACT_HL2MP_JUMP, ACT_HL2MP_JUMP_GRENADE, false },
In weapon_physcannon.cpp:
In the class description, around line 1203, under the macro "DECLARE_DATADESC()", add this line:
DECLARE_ACTTABLE();
Under the class definition around line 1374, add this activity table list:
acttable_t CWeaponPhysCannon::m_acttable[] =
{
{ ACT_HL2MP_IDLE, ACT_HL2MP_IDLE_PHYSGUN, false },
{ ACT_HL2MP_RUN, ACT_HL2MP_RUN_PHYSGUN, false },
{ ACT_HL2MP_IDLE_CROUCH, ACT_HL2MP_IDLE_CROUCH_PHYSGUN, false },
{ ACT_HL2MP_WALK_CROUCH, ACT_HL2MP_WALK_CROUCH_PHYSGUN, false },
{ ACT_HL2MP_GESTURE_RANGE_ATTACK, ACT_HL2MP_GESTURE_RANGE_ATTACK_PHYSGUN, false },
{ ACT_HL2MP_GESTURE_RELOAD, ACT_HL2MP_GESTURE_RELOAD_PHYSGUN, false },
{ ACT_HL2MP_JUMP, ACT_HL2MP_JUMP_PHYSGUN, false },
{ ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_SLAM, false },
};
IMPLEMENT_ACTTABLE( CWeaponPhysCannon );
In weapon_pistol.cpp:
There is already an activity list. At it's foot, after { ACT_RUN, ACT_RUN_PISTOL, false },
add the following list:
{ ACT_HL2MP_IDLE, ACT_HL2MP_IDLE_PISTOL, false },
{ ACT_HL2MP_RUN, ACT_HL2MP_RUN_PISTOL, false },
{ ACT_HL2MP_IDLE_CROUCH, ACT_HL2MP_IDLE_CROUCH_PISTOL, false },
{ ACT_HL2MP_WALK_CROUCH, ACT_HL2MP_WALK_CROUCH_PISTOL, false },
{ ACT_HL2MP_GESTURE_RANGE_ATTACK, ACT_HL2MP_GESTURE_RANGE_ATTACK_PISTOL, false },
{ ACT_HL2MP_GESTURE_RELOAD, ACT_HL2MP_GESTURE_RELOAD_PISTOL, false },
{ ACT_HL2MP_JUMP, ACT_HL2MP_JUMP_PISTOL, false },
{ ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_PISTOL, false },
In weapon_rpg.cpp:
There is already an activity list. At it's foot, after { ACT_COVER_LOW, ACT_COVER_LOW_RPG, true },
add the following list:
{ ACT_HL2MP_IDLE, ACT_HL2MP_IDLE_RPG, false },
{ ACT_HL2MP_RUN, ACT_HL2MP_RUN_RPG, false },
{ ACT_HL2MP_IDLE_CROUCH, ACT_HL2MP_IDLE_CROUCH_RPG, false },
{ ACT_HL2MP_WALK_CROUCH, ACT_HL2MP_WALK_CROUCH_RPG, false },
{ ACT_HL2MP_GESTURE_RANGE_ATTACK, ACT_HL2MP_GESTURE_RANGE_ATTACK_RPG, false },
{ ACT_HL2MP_GESTURE_RELOAD, ACT_HL2MP_GESTURE_RELOAD_RPG, false },
{ ACT_HL2MP_JUMP, ACT_HL2MP_JUMP_RPG, false },
{ ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_RPG, false },
In weapon_shotgun.cpp:
There is already an activity list. At it's foot, after { ACT_GESTURE_RELOAD, ACT_GESTURE_RELOAD_SHOTGUN, false },
add the following list:
{ ACT_HL2MP_IDLE, ACT_HL2MP_IDLE_SHOTGUN, false },
{ ACT_HL2MP_RUN, ACT_HL2MP_RUN_SHOTGUN, false },
{ ACT_HL2MP_IDLE_CROUCH, ACT_HL2MP_IDLE_CROUCH_SHOTGUN, false },
{ ACT_HL2MP_WALK_CROUCH, ACT_HL2MP_WALK_CROUCH_SHOTGUN, false },
{ ACT_HL2MP_GESTURE_RANGE_ATTACK, ACT_HL2MP_GESTURE_RANGE_ATTACK_SHOTGUN, false },
{ ACT_HL2MP_GESTURE_RELOAD, ACT_HL2MP_GESTURE_RELOAD_SHOTGUN, false },
{ ACT_HL2MP_JUMP, ACT_HL2MP_JUMP_SHOTGUN, false },
{ ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_SHOTGUN, false },
In weapon_smg1.cpp:
There is already an activity list. At it's foot, after { ACT_GESTURE_RELOAD, ACT_GESTURE_RELOAD_SMG1, true },
add the following list:
{ ACT_HL2MP_IDLE, ACT_HL2MP_IDLE_SMG1, false },
{ ACT_HL2MP_RUN, ACT_HL2MP_RUN_SMG1, false },
{ ACT_HL2MP_IDLE_CROUCH, ACT_HL2MP_IDLE_CROUCH_SMG1, false },
{ ACT_HL2MP_WALK_CROUCH, ACT_HL2MP_WALK_CROUCH_SMG1, false },
{ ACT_HL2MP_GESTURE_RANGE_ATTACK, ACT_HL2MP_GESTURE_RANGE_ATTACK_SMG1, false },
{ ACT_HL2MP_GESTURE_RELOAD, ACT_GESTURE_RELOAD_SMG1, false },
{ ACT_HL2MP_JUMP, ACT_HL2MP_JUMP_SMG1, false },
{ ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_SMG1, false },
Weapon attack sequence call fix.
The weapon animation lists should be completed for the moment but quite a few issues remain since because the player 'animation sequence' call is missing in several parts of the weapon code. Even if the ( Half-Life 2 ) crowbar now has valid animations, the player will not play the attack sequence since this sequence is not called. To fix it, direct yourself to 'basebludgeonweapon.cpp' in the Server solution and find the last function named: 'void CBaseHLBludgeonWeapon::Swing( int bIsSecondary )'.
At the end of this function, under
//Play swing sound
WeaponSound( SINGLE );
add these assets of code:
// Send the player 'attack' animation.
pOwner->SetAnimation(PLAYER_ATTACK1);
There are a few other weapons that have deficience toward the animation state. Open up 'weapon_frag.cpp' and find a function around line 409 named: 'void CWeaponFrag::ThrowGrenade( CBasePlayer *pPlayer )'
After the snippet of code:
WeaponSound( SINGLE );
add these assets of code:
// Send the player 'attack' animation.
pPlayer->SetAnimation(PLAYER_ATTACK1);
In 'weapon_frag.cpp' there are other functions named: 'void CWeaponFrag::LobGrenade( CBasePlayer *pPlayer )' & 'void CWeaponFrag::RollGrenade( CBasePlayer *pPlayer )'
In these, under the snippet of code:
WeaponSound( Parameter );
redo the same for them by adding:
// Send the player 'attack' animation.
pPlayer->SetAnimation(PLAYER_ATTACK1);
The last file to explore is 'weapon_shotgun.cpp'. Although this is not really a disturbing issue but it might be useful in the case where you would not desire to have the ( Half-Life 2 )Shotgun fire rate too fast.
in weapon_shotgun.cpp:
Proceed to the function around line 457 named: 'void CWeaponShotgun::PrimaryAttack( void )'
find the following line:
// Don't fire again until fire animation has completed
m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration();
replace this one with:
// Don't fire again until fire animation has completed
m_flNextPrimaryAttack = gpGlobals->curtime + GetViewModelSequenceDuration();
Proceed to the function around line 514 named: 'void CWeaponShotgun::SecondaryAttack( void )'
find the following line:
// Don't fire again until fire animation has completed
m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration();
replace this one with:
// Don't fire again until fire animation has completed
m_flNextPrimaryAttack = gpGlobals->curtime + GetViewModelSequenceDuration();
The main fire rate of the ( Half-Life 2 ) shotgun should be fixed.
Usually, this tutorial would end here but there is another addition you could see if you would like to have a look at it below. It consists of creating a "Third Person" death view when the player is in third person and is no longer alive.
Third Person death view camera.
In c_baseplayer.h:
In the class 'C_Baseplayer', find a few cameras calculation functions around line 408 and under these, add the following declaration:
virtual void CalcThirdPersonDeathView( Vector& eyeOrigin, QAngle& eyeAngles, float& fov );
Once this task is completed, you shall be allowed to open up the file 'c_baseplayer.cpp' and add this code:
void C_BasePlayer::CalcThirdPersonDeathView(Vector& eyeOrigin, QAngle& eyeAngles, float& fov)
{
if ( m_lifeState != LIFE_ALIVE )
{
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
if ( !pPlayer )
return;
// Keep a copy of the actual player model.
pPlayer->SnatchModelInstance(this);
// Creates the ragdoll asset from the player's model.
m_nRenderFX = kRenderFxRagdoll;
matrix3x4_t boneDelta0[MAXSTUDIOBONES];
matrix3x4_t boneDelta1[MAXSTUDIOBONES];
matrix3x4_t currentBones[MAXSTUDIOBONES];
const float boneDt = 0.05f;
if ( pPlayer && !pPlayer->IsDormant() )
{
pPlayer->GetRagdollInitBoneArrays( boneDelta0, boneDelta1, currentBones, boneDt );
}
else
{
GetRagdollInitBoneArrays( boneDelta0, boneDelta1, currentBones, boneDt );
}
// Sets the ragdoll.
InitAsClientRagdoll( boneDelta0, boneDelta1, currentBones, boneDt );
// Calculate origin of the ragdoll.
Vector origin = EyePosition();
IRagdoll *pRagdoll = GetRepresentativeRagdoll();
if ( pRagdoll )
{
origin = pRagdoll->GetRagdollOrigin();
origin.z += VEC_DEAD_VIEWHEIGHT.z; // look over ragdoll, not through
}
eyeOrigin = origin;
Vector vForward;
AngleVectors( eyeAngles, &vForward );
VectorNormalize( vForward );
VectorMA( origin, -CHASE_CAM_DISTANCE, vForward, eyeOrigin );
Vector WALL_MIN( -WALL_OFFSET, -WALL_OFFSET, -WALL_OFFSET );
Vector WALL_MAX( WALL_OFFSET, WALL_OFFSET, WALL_OFFSET );
trace_t trace; // clip against world
C_BaseEntity::PushEnableAbsRecomputations( false ); // HACK don't recompute positions while doing RayTrace
UTIL_TraceHull( origin, eyeOrigin, WALL_MIN, WALL_MAX, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &trace );
C_BaseEntity::PopEnableAbsRecomputations();
if (trace.fraction < 1.0)
{
eyeOrigin = trace.endpos;
}
fov = GetFOV();
return;
}
}
After this, you will be required to have this function called and evaluated as soon as the player will be no longer be alive.
In 'baseplayer_shared.cpp' you will need to add
#ifdef CLIENT_DLL
#include "input.h"
#endif
at the top in the nest of "#include" definitions.
Find a function around line 1443 named: void CBasePlayer::CalcView( Vector &eyeOrigin, QAngle &eyeAngles, float &zNear, float &zFar, float &fov )
find the main control block:
if ( IsObserver() )
{
CalcObserverView( eyeOrigin, eyeAngles, fov );
}
Under this statement block, put this snippet:
#ifdef CLIENT_DLL
else if ( !this->IsAlive() && ::input->CAM_IsThirdPerson() )
{
CalcThirdPersonDeathView( eyeOrigin, eyeAngles, fov );
}
#endif
When the player's life will be equal to 0, this function will be called and create a copy of the player's main model as a ragdoll. Then, the ragdoll's position will be evaluated and the view offset will be following this ragdoll.
There are still a few troubles with the thirdperson attachments on weapons that I am still trying to solve and this will probably be fixed in the mean time.
I do sincerely hope that this will help people who are willing to implement valid "Third Person" systems in new ( modifications ) base on Valve's award winning Source Engine.
Source 2013 Fixes
When the main code (before anything is adjusted to the weapon tables, third person death camera, etc) is compiled with no changes in the 2013 build of Source, error C3861 will usually occur in the compiling process because of a mishandled definition link inside singleplayer_animstate.cpp for "MDLCACHE_CRITICAL_SECTION()." To fix this, add this "include" line in singleplayer_animstate.h:
#include "..\public\datacache\imdlcache.h"
In addition, the engine will crash upon map load if PLAYER_MODEL is not properly precached. Find the CHL2_Player::Precache function inside hl2_player.cpp and add this code at the bottom underneath the other precaches:
PrecacheModel(PLAYER_MODEL); //needs to be precached or else engine will crash!
This concludes the fixes needed to get the code working in Source 2013.