First Person Ragdolls In Singleplayer
In this article, we will go through implementing a first-person perspective ragdoll death system for singleplayer branch mods. One of the main problems (and the reason why we cannot directly copy the code from the original article) is that the singleplayer branch doesn't have a system for player ragdolls.
Therefore what we will do is create such a system and then send the ragdoll information to the client-side (which is where we will change the camera).
Contents
Requirements
For the code to work, it is necessary that the player model has an attachment called eyes
that represents, well, the eyes where the player's camera will be attached. And because the default HL2 player model doesn't have this attachment, it will be necessary to fix the player animation state to be able to use player models such as the ones in HL2DM.
Once you have implemented the solution to the animation state you can continue without problems.
Before starting
You may find some steps a little "bundled", this is because it is about explaining what each piece of code is for and in the highest possible order so that everything works perfectly.
It is also likely that the code being handled is not entirely perfect, so I ask anyone with knowledge of this to try to improve it.
The following code is intended for singleplayer branch mods. If your mod is based on the multiplayer branch you can follow this article.
[Server-side]
Ragdoll system
To begin we will create a class that is in charge of the ragdoll system. We will open the file server/hl2/hl2_player.h and just below the CHL2Player
class definition we will add the following code:
>
class CHL2Ragdoll : public CBaseAnimatingOverlay
{
public:
DECLARE_CLASS( CHL2Ragdoll, CBaseAnimatingOverlay );
DECLARE_SERVERCLASS();
// Transmit ragdolls to everyone.
virtual int UpdateTransmitState()
{
return SetTransmitState( FL_EDICT_ALWAYS );
}
public:
// In case the client has the player entity, we transmit the player index.
// In case the client doesn't have it, we transmit the player's model index, origin, and angles
// so they can create a ragdoll in the right place.
CNetworkHandle( CBaseEntity, m_hPlayer ); // Networked entity handle
CNetworkVector( m_vecRagdollVelocity );
CNetworkVector( m_vecRagdollOrigin );
};
Now we must link the class to an entity, for this we will now open the server/hl2/hl2_player.cpp file and locate the END_DATADESC()
code around line 382, just below that line but above the CHL2_Player()
function we will add the following:
>
// -------------------------------------------------------------------------------- //
// Ragdoll entities.
// -------------------------------------------------------------------------------- //
LINK_ENTITY_TO_CLASS( hl2_ragdoll, CHL2Ragdoll );
IMPLEMENT_SERVERCLASS_ST_NOBASE( CHL2Ragdoll, DT_HL2Ragdoll )
SendPropVector( SENDINFO( m_vecRagdollOrigin ), -1, SPROP_COORD ),
SendPropEHandle( SENDINFO( m_hPlayer ) ),
SendPropModelIndex( SENDINFO( m_nModelIndex ) ),
SendPropInt ( SENDINFO( m_nForceBone ), 8, 0 ),
SendPropVector ( SENDINFO( m_vecForce ), -1, SPROP_NOSCALE ),
SendPropVector( SENDINFO( m_vecRagdollVelocity ) )
END_SEND_TABLE()
Creating the ragdoll
Since we have the hl2_ragdoll
entity, we must now tell Source that when the player dies, that entity must be created and act as their ragdoll.
For this we will open the server/player.cpp file and locate the Event_Dying()
function, now we look for the line that says:
SetLocalAngles( angles );
And just below we will add:
CreateRagdollEntity(); BecomeRagdollOnClient( vec3_origin );
CreateRagdollEntity()
as its name implies is responsible for creating the hl2_ragdoll
entity, however and although this function is defined within the source code it does absolutely nothing so we will have to make it work.
We go back to the server/hl2/hl2_player.h file and locate the definition of the OnTakeDamage_Alive( const CTakeDamageInfo &info )
function, just below we add the definition:
virtual void CreateRagdollEntity();
Now in server/hl2/hl2_player.cpp just below the OnTakeDamage_Alive( ... )
function (to maintain order) we add:
//========================================================= // Create a ragdoll //========================================================= void CHL2_Player::CreateRagdollEntity() { // There is already a ragdoll. if ( m_hRagdoll ) { // Remove it. UTIL_RemoveImmediate( m_hRagdoll ); m_hRagdoll = NULL; } // We get the corpse. CHL2Ragdoll *pRagdoll = dynamic_cast<CHL2Ragdoll *>( m_hRagdoll.Get() ); // Apparently there is none, create it. if ( !pRagdoll ) pRagdoll = dynamic_cast<CHL2Ragdoll *>( CreateEntityByName("hl2_ragdoll") ); if ( pRagdoll ) { pRagdoll->m_hPlayer = this; pRagdoll->m_vecRagdollOrigin = GetAbsOrigin(); pRagdoll->m_vecRagdollVelocity = GetAbsVelocity(); pRagdoll->m_nModelIndex = m_nModelIndex; pRagdoll->m_nForceBone = m_nForceBone; pRagdoll->SetAbsOrigin( GetAbsOrigin() ); } m_hRagdoll = pRagdoll; }
At this moment it is likely that errors will appear indicating that the variable m_hRagdoll
does not exist, it is normal and we will define it below...
Sending the ragdoll
Now it's time to send the ragdoll to the client, but first of all, it is necessary to define the missing variable.
Go back to server/hl2/hl2_player.h and in the public:
section of the CHL2_Player
class we declare the variable:
// Tracks our ragdoll entity. CNetworkHandle( CBaseEntity, m_hRagdoll ); // Networked entity handle
We go back to server/hl2/hl2_player.cpp (yes, again) and locate the line:
DEFINE_FIELD( m_flTimeNextLadderHint, FIELD_TIME ),
Just below we add:
DEFINE_FIELD( m_hRagdoll, FIELD_EHANDLE ),
Now a few lines below (around 434) you can find the following:
SendPropBool( SENDINFO(m_fIsSprinting) ),
Just below we add:
SendPropEHandle( SENDINFO(m_hRagdoll) ),
And that's it! (or at least on the server-side). Our ragdoll system is already created and ready to be shipped to the client-side.
[Client-side]
Ragdoll system
Now it's time for the client-side, we must create a ragdoll system (a class) that gives us access to it.
To do this, open the client/hl2/c_basehlplayer.h file and just below the C_BaseHLPlayer
class definition approximately line 83 we will add the following code:
class C_BaseHLRagdoll : public C_BaseAnimatingOverlay { public: DECLARE_CLASS( C_BaseHLRagdoll, C_BaseAnimatingOverlay ); DECLARE_CLIENTCLASS(); C_BaseHLRagdoll(); ~C_BaseHLRagdoll(); virtual void OnDataChanged( DataUpdateType_t type ); int GetPlayerEntIndex() const; IRagdoll *GetIRagdoll() const; void ImpactTrace( trace_t *pTrace, int iDamageType, char *pCustomImpactName ); void UpdateOnRemove(); virtual void SetupWeights( const matrix3x4_t *pBoneToWorld, int nFlexWeightCount, float *pFlexWeights, float *pFlexDelayedWeights ); private: C_BaseHLRagdoll( const C_BaseHLRagdoll & ) {} void Interp_Copy( C_BaseAnimatingOverlay *pDestinationEntity ); void CreateHL2Ragdoll(); private: EHANDLE m_hPlayer; CNetworkVector( m_vecRagdollVelocity ); CNetworkVector( m_vecRagdollOrigin ); };
In this case, we will not link it with an entity but if it will be necessary to implement it, for this we will open the client/hl2/c_basehlplayer.cpp file and at the end of it we will add:
//C_BaseHLRagdoll IMPLEMENT_CLIENTCLASS_DT_NOBASE( C_BaseHLRagdoll, DT_HL2Ragdoll, CHL2Ragdoll ) RecvPropVector( RECVINFO( m_vecRagdollOrigin ) ), RecvPropEHandle( RECVINFO( m_hPlayer ) ), RecvPropInt( RECVINFO( m_nModelIndex ) ), RecvPropInt( RECVINFO( m_nForceBone ) ), RecvPropVector( RECVINFO( m_vecForce ) ), RecvPropVector( RECVINFO( m_vecRagdollVelocity ) ) END_RECV_TABLE() C_BaseHLRagdoll::C_BaseHLRagdoll() { } C_BaseHLRagdoll::~C_BaseHLRagdoll() { PhysCleanupFrictionSounds( this ); if ( m_hPlayer ) { m_hPlayer->CreateModelInstance(); } } void C_BaseHLRagdoll::Interp_Copy( C_BaseAnimatingOverlay *pSourceEntity ) { if ( !pSourceEntity ) return; VarMapping_t *pSrc = pSourceEntity->GetVarMapping(); VarMapping_t *pDest = GetVarMapping(); // Find all the VarMapEntry_t's that represent the same variable. for ( int i = 0; i < pDest->m_Entries.Count(); i++ ) { VarMapEntry_t *pDestEntry = &pDest->m_Entries[i]; const char *pszName = pDestEntry->watcher->GetDebugName(); for ( int j = 0; j < pSrc->m_Entries.Count(); j++ ) { VarMapEntry_t *pSrcEntry = &pSrc->m_Entries[j]; if ( !Q_strcmp( pSrcEntry->watcher->GetDebugName(), pszName ) ) { pDestEntry->watcher->Copy( pSrcEntry->watcher ); break; } } } } void C_BaseHLRagdoll::ImpactTrace( trace_t *pTrace, int iDamageType, char *pCustomImpactName ) { IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); if( !pPhysicsObject ) return; Vector dir = pTrace->endpos - pTrace->startpos; if ( iDamageType == DMG_BLAST ) { dir *= 4000; // Adjust impact strength // Apply force at object mass center pPhysicsObject->ApplyForceCenter( dir ); } else { Vector hitpos; VectorMA( pTrace->startpos, pTrace->fraction, dir, hitpos ); VectorNormalize( dir ); dir *= 4000; // Adjust impact strength // Apply force where we hit it pPhysicsObject->ApplyForceOffset( dir, hitpos ); // Blood spray! //FX_CS_BloodSpray( hitpos, dir, 10 ); } m_pRagdoll->ResetRagdollSleepAfterTime(); } void C_BaseHLRagdoll::CreateHL2Ragdoll( void ) { // First, initialize all our data. If we have the player's entity on our client, // then we can make ourselves start exactly where the player is. C_BasePlayer *pPlayer = dynamic_cast<C_BasePlayer *>( m_hPlayer.Get() ); if ( pPlayer && !pPlayer->IsDormant() ) { // Move my current model instance to the ragdoll's so decals are preserved. pPlayer->SnatchModelInstance( this ); VarMapping_t *varMap = GetVarMapping(); // Copy all the interpolated vars from the player entity. // The entity uses the interpolated history to get bone velocity. bool bRemotePlayer = (pPlayer != C_BasePlayer::GetLocalPlayer()); if ( bRemotePlayer ) { Interp_Copy( pPlayer ); SetAbsAngles( pPlayer->GetRenderAngles() ); GetRotationInterpolator().Reset(); m_flAnimTime = pPlayer->m_flAnimTime; SetSequence( pPlayer->GetSequence() ); m_flPlaybackRate = pPlayer->GetPlaybackRate(); } else { // This is the local player, so set them in a default // pose and slam their velocity, angles and origin SetAbsOrigin( m_vecRagdollOrigin ); SetAbsAngles( pPlayer->GetRenderAngles() ); SetAbsVelocity( m_vecRagdollVelocity ); int iSeq = pPlayer->GetSequence(); if ( iSeq == -1 ) { Assert( false ); // missing walk_lower? iSeq = 0; } SetSequence( iSeq ); // walk_lower, basic pose SetCycle( 0.0 ); Interp_Reset( varMap ); } } else { // Overwrite network origin so later interpolation will // use this position SetNetworkOrigin( m_vecRagdollOrigin ); SetAbsOrigin( m_vecRagdollOrigin ); SetAbsVelocity( m_vecRagdollVelocity ); Interp_Reset( GetVarMapping() ); } SetModelIndex( m_nModelIndex ); // Make us a ragdoll... 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 ); } InitAsClientRagdoll( boneDelta0, boneDelta1, currentBones, boneDt ); } void C_BaseHLRagdoll::OnDataChanged( DataUpdateType_t type ) { BaseClass::OnDataChanged( type ); if ( type == DATA_UPDATE_CREATED ) { CreateHL2Ragdoll(); } } IRagdoll *C_BaseHLRagdoll::GetIRagdoll() const { return m_pRagdoll; } void C_BaseHLRagdoll::UpdateOnRemove( void ) { VPhysicsSetObject( NULL ); BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Purpose: Clear out any face/eye values stored in the material system //----------------------------------------------------------------------------- void C_BaseHLRagdoll::SetupWeights( const matrix3x4_t *pBoneToWorld, int nFlexWeightCount, float *pFlexWeights, float *pFlexDelayedWeights ) { BaseClass::SetupWeights( pBoneToWorld, nFlexWeightCount, pFlexWeights, pFlexDelayedWeights ); static float destweight[128]; static bool bIsInited = false; CStudioHdr *hdr = GetModelPtr(); if ( !hdr ) return; int nFlexDescCount = hdr->numflexdesc(); if ( nFlexDescCount ) { Assert( !pFlexDelayedWeights ); memset( pFlexWeights, 0, nFlexWeightCount * sizeof(float) ); } if ( m_iEyeAttachment > 0 ) { matrix3x4_t attToWorld; if (GetAttachment( m_iEyeAttachment, attToWorld )) { Vector local, tmp; local.Init( 1000.0f, 0.0f, 0.0f ); VectorTransform( local, attToWorld, tmp ); modelrender->SetViewTarget( GetModelPtr(), GetBody(), tmp ); } } }
Receiving the ragdoll
Then we must go back to client/hl2/c_basehlplayer.h and inside the C_BaseHLPlayer
class in the private:
section (around line 77) we must define the variable m_hRagdoll
:
EHANDLE m_hRagdoll;
Now we just need to receive it, so return to client/hl2/c_basehlplayer.cpp and locate the line:
RecvPropBool( RECVINFO( m_fIsSprinting ) ),
And just below we add:
RecvPropEHandle( RECVINFO( m_hRagdoll ) ),
The camera
Perfect, now m_hRagdoll
already contains the information of our corpse at the time of death, however we face another small problem... camera-related functions are located in the client/c_baseplayer.cpp file and will not have access to the m_hRagdoll
variable so we will have to define the functions we need inside c_basehlplayer.
And well, for our luck the only function that we will need is called CalcDeathCamView
so inside client/hl2/c_basehlplayer.h we will locate the definition:
bool IsWeaponLowered( void ) { return m_HL2Local.m_bWeaponLowered; }
And just below we will add:
virtual void CalcDeathCamView( Vector& eyeOrigin, QAngle& eyeAngles, float& fov );
Now in client/hl2/c_basehlplayer.cpp we will add (anywhere) the function:
void C_BaseHLPlayer::CalcDeathCamView(Vector &eyeOrigin, QAngle &eyeAngles, float &fov) { // The player's ragdoll has been created. if ( m_hRagdoll.Get() ) { // We get the location of the model's eyes. C_BaseHLRagdoll *pRagdoll = dynamic_cast<C_BaseHLRagdoll *>( m_hRagdoll.Get() ); pRagdoll->GetAttachment( pRagdoll->LookupAttachment("eyes"), eyeOrigin, eyeAngles ); // We adjust the camera in the eyes of the model. Vector vForward; AngleVectors(eyeAngles, &vForward); trace_t tr; UTIL_TraceLine( eyeOrigin, eyeOrigin + (vForward * 10000), MASK_ALL, pRagdoll, COLLISION_GROUP_NONE, &tr ); if ( ( !(tr.fraction < 1) || (tr.endpos.DistTo(eyeOrigin) > 25) ) ) return; } }
Operating the camera
At last! We will open the shared/baseplayer_shared.cpp file and locate the CBasePlayer::CalcView
function. If you have implemented third-person death view camera you will probably find something like this inside:
#ifdef CLIENT_DLL else if ( !this->IsAlive() && ::input->CAM_IsThirdPerson() ) { CalcThirdPersonDeathView( eyeOrigin, eyeAngles, fov ); } #endif
Under the else if
just add:
else if ( !this->IsAlive() ) { CalcDeathCamView( eyeOrigin, eyeAngles, fov ); }
So that this remains:
#ifdef CLIENT_DLL else if ( !this->IsAlive() && ::input->CAM_IsThirdPerson() ) { CalcThirdPersonDeathView( eyeOrigin, eyeAngles, fov ); } else if ( !this->IsAlive() ) { CalcDeathCamView(eyeOrigin, eyeAngles, fov); } #endif
And that's it! Once you compile the code you have first-person ragdoll deaths.
All the code provided here is the property of Valve.
Minor problems
If you are in third-person you will see that when dying 2 ragdolls is generated, this is caused by the CreateRagdollEntity()
function since third-person already has its own ragdoll, a possible solution would be to not execute this function when we're in third-person.
See also
Demo video First Person Ragdolls in Multiplayer