Alternate Multiplayer Physics: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
No edit summary
m (clean up, added deadend tag)
 
(5 intermediate revisions by 3 users not shown)
Line 1: Line 1:
{{Cleanup}}
{{Dead end|date=January 2024}}
<div class="content">


= Alternative Multiplayer Physics =
== Overview ==
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 by touching them. They have to press USE to apply a pushing force.


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, update your Source SDK code base via Steam.
 
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.


== Instructions ==
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.
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_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 17:


  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 23:
  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 30:
  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 37:
  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 46:
  }
  }
  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 55:
  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 77:
  {
  {
   ... // 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 83:
  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 89:
  return true;
  return true;
  }
  }
 
  ... // rest of ShouldCollide
  ... // rest of ShouldCollide
  }
  }
Line 99: Line 98:
  {
  {
  BaseClass::VPhysicsUpdate( pPhysics );
  BaseClass::VPhysicsUpdate( pPhysics );
 
  if ( m_bAwake )
  if ( m_bAwake )
  SetCollisionGroup( COLLISION_GROUP_PUSHAWAY );
  SetCollisionGroup( COLLISION_GROUP_PUSHAWAY );
Line 112: Line 111:
  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 119:
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 131:
  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 143:
  {
  {
  player->AvoidPhysicsProps( ucmd );
  player->AvoidPhysicsProps( ucmd );
 
  ...  // rest of function
  ...  // rest of function
  }
  }
Line 154: Line 153:
  {
  {
  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 166:
  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 181:
  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 198:
  {
  {
  ... // 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 204:
  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 usable)
  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]]
[[Category:Physics]]

Latest revision as of 08:47, 21 January 2024

Dead End - Icon.png
This article has no Wikipedia icon links to other VDC articles. Please help improve this article by adding links Wikipedia icon that are relevant to the context within the existing text.
January 2024

Overview

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 by 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, update your Source SDK code base via Steam.

Instructions

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