Alternate Multiplayer Physics: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
No edit summary
mNo edit summary
Line 1: Line 1:
{{Cleanup}}
<div class="content">
= Alternative Multiplayer Physics =
= Alternative Multiplayer Physics =


Line 12: Line 9:
  extern ConVar sv_pushaway_force;
  extern ConVar sv_pushaway_force;
  extern ConVar sv_pushaway_max_force;
  extern ConVar sv_pushaway_max_force;
 
  void AvoidPushawayProps(  CBaseCombatCharacter *pPlayer, CUserCmd *pCmd );
  void AvoidPushawayProps(  CBaseCombatCharacter *pPlayer, CUserCmd *pCmd );


Line 18: Line 15:


  void AvoidPushawayProps( CBaseCombatCharacter *pPlayer, CUserCmd *pCmd )
  void AvoidPushawayProps( CBaseCombatCharacter *pPlayer, CUserCmd *pCmd )
{
{
  // Figure out what direction we're moving and the extents of the box we're going to sweep  
  // Figure out what direction we're moving and the extents of the box we're going to sweep  
  // against physics objects.
  // against physics objects.
Line 24: Line 21:
  Vector rightdir;
  Vector rightdir;
  AngleVectors( pCmd->viewangles, &currentdir, &rightdir, NULL );
  AngleVectors( pCmd->viewangles, &currentdir, &rightdir, NULL );
 
  CBaseEntity *props[512];
  CBaseEntity *props[512];
  #ifdef CLIENT_DLL
  #ifdef CLIENT_DLL
Line 31: Line 28:
  int nEnts = GetPushawayEnts( pPlayer, props, ARRAYSIZE( props ), 0.0f, PARTITION_ENGINE_SOLID_EDICTS, NULL );
  int nEnts = GetPushawayEnts( pPlayer, props, ARRAYSIZE( props ), 0.0f, PARTITION_ENGINE_SOLID_EDICTS, NULL );
  #endif
  #endif
 
  for ( int i=0; i < nEnts; i++ )
  for ( int i=0; i < nEnts; i++ )
  {
  {
Line 38: Line 35:
  if ( pInterface && pInterface->GetMultiplayerPhysicsMode() != PHYSICS_MULTIPLAYER_SOLID )
  if ( pInterface && pInterface->GetMultiplayerPhysicsMode() != PHYSICS_MULTIPLAYER_SOLID )
  continue;
  continue;
 
  const float minMass = 10.0f; // minimum mass that can push a player back
  const float minMass = 10.0f; // minimum mass that can push a player back
  const float maxMass = 30.0f; // cap at a decently large value
  const float maxMass = 30.0f; // cap at a decently large value
Line 47: Line 44:
  }
  }
  mass = clamp( mass, minMass, maxMass );
  mass = clamp( mass, minMass, maxMass );
 
  mass = max( mass, 0 );
  mass = max( mass, 0 );
  mass /= maxMass; // bring into a 0..1 range
  mass /= maxMass; // bring into a 0..1 range
 
  // Push away from the collision point. The closer our center is to the collision point,
  // Push away from the collision point. The closer our center is to the collision point,
  // the harder we push away.
  // the harder we push away.
Line 56: Line 53:
  float flDist = VectorNormalize( vPushAway );
  float flDist = VectorNormalize( vPushAway );
  flDist = max( flDist, 1 );
  flDist = max( flDist, 1 );
 
  float flForce = sv_pushaway_player_force.GetFloat() / flDist * mass;
  float flForce = sv_pushaway_player_force.GetFloat() / flDist * mass;
  flForce = min( flForce, sv_pushaway_max_player_force.GetFloat() );
  flForce = min( flForce, sv_pushaway_max_player_force.GetFloat() );
  vPushAway *= flForce;
  vPushAway *= flForce;
 
  pCmd->forwardmove += vPushAway.Dot( currentdir );
  pCmd->forwardmove += vPushAway.Dot( currentdir );
  pCmd->sidemove    += vPushAway.Dot( rightdir );
  pCmd->sidemove    += vPushAway.Dot( rightdir );
Line 78: Line 75:
  {
  {
   ... // swap froups if necessary
   ... // swap froups if necessary
 
  if ( (collisionGroup0 == COLLISION_GROUP_PLAYER || collisionGroup0 == COLLISION_GROUP_PLAYER_MOVEMENT) &&
  if ( (collisionGroup0 == COLLISION_GROUP_PLAYER || collisionGroup0 == COLLISION_GROUP_PLAYER_MOVEMENT) &&
  collisionGroup1 == COLLISION_GROUP_PUSHAWAY )
  collisionGroup1 == COLLISION_GROUP_PUSHAWAY )
Line 84: Line 81:
  return false;
  return false;
  }
  }
 
  if ( collisionGroup0 == COLLISION_GROUP_DEBRIS && collisionGroup1 == COLLISION_GROUP_PUSHAWAY )
  if ( collisionGroup0 == COLLISION_GROUP_DEBRIS && collisionGroup1 == COLLISION_GROUP_PUSHAWAY )
  {
  {
Line 90: Line 87:
  return true;
  return true;
  }
  }
 
  ... // rest of ShouldCollide
  ... // rest of ShouldCollide
  }
  }
Line 99: Line 96:
  {
  {
  BaseClass::VPhysicsUpdate( pPhysics );
  BaseClass::VPhysicsUpdate( pPhysics );
 
  if ( m_bAwake )
  if ( m_bAwake )
  SetCollisionGroup( COLLISION_GROUP_PUSHAWAY );
  SetCollisionGroup( COLLISION_GROUP_PUSHAWAY );
Line 112: Line 109:
  return; // don't create a physics player shadow
  return; // don't create a physics player shadow
  }
  }
 
  void CBasePlayer::VPhysicsShadowUpdate( IPhysicsObject *pPhysics )
  void CBasePlayer::VPhysicsShadowUpdate( IPhysicsObject *pPhysics )
  {
  {
Line 120: Line 117:
Add a new shared function to game_shared/baseplayer_shared.cpp:
Add a new shared function to game_shared/baseplayer_shared.cpp:


void CBasePlayer::AvoidPhysicsProps( CUserCmd *pCmd )
void CBasePlayer::AvoidPhysicsProps( CUserCmd *pCmd )
  {
  {
  // Don't avoid if noclipping or in movetype none
  // Don't avoid if noclipping or in movetype none
Line 132: Line 129:
  break;
  break;
  }
  }
 
  if ( GetObserverMode() != OBS_MODE_NONE || !IsAlive() )
  if ( GetObserverMode() != OBS_MODE_NONE || !IsAlive() )
  return;
  return;
 
  AvoidPushawayProps( this, pCmd );
  AvoidPushawayProps( this, pCmd );
  }
  }
Line 144: Line 141:
  {
  {
  player->AvoidPhysicsProps( ucmd );
  player->AvoidPhysicsProps( ucmd );
 
  ...  // rest of function
  ...  // rest of function
  }
  }
Line 154: Line 151:
  {
  {
  player->AvoidPhysicsProps( ucmd );
  player->AvoidPhysicsProps( ucmd );
 
  // Call the default SetupMove code.
  // Call the default SetupMove code.
  BaseClass::SetupMove( player, ucmd, pHelper, move );
  BaseClass::SetupMove( player, ucmd, pHelper, move );
Line 167: Line 164:
  SetNextThink( gpGlobals->curtime + 0.05f, "PushawayThink" );
  SetNextThink( gpGlobals->curtime + 0.05f, "PushawayThink" );
  }
  }
 
  void CSDKPlayer::Spawn()
  void CSDKPlayer::Spawn()
  {
  {
  ...
  ...
 
   SetContextThink( &CSKDPlayer::PushawayThink, gpGlobals->curtime + 0.05f, "PushawayThink" );
   SetContextThink( &CSKDPlayer::PushawayThink, gpGlobals->curtime + 0.05f, "PushawayThink" );
  }
  }
Line 182: Line 179:
  m_fNextThinkPushAway = 0.0f; // new float in C_SDKPlayer
  m_fNextThinkPushAway = 0.0f; // new float in C_SDKPlayer
  }
  }
 
  void C_SDKPlayer::ClientThink()
  void C_SDKPlayer::ClientThink()
  {
  {
  BaseClass::ClientThink();
  BaseClass::ClientThink();
 
  if ( gpGlobals->curtime >= m_fNextThinkPushAway )
  if ( gpGlobals->curtime >= m_fNextThinkPushAway )
  {
  {
Line 199: Line 196:
  {
  {
  ... // observer code
  ... // observer code
 
  // push objects in turbo physics mode
  // push objects in turbo physics mode
  if ( m_nButtons & IN_USE )
  if ( m_nButtons & IN_USE )
Line 205: Line 202:
  Vector forward, up;
  Vector forward, up;
  EyeVectors( &forward, NULL, &up );
  EyeVectors( &forward, NULL, &up );
 
  trace_t tr;
  trace_t tr;
  // Search for objects in a sphere (tests for entities that are not solid, yet still useable)
  // Search for objects in a sphere (tests for entities that are not solid, yet still useable)
  Vector searchCenter = EyePosition();
  Vector searchCenter = EyePosition();
 
  UTIL_TraceLine( searchCenter, searchCenter + forward * 96.0f, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr );
  UTIL_TraceLine( searchCenter, searchCenter + forward * 96.0f, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr );
 
  // try the hit entity if there is one, or the ground entity if there isn't.
  // try the hit entity if there is one, or the ground entity if there isn't.
  CBaseEntity *entity = tr.m_pEnt;
  CBaseEntity *entity = tr.m_pEnt;
 
  if (entity && entity->VPhysicsGetObject() )
  if (entity && entity->VPhysicsGetObject() )
  {
  {
  IPhysicsObject *pObj = entity->VPhysicsGetObject();
  IPhysicsObject *pObj = entity->VPhysicsGetObject();
 
  Vector vPushAway = (entity->WorldSpaceCenter() - WorldSpaceCenter());
  Vector vPushAway = (entity->WorldSpaceCenter() - WorldSpaceCenter());
  vPushAway.z = 0;
  vPushAway.z = 0;
 
  float flDist = VectorNormalize( vPushAway );
  float flDist = VectorNormalize( vPushAway );
  flDist = max( flDist, 1 );
  flDist = max( flDist, 1 );
 
  float flForce = sv_pushaway_force.GetFloat() / flDist;
  float flForce = sv_pushaway_force.GetFloat() / flDist;
  flForce = min( flForce, sv_pushaway_max_force.GetFloat() );
  flForce = min( flForce, sv_pushaway_max_force.GetFloat() );
 
  pObj->ApplyForceOffset( vPushAway * flForce, WorldSpaceCenter() );
  pObj->ApplyForceOffset( vPushAway * flForce, WorldSpaceCenter() );
  }
  }
  }
  }
 
  ... // rest of PlayerUse
  ... // rest of PlayerUse
  }
  }


<br />
----
<span style="font-size: xx-small; font-family:Arial, Helvetica, sans-serif"> � 2004 Valve Corporation. All rights reserved. Valve, the Valve logo, Half-Life, the Half-Life logo, the Lambda logo, Steam, the Steam logo, Team Fortress, the Team Fortress logo, Opposing Force, Day of Defeat, the Day of Defeat logo, Counter-Strike, the Counter-Strike logo, Source, the Source logo, Hammer and Counter-Strike: Condition Zero are trademarks and/or registered trademarks of Valve Corporation.��Microsoft and Visual Studio are trademarks and/or registered trademarks of Microsoft Corporation.� All other trademarks are property of their respective owners.�</span>
</span>
[[Category:Programming]]
[[Category:Programming]]

Revision as of 15:06, 24 August 2005

Alternative Multiplayer Physics

This document describes an alternative multiplayer physics behavior for "prop_physics_multiplayer" objects. The basic idea is to remove direct collision between a player and moving physics objects and push players away from them based on their distance. Once a physics object has settled and isn't moving anymore, the push-away force is removed and the normal collision rules are restored, so players can stand on physics objects. Players don't have a physics shadow anymore, which means they can't move objects just be touching them. They have to press USE to apply a pushing force.

This code sample is not complete, sometimes you have to add new class function declarations or include "obstacle_pushaway.h" in some files. But all additional changes should be obvious. If the files game_shared\obstacle_pushaway.cpp/.h are missing, updated your Source SDK code base via Steam.

Add to game_shared\obstacle_pushaway.h the following lines and include it in base_player_shared.cpp, c_sdk_player.cpp, sdk_player.cpp.

extern ConVar sv_pushaway_force;
extern ConVar sv_pushaway_max_force;

void AvoidPushawayProps(  CBaseCombatCharacter *pPlayer, CUserCmd *pCmd );

Implement AvoidPushawayProps in game_shared\obstacle_pushaway.cpp and include file in build:

void AvoidPushawayProps( CBaseCombatCharacter *pPlayer, CUserCmd *pCmd )
{
		// Figure out what direction we're moving and the extents of the box we're going to sweep 
		// against physics objects.
		Vector currentdir;
		Vector rightdir;
		AngleVectors( pCmd->viewangles, &currentdir, &rightdir, NULL );

		CBaseEntity *props[512];
	#ifdef CLIENT_DLL
		int nEnts = GetPushawayEnts( pPlayer, props, ARRAYSIZE( props ), 0.0f, PARTITION_CLIENT_SOLID_EDICTS, NULL );
	#else
		int nEnts = GetPushawayEnts( pPlayer, props, ARRAYSIZE( props ), 0.0f, PARTITION_ENGINE_SOLID_EDICTS, NULL );
	#endif

		for ( int i=0; i < nEnts; i++ )
		{
			// Don't respond to this entity on the client unless it has PHYSICS_MULTIPLAYER_FULL set.
			IMultiplayerPhysics *pInterface = dynamic_cast<IMultiplayerPhysics*>( props[i] );
			if ( pInterface && pInterface->GetMultiplayerPhysicsMode() != PHYSICS_MULTIPLAYER_SOLID )
				continue;

			const float minMass = 10.0f; // minimum mass that can push a player back
			const float maxMass = 30.0f; // cap at a decently large value
			float mass = maxMass;
			if ( pInterface )
			{
				mass = pInterface->GetMass();
			}
			mass = clamp( mass, minMass, maxMass );

			mass = max( mass, 0 );
			mass /= maxMass; // bring into a 0..1 range

			// Push away from the collision point. The closer our center is to the collision point,
			// the harder we push away.
			Vector vPushAway = (pPlayer->WorldSpaceCenter() - props[i]->WorldSpaceCenter());
			float flDist = VectorNormalize( vPushAway );
			flDist = max( flDist, 1 );

			float flForce = sv_pushaway_player_force.GetFloat() / flDist * mass;
			flForce = min( flForce, sv_pushaway_max_player_force.GetFloat() );
			vPushAway *= flForce;

			pCmd->forwardmove += vPushAway.Dot( currentdir );
			pCmd->sidemove    += vPushAway.Dot( rightdir );
		}
	}

Also in obstacle_pushaway.cpp remove these lines from function PerformObstaclePushaway:

	#ifdef GAME_DLL
			if ( pInterface->IsAsleep() && sv_turbophysics.GetBool() )
				continue;
	#endif

In CSDKGameRules add 2 new rules so players don't get stuck with object in COLLISION_GROUP_PUSHAWAY mode.

bool CSDKGameRules::ShouldCollide( int collisionGroup0, int collisionGroup1 )
{
  ... // swap froups if necessary

	if ( (collisionGroup0 == COLLISION_GROUP_PLAYER || collisionGroup0 == COLLISION_GROUP_PLAYER_MOVEMENT) &&
		collisionGroup1 == COLLISION_GROUP_PUSHAWAY )
	{
		return false;
	}

	if ( collisionGroup0 == COLLISION_GROUP_DEBRIS && collisionGroup1 == COLLISION_GROUP_PUSHAWAY )
	{
		// let debris and multiplayer objects collide
		return true;
	}

	... // rest of ShouldCollide
}

Use "prop_physics_multiplayer" in your maps and override their virtual function VPhysicsUpdate:

void CPhysicsPropMultiplayer::VPhysicsUpdate( IPhysicsObject *pPhysics )
{
	BaseClass::VPhysicsUpdate( pPhysics );

	if ( m_bAwake )
		SetCollisionGroup( COLLISION_GROUP_PUSHAWAY );
	else
		SetCollisionGroup( COLLISION_GROUP_NONE );
}	

Remove the physics shadows for players:

void CBasePlayer::InitVCollision( void )
{
	return; // don't create a physics player shadow
}

void CBasePlayer::VPhysicsShadowUpdate( IPhysicsObject *pPhysics )
{
	return; // player doesn't have a physics shadow
}

Add a new shared function to game_shared/baseplayer_shared.cpp:

	void CBasePlayer::AvoidPhysicsProps( CUserCmd *pCmd )
	{
		// Don't avoid if noclipping or in movetype none
		switch ( GetMoveType() )
		{
		case MOVETYPE_NOCLIP:
		case MOVETYPE_NONE:
		case MOVETYPE_OBSERVER:
			return;
		default:
			break;
		}

		if ( GetObserverMode() != OBS_MODE_NONE || !IsAlive() )
			return;

		AvoidPushawayProps( this, pCmd );
	}

This function is called on the server in CSDKPlayerMove::SetupMove:

void CSDKPlayerMove::SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move )
{
	player->AvoidPhysicsProps( ucmd );

	...  // rest of function
}

And on the client in CSDKPrediction::SetupMove:

void CSDKPrediction::SetupMove( C_BasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, 
	CMoveData *move )
{
	player->AvoidPhysicsProps( ucmd );

	// Call the default SetupMove code.
	BaseClass::SetupMove( player, ucmd, pHelper, move );
}

Also PushawayThink must be called every 0.05 seconds on both client and server. That's done in Think functions. Here code for the server:

void CSDKPlayer::PushawayThink()
{
	// Push physics props out of our way.
	PerformObstaclePushaway( this );
	SetNextThink( gpGlobals->curtime + 0.05f, "PushawayThink" );
}

void CSDKPlayer::Spawn()
{
	...

 	SetContextThink( &CSKDPlayer::PushawayThink, gpGlobals->curtime + 0.05f, "PushawayThink" );
}

And the same call every 0.05 seconds on the client by overriding virtual function ClientThink:

C_SDKPlayer::C_SDKPlayer()
{
	...
	m_fNextThinkPushAway = 0.0f; // new float in C_SDKPlayer
}

void C_SDKPlayer::ClientThink()
{
	BaseClass::ClientThink();

	if ( gpGlobals->curtime >= m_fNextThinkPushAway )
	{
		PerformObstaclePushaway( this );
		m_fNextThinkPushAway =  gpGlobals->curtime + 0.05f;
	}
}

Finally, let the player push away objects using the USE key

void CSDKPlayer::PlayerUse ( void )
{
	... // observer code

	// push objects in turbo physics mode
	if ( m_nButtons & IN_USE )
	{
		Vector forward, up;
		EyeVectors( &forward, NULL, &up );

		trace_t tr;
		// Search for objects in a sphere (tests for entities that are not solid, yet still useable)
		Vector searchCenter = EyePosition();

		UTIL_TraceLine( searchCenter, searchCenter + forward * 96.0f, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr );

		// try the hit entity if there is one, or the ground entity if there isn't.
		CBaseEntity *entity = tr.m_pEnt;

		if (entity && entity->VPhysicsGetObject() )
		{
			IPhysicsObject *pObj = entity->VPhysicsGetObject();

			Vector vPushAway = (entity->WorldSpaceCenter() - WorldSpaceCenter());
			vPushAway.z = 0;

			float flDist = VectorNormalize( vPushAway );
			flDist = max( flDist, 1 );

			float flForce = sv_pushaway_force.GetFloat() / flDist;
			flForce = min( flForce, sv_pushaway_max_force.GetFloat() );

			pObj->ApplyForceOffset( vPushAway * flForce, WorldSpaceCenter() );
		}
	}

	... // rest of PlayerUse
}