First Person Ragdolls In Singleplayer

From Valve Developer Community
< Es
Jump to navigation Jump to search
English (en)Español (es)Translate (Translate)
Muerte desde una perspectiva en primera persona en HL2DM

En este articulo veremos como implementar la muerte en primera persona dentro de un MOD en Singleplayer (HL2). Uno de los principales problemas (y la razón de porque no podemos copiar directamente el código del articulo original(en)) es que la versión Singleplayer de Source Engine no cuenta con un sistema de cadáveres (ragdolls).

Por lo tanto lo que haremos es crear tal sistema y después enviar la información del cadáver al lado del cliente (que es donde cambiaremos la cámara)

Requisitos

Para que el código funcione es necesario que el modelo del cadáver tenga un acoplamiento llamado eyes que representa los ojos del mismo y desde donde el jugador podrá ver. Debido a que el modelo predeterminado de HL2 no cuenta con este acoplamiento será necesario solucionar el estado de la animación en Singleplayer(en) para poder usar modelos como los de HL2DM (Half-Life 2: Deathmatch)

Una vez que hayas implementado la solución al estado de la animación podrás continuar sin problemas.

Antes de empezar

Quizá encuentres algunos pasos un poco "liados", esto es debido a que se trata de explicar para que sirve cada código y con el mayor orden posible para que todo funcione a la perfección.

Igualmente es probable que el código que se maneje no sea del todo perfecto, por lo que pido a cualquiera con conocimientos en esto que trate de mejorarlo.

El siguiente código esta destinado para los MODS de HL2 (Half-Life 2). Si tu MOD esta basado en HL2DM (Multiplayer) puedes ver el código para esta versión aquí(en).

[Server-Side] Sistema de cadáveres

Para empezar deberemos crear el sistema de cadáveres, una clase que se encargue de ello. Abriremos el archivo server/hl2/hl2_player.h y justo al terminar la definición de la clase CHL2Player Línea 391 Aprox agregaremos el siguiente código:

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 );
};

Ahora debemos vincular la clase a una entidad, para ello abriremos ahora el archivo hl2/server/hl2_player.cpp y ubicaremos el código END_DATADESC() que se encuentra más o menos en la línea 382, justo debajo de esa línea pero arriba de la función CHL2_Player() agregaremos lo siguiente:

// -------------------------------------------------------------------------------- //
// 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()

[Server-Side] Creando el cadáver

Ya que tengamos la entidad hl2_ragdoll ahora debemos decirle a Source que cuando el jugador muera se deba crear esa entidad que actuará como el cadáver del mismo.

Para ello abriremos el archivo server/player.cpp y ubicaremos la función Event_Dying(), ahora buscamos la línea que dice:

SetLocalAngles( angles );

Y justo debajo agregaremos:

CreateRagdollEntity();
BecomeRagdollOnClient(vec3_origin);

CreateRagdollEntity() como su nombre lo indica es el responsable de crear la entidad hl2_ragdoll, sin embargo y aunque esta función esta definida dentro del código de Source la misma no hace absolutamente nada por lo que nosotros tendremos que hacerla funcionar.

Volvemos al archivo server/hl2/hl2_player.h y ubicamos la definición de la función OnTakeDamage_Alive( const CTakeDamageInfo &info ) (Línea 217 Aprox), justo debajo añadimos la definición:

virtual void		CreateRagdollEntity();

Ahora en server/hl2/hl2_player.cpp justo debajo de la función OnTakeDamage_Alive (Para mantener el orden) agregamos:

//=========================================================
// Crea un cadaver
//=========================================================
void CHL2_Player::CreateRagdollEntity()
{
	// Ya hay un cadaver.
	if ( m_hRagdoll )
	{
		// Removerlo.
		UTIL_RemoveImmediate( m_hRagdoll );
		m_hRagdoll	= NULL;
	}

	// Obtenemos el cadaver.
	CHL2Ragdoll *pRagdoll = dynamic_cast< CHL2Ragdoll* >(m_hRagdoll.Get());
	
	// Al parecer no hay ninguno, crearlo.
	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;
}

En este instante es probable que te aparezcan errores indicando que la variable m_hRagdoll no existe, es normal y la definiremos a continuación...

[Server-Side] Enviando el cadáver

Ahora es momento de enviar el cadáver al lado del cliente, pero antes de todo es necesario definir la variable que nos falta.

Vuelve a server/hl2/hl2_player.h y en la sección public de la clase CHL2_Player (Es decir, por la línea 110) declaramos la variable:

// Tracks our ragdoll entity.
CNetworkHandle( CBaseEntity, m_hRagdoll );	// networked entity handle 

Volvemos a server/hl2/hl2_player.cpp (Si, de nuevo) y ubicamos la línea:

DEFINE_FIELD( m_flTimeNextLadderHint, FIELD_TIME ),

Justo debajo agregamos:

DEFINE_FIELD(m_hRagdoll, FIELD_EHANDLE),

Ahora unas líneas más abajo (Por ahí de la 434) podrás encontrar lo siguiente:

SendPropBool( SENDINFO(m_fIsSprinting) ),

Justo debajo agregamos:

SendPropEHandle( SENDINFO(m_hRagdoll) ),

¡¡Y eso es todo!! (O al menos en el lado del servidor). Nuestro sistema de cadáveres ya esta creado y listo para ser enviado al lado del cliente.

[Client-Side] Sistema de cadáveres

Ahora es el turno del lado del cliente, debemos crear un sistema de cadáveres (una clase) que nos proporcione acceso al mismo.

Para ello abre el archivo client/hl2/c_basehlplayer.h y justo al terminar la definición de la clase C_BaseHLPlayer Línea 83 Aprox agregaremos el siguiente código:

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 );
};

En este caso no lo vincularemos con una entidad pero si será necesario implementarlo, para esto abriremos el archivo client/hl2/c_basehlplayer.cpp y al final del archivo añadiremos:

//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 strenght
				
		// 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 strenght

		// 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 out 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 );
		}
	}
}

[Client-Side] Recibiendo el cadáver

A continuación debemos volver a client/hl2/c_basehlplayer.h y dentro de la clase C_BaseHLPlayer en una sección private (es decir, por ahí de la línea 77) debemos definir la variable m_hRagdoll:

EHANDLE				m_hRagdoll;

Ahora ya solo falta recibirla por lo que devuelta a client/hl2/c_basehlplayer.cpp ubicamos la línea:

RecvPropBool( RECVINFO( m_fIsSprinting ) ),

Y justo debajo agregamos:

RecvPropEHandle( RECVINFO( m_hRagdoll ) ),

[Client-Side] La cámara

Perfecto, ahora m_hRagdoll ya contiene la información de nuestro cadáver a la hora de morir sin embargo enfrentamos otro pequeño problema... las funciones relacionadas a la cámara (en lo que queremos) están ubicadas en el archivo client/c_baseplayer.cpp y no tendrán acceso a la variable m_hRagdoll por lo que tendremos que definir las funciones que necesitemos dentro de c_basehlplayer

Y bueno, para nuestra suerte la única función que necesitaremos se llama CalcDeathCamView por lo que dentro de client/hl2/c_basehlplayer.h ubicaremos la definición:

bool				IsWeaponLowered( void ) { return m_HL2Local.m_bWeaponLowered; }

Y justo debajo agregaremos:

virtual void		CalcDeathCamView( Vector& eyeOrigin, QAngle& eyeAngles, float& fov );

Ahora en client/hl2/c_basehlplayer.cpp agregaremos (en cualquier lugar) la función:

void C_BaseHLPlayer::CalcDeathCamView(Vector& eyeOrigin, QAngle& eyeAngles, float& fov)
{
	// El ragdoll del jugador ha sido creado.
	if ( m_hRagdoll.Get() )
	{
		// Obtenemos la ubicación de los ojos del modelo.
		C_BaseHLRagdoll *pRagdoll = dynamic_cast<C_BaseHLRagdoll*>(m_hRagdoll.Get());
		pRagdoll->GetAttachment(pRagdoll->LookupAttachment("eyes"), eyeOrigin, eyeAngles);

		// Ajustamos la cámara en los ojos del modelo.
		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;
	}
}

[Shared] Haciendo funcionar la cámara

¡Por último! (Al fin) Abriremos el archivo shared/baseplayer_shared.cpp y ubicaremos la función CBasePlayer::CalcView. Si has implementado la Muerte en tercera persona(en) es probable que encuentres algo así dentro:

#ifdef CLIENT_DLL
else if ( !this->IsAlive() && ::input->CAM_IsThirdPerson() )
{
	CalcThirdPersonDeathView(eyeOrigin, eyeAngles, fov);
}
#endif

Debajo del else if solo agrega:

else if ( !this->IsAlive() )
{
	CalcDeathCamView(eyeOrigin, eyeAngles, fov);
}

De forma que quede:

#ifdef CLIENT_DLL
else if ( !this->IsAlive() && ::input->CAM_IsThirdPerson() )
{
	CalcThirdPersonDeathView(eyeOrigin, eyeAngles, fov);
}
else if ( !this->IsAlive() )
{
	CalcDeathCamView(eyeOrigin, eyeAngles, fov);
}
#endif

¡¡Y eso es todo!! Una vez que compiles el código podrás obtener la muerte en primera persona.

Todo el código proporcionado aquí es propiedad de Valve.

Problemas menores

Si te encuentras en la perspectiva de tercera persona podrás ver que al morir se generan 2 cadáveres, esto es ocasionado por la función CreateRagdollEntity pues la tercera persona ya tiene su propio cadáver, una posible solución sería no ejecutar esta función cuando se esta en tercera persona.

Véase también

Vídeo de demostración

Referencias