//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Physics constraint entities // // $NoKeywords: $ //===========================================================================// #include "cbase.h" #include "physics.h" #include "entityoutput.h" #include "engine/IEngineSound.h" #include "igamesystem.h" #include "physics_saverestore.h" #include "vcollide_parse.h" #include "positionwatcher.h" #include "fmtstr.h" #include "physics_prop_ragdoll.h" #define HINGE_NOTIFY HL2_EPISODIC #if HINGE_NOTIFY #include "physconstraint_sounds.h" #endif #include "physconstraint.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" #define SF_CONSTRAINT_DISABLE_COLLISION 0x0001 #define SF_SLIDE_LIMIT_ENDS 0x0002 #define SF_PULLEY_RIGID 0x0002 #define SF_LENGTH_RIGID 0x0002 #define SF_RAGDOLL_FREEMOVEMENT 0x0002 #define SF_CONSTRAINT_START_INACTIVE 0x0004 #define SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY 0x0008 #define SF_CONSTRAINT_NO_CONNECT_UNTIL_ACTIVATED 0x0010 // Will only check the two attached entities at activation ConVar g_debug_constraint_sounds ( "g_debug_constraint_sounds", "0", FCVAR_CHEAT, "Enable debug printing about constraint sounds."); struct constraint_anchor_t { Vector localOrigin; EHANDLE hEntity; int parentAttachment; string_t name; float massScale; }; class CAnchorList : public CAutoGameSystem { public: CAnchorList( char const *name ) : CAutoGameSystem( name ) { } void LevelShutdownPostEntity() { m_list.Purge(); } void AddToList( CBaseEntity *pEntity, float massScale ) { int index = m_list.AddToTail(); constraint_anchor_t *pAnchor = &m_list[index]; pAnchor->hEntity = pEntity->GetParent(); pAnchor->parentAttachment = pEntity->GetParentAttachment(); pAnchor->name = pEntity->GetEntityName(); pAnchor->localOrigin = pEntity->GetLocalOrigin(); pAnchor->massScale = massScale; } constraint_anchor_t *Find( string_t name ) { for ( int i = m_list.Count()-1; i >=0; i-- ) { if ( FStrEq( STRING(m_list[i].name), STRING(name) ) ) { return &m_list[i]; } } return NULL; } private: CUtlVector m_list; }; static CAnchorList g_AnchorList( "CAnchorList" ); class CConstraintAnchor : public CPointEntity { DECLARE_CLASS( CConstraintAnchor, CPointEntity ); public: CConstraintAnchor() { m_massScale = 1.0f; } void Spawn( void ) { if ( GetParent() ) { g_AnchorList.AddToList( this, m_massScale ); UTIL_Remove( this ); } } DECLARE_DATADESC(); float m_massScale; }; BEGIN_DATADESC( CConstraintAnchor ) DEFINE_KEYFIELD( m_massScale, FIELD_FLOAT, "massScale" ), END_DATADESC() LINK_ENTITY_TO_CLASS( info_constraint_anchor, CConstraintAnchor ); class CPhysConstraintSystem : public CLogicalEntity { DECLARE_CLASS( CPhysConstraintSystem, CLogicalEntity ); public: void Spawn(); IPhysicsConstraintGroup *GetVPhysicsGroup() { return m_pMachine; } DECLARE_DATADESC(); private: IPhysicsConstraintGroup *m_pMachine; int m_additionalIterations; }; BEGIN_DATADESC( CPhysConstraintSystem ) DEFINE_PHYSPTR( m_pMachine ), DEFINE_KEYFIELD( m_additionalIterations, FIELD_INTEGER, "additionaliterations" ), END_DATADESC() void CPhysConstraintSystem::Spawn() { constraint_groupparams_t group; group.Defaults(); group.additionalIterations = m_additionalIterations; m_pMachine = physenv->CreateConstraintGroup( group ); } LINK_ENTITY_TO_CLASS( phys_constraintsystem, CPhysConstraintSystem ); void PhysTeleportConstrainedEntity( CBaseEntity *pTeleportSource, IPhysicsObject *pObject0, IPhysicsObject *pObject1, const Vector &prevPosition, const QAngle &prevAngles, bool physicsRotate ) { // teleport the other object CBaseEntity *pEntity0 = static_cast (pObject0->GetGameData()); CBaseEntity *pEntity1 = static_cast (pObject1->GetGameData()); if ( !pEntity0 || !pEntity1 ) return; // figure out which entity needs to be fixed up (the one that isn't pTeleportSource) CBaseEntity *pFixup = pEntity1; // teleport the other object if ( pTeleportSource != pEntity0 ) { if ( pTeleportSource != pEntity1 ) { Msg("Bogus teleport notification!!\n"); return; } pFixup = pEntity0; } // constraint doesn't move this entity if ( pFixup->GetMoveType() != MOVETYPE_VPHYSICS ) return; if ( !pFixup->VPhysicsGetObject() || !pFixup->VPhysicsGetObject()->IsMoveable() ) return; QAngle oldAngles = prevAngles; if ( !physicsRotate ) { oldAngles = pTeleportSource->GetAbsAngles(); } matrix3x4_t startCoord, startInv, endCoord, xform; AngleMatrix( oldAngles, prevPosition, startCoord ); MatrixInvert( startCoord, startInv ); ConcatTransforms( pTeleportSource->EntityToWorldTransform(), startInv, xform ); QAngle fixupAngles; Vector fixupPos; ConcatTransforms( xform, pFixup->EntityToWorldTransform(), endCoord ); MatrixAngles( endCoord, fixupAngles, fixupPos ); pFixup->Teleport( &fixupPos, &fixupAngles, NULL ); } static void DrawPhysicsBounds( IPhysicsObject *pObject, int r, int g, int b, int a ) { const CPhysCollide *pCollide = pObject->GetCollide(); Vector pos; QAngle angles; pObject->GetPosition( &pos, &angles ); Vector mins, maxs; physcollision->CollideGetAABB( &mins, &maxs, pCollide, vec3_origin, vec3_angle ); // don't fight the z-buffer mins -= Vector(1,1,1); maxs += Vector(1,1,1); NDebugOverlay::BoxAngles( pos, mins, maxs, angles, r, g, b, a, 0 ); } static void DrawConstraintObjectsAxes(CBaseEntity *pConstraintEntity, IPhysicsConstraint *pConstraint) { if ( !pConstraint || !pConstraintEntity ) return; matrix3x4_t xformRef, xformAtt; bool bXform = pConstraint->GetConstraintTransform( &xformRef, &xformAtt ); IPhysicsObject *pRef = pConstraint->GetReferenceObject(); if ( pRef && !pRef->IsStatic() ) { if ( bXform ) { Vector pos, posWorld; QAngle angles; MatrixAngles( xformRef, angles, pos ); pRef->LocalToWorld( &posWorld, pos ); NDebugOverlay::Axis( posWorld, vec3_angle, 12, false, 0 ); } DrawPhysicsBounds( pRef, 0, 255, 0, 12 ); } IPhysicsObject *pAttach = pConstraint->GetAttachedObject(); if ( pAttach && !pAttach->IsStatic() ) { if ( bXform ) { Vector pos, posWorld; QAngle angles; MatrixAngles( xformAtt, angles, pos ); pAttach->LocalToWorld( &posWorld, pos ); NDebugOverlay::Axis( posWorld, vec3_angle, 12, false, 0 ); } DrawPhysicsBounds( pAttach, 255, 0, 0, 12 ); } } void CPhysConstraint::ClearStaticFlag( IPhysicsObject *pObj ) { if ( !pObj ) return; PhysClearGameFlags( pObj, FVPHYSICS_CONSTRAINT_STATIC ); } void CPhysConstraint::Deactivate() { if ( !m_pConstraint ) return; m_pConstraint->Deactivate(); ClearStaticFlag( m_pConstraint->GetReferenceObject() ); ClearStaticFlag( m_pConstraint->GetAttachedObject() ); if ( m_spawnflags & SF_CONSTRAINT_DISABLE_COLLISION ) { // constraint may be getting deactivated because an object got deleted, so check them here. IPhysicsObject *pRef = m_pConstraint->GetReferenceObject(); IPhysicsObject *pAtt = m_pConstraint->GetAttachedObject(); if ( pRef && pAtt ) { PhysEnableEntityCollisions( pRef, pAtt ); } } } void CPhysConstraint::OnBreak( void ) { Deactivate(); if ( m_breakSound != NULL_STRING ) { CPASAttenuationFilter filter( this, ATTN_STATIC ); Vector origin = GetAbsOrigin(); Vector refPos = origin, attachPos = origin; IPhysicsObject *pRef = m_pConstraint->GetReferenceObject(); if ( pRef && (pRef != g_PhysWorldObject) ) { pRef->GetPosition( &refPos, NULL ); attachPos = refPos; } IPhysicsObject *pAttach = m_pConstraint->GetAttachedObject(); if ( pAttach && (pAttach != g_PhysWorldObject) ) { pAttach->GetPosition( &attachPos, NULL ); if ( !pRef || (pRef == g_PhysWorldObject) ) { refPos = attachPos; } } VectorAdd( refPos, attachPos, origin ); origin *= 0.5f; EmitSound_t ep; ep.m_nChannel = CHAN_STATIC; ep.m_pSoundName = STRING(m_breakSound); ep.m_flVolume = VOL_NORM; ep.m_SoundLevel = ATTN_TO_SNDLVL( ATTN_STATIC ); ep.m_pOrigin = &origin; EmitSound( filter, entindex(), ep ); } m_OnBreak.FireOutput( this, this ); // queue this up to be deleted at the end of physics // The Deactivate() call should make sure we don't get more of these callbacks. PhysCallbackRemove( this->NetworkProp() ); } void CPhysConstraint::InputBreak( inputdata_t &inputdata ) { if ( m_pConstraint ) m_pConstraint->Deactivate(); OnBreak(); } void CPhysConstraint::InputOnBreak( inputdata_t &inputdata ) { OnBreak(); } void CPhysConstraint::InputTurnOn( inputdata_t &inputdata ) { if ( HasSpawnFlags( SF_CONSTRAINT_NO_CONNECT_UNTIL_ACTIVATED ) ) { ActivateConstraint(); } if ( !m_pConstraint || !m_pConstraint->GetReferenceObject() || !m_pConstraint->GetAttachedObject() ) return; m_pConstraint->Activate(); m_pConstraint->GetReferenceObject()->Wake(); m_pConstraint->GetAttachedObject()->Wake(); } void CPhysConstraint::InputTurnOff( inputdata_t &inputdata ) { Deactivate(); } int CPhysConstraint::DrawDebugTextOverlays() { int pos = BaseClass::DrawDebugTextOverlays(); if ( m_pConstraint && (m_debugOverlays & OVERLAY_TEXT_BIT) ) { constraint_breakableparams_t params; Q_memset(¶ms,0,sizeof(params)); m_pConstraint->GetConstraintParams( ¶ms ); if ( (params.bodyMassScale[0] != 1.0f && params.bodyMassScale[0] != 0.0f) || (params.bodyMassScale[1] != 1.0f && params.bodyMassScale[1] != 0.0f) ) { CFmtStr str("mass ratio %.4f:%.4f\n", params.bodyMassScale[0], params.bodyMassScale[1] ); NDebugOverlay::EntityTextAtPosition( GetAbsOrigin(), pos, str.Access(), 0, 255, 255, 0, 255 ); } pos++; } return pos; } void CPhysConstraint::DrawDebugGeometryOverlays() { if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) { DrawConstraintObjectsAxes(this, m_pConstraint); } BaseClass::DrawDebugGeometryOverlays(); } void CPhysConstraint::GetBreakParams( constraint_breakableparams_t ¶ms, const hl_constraint_info_t &info ) { params.Defaults(); params.forceLimit = lbs2kg(m_forceLimit); params.torqueLimit = lbs2kg(m_torqueLimit); params.isActive = HasSpawnFlags( SF_CONSTRAINT_START_INACTIVE ) ? false : true; params.bodyMassScale[0] = info.massScale[0]; params.bodyMassScale[1] = info.massScale[1]; } BEGIN_DATADESC( CPhysConstraint ) DEFINE_PHYSPTR( m_pConstraint ), DEFINE_KEYFIELD( m_nameSystem, FIELD_STRING, "constraintsystem" ), DEFINE_KEYFIELD( m_nameAttach1, FIELD_STRING, "attach1" ), DEFINE_KEYFIELD( m_nameAttach2, FIELD_STRING, "attach2" ), DEFINE_KEYFIELD( m_breakSound, FIELD_SOUNDNAME, "breaksound" ), DEFINE_KEYFIELD( m_forceLimit, FIELD_FLOAT, "forcelimit" ), DEFINE_KEYFIELD( m_torqueLimit, FIELD_FLOAT, "torquelimit" ), DEFINE_KEYFIELD( m_minTeleportDistance, FIELD_FLOAT, "teleportfollowdistance" ), // DEFINE_FIELD( m_teleportTick, FIELD_INTEGER ), DEFINE_OUTPUT( m_OnBreak, "OnBreak" ), DEFINE_INPUTFUNC( FIELD_VOID, "Break", InputBreak ), DEFINE_INPUTFUNC( FIELD_VOID, "ConstraintBroken", InputOnBreak ), DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), END_DATADESC() CPhysConstraint::CPhysConstraint( void ) { m_pConstraint = NULL; m_nameAttach1 = NULL_STRING; m_nameAttach2 = NULL_STRING; m_forceLimit = 0; m_torqueLimit = 0; m_teleportTick = 0xFFFFFFFF; m_minTeleportDistance = 0.0f; } CPhysConstraint::~CPhysConstraint() { Deactivate(); physenv->DestroyConstraint( m_pConstraint ); } void CPhysConstraint::Precache( void ) { if ( m_breakSound != NULL_STRING ) { PrecacheScriptSound( STRING(m_breakSound) ); } } void CPhysConstraint::Spawn( void ) { BaseClass::Spawn(); Precache(); } // debug function - slow, uses dynamic_cast<> - use this to query the attached objects // physics_debug_entity toggles the constraint system for an object using this bool GetConstraintAttachments( CBaseEntity *pEntity, CBaseEntity *pAttachOut[2], IPhysicsObject *pAttachVPhysics[2] ) { CPhysConstraint *pConstraintEntity = dynamic_cast(pEntity); if ( pConstraintEntity ) { IPhysicsConstraint *pConstraint = pConstraintEntity->GetPhysConstraint(); if ( pConstraint ) { IPhysicsObject *pRef = pConstraint->GetReferenceObject(); pAttachVPhysics[0] = pRef; pAttachOut[0] = pRef ? static_cast(pRef->GetGameData()) : NULL; IPhysicsObject *pAttach = pConstraint->GetAttachedObject(); pAttachVPhysics[1] = pAttach; pAttachOut[1] = pAttach ? static_cast(pAttach->GetGameData()) : NULL; return true; } } return false; } void DebugConstraint(CBaseEntity *pEntity) { CPhysConstraint *pConstraintEntity = dynamic_cast(pEntity); if ( pConstraintEntity ) { IPhysicsConstraint *pConstraint = pConstraintEntity->GetPhysConstraint(); if ( pConstraint ) { pConstraint->OutputDebugInfo(); } } } void FindPhysicsAnchor( string_t name, hl_constraint_info_t &info, int index, CBaseEntity *pErrorEntity ) { constraint_anchor_t *pAnchor = g_AnchorList.Find( name ); if ( pAnchor ) { CBaseEntity *pEntity = pAnchor->hEntity; if ( pEntity ) { info.massScale[index] = pAnchor->massScale; bool bWroteAttachment = false; if ( pAnchor->parentAttachment > 0 ) { CBaseAnimating *pAnim = pAnchor->hEntity->GetBaseAnimating(); if ( pAnim ) { IPhysicsObject *list[VPHYSICS_MAX_OBJECT_LIST_COUNT]; int listCount = pAnchor->hEntity->VPhysicsGetObjectList( list, ARRAYSIZE(list) ); int iPhysicsBone = pAnim->GetPhysicsBone( pAnim->GetAttachmentBone( pAnchor->parentAttachment ) ); if ( iPhysicsBone < listCount ) { Vector pos; info.pObjects[index] = list[iPhysicsBone]; pAnim->GetAttachment( pAnchor->parentAttachment, pos ); list[iPhysicsBone]->WorldToLocal( &info.anchorPosition[index], pos ); bWroteAttachment = true; } } } if ( !bWroteAttachment ) { info.anchorPosition[index] = pAnchor->localOrigin; info.pObjects[index] = pAnchor->hEntity->VPhysicsGetObject(); } } else { pAnchor = NULL; } } if ( !pAnchor ) { info.anchorPosition[index] = vec3_origin; info.pObjects[index] = FindPhysicsObjectByName( STRING(name), pErrorEntity ); info.massScale[index] = 1.0f; } } void CPhysConstraint::OnConstraintSetup( hl_constraint_info_t &info ) { if ( info.pObjects[0] && info.pObjects[1] ) { SetupTeleportationHandling( info ); } if ( m_spawnflags & SF_CONSTRAINT_DISABLE_COLLISION ) { PhysDisableEntityCollisions( info.pObjects[0], info.pObjects[1] ); } } void CPhysConstraint::SetupTeleportationHandling( hl_constraint_info_t &info ) { CBaseEntity *pEntity0 = (CBaseEntity *)info.pObjects[0]->GetGameData(); if ( pEntity0 ) { g_pNotify->AddEntity( this, pEntity0 ); } CBaseEntity *pEntity1 = (CBaseEntity *)info.pObjects[1]->GetGameData(); if ( pEntity1 ) { g_pNotify->AddEntity( this, pEntity1 ); } } static IPhysicsConstraintGroup *GetRagdollConstraintGroup( IPhysicsObject *pObj ) { if ( pObj ) { CBaseEntity *pEntity = static_cast(pObj->GetGameData()); ragdoll_t *pRagdoll = Ragdoll_GetRagdoll(pEntity); if ( pRagdoll ) return pRagdoll->pGroup; } return NULL; } void CPhysConstraint::GetConstraintObjects( hl_constraint_info_t &info ) { FindPhysicsAnchor( m_nameAttach1, info, 0, this ); FindPhysicsAnchor( m_nameAttach2, info, 1, this ); // Missing one object, assume the world instead if ( info.pObjects[0] == NULL && info.pObjects[1] ) { if ( Q_strlen(STRING(m_nameAttach1)) ) { Warning("Bogus constraint %s (attaches ENTITY NOT FOUND:%s to %s)\n", GetDebugName(), STRING(m_nameAttach1), STRING(m_nameAttach2)); #ifdef HL2_EPISODIC info.pObjects[0] = info.pObjects[1] = NULL; return; #endif // HL2_EPISODIC } info.pObjects[0] = g_PhysWorldObject; info.massScale[0] = info.massScale[1] = 1.0f; // no mass scale on world constraint } else if ( info.pObjects[0] && !info.pObjects[1] ) { if ( Q_strlen(STRING(m_nameAttach2)) ) { Warning("Bogus constraint %s (attaches %s to ENTITY NOT FOUND:%s)\n", GetDebugName(), STRING(m_nameAttach1), STRING(m_nameAttach2)); #ifdef HL2_EPISODIC info.pObjects[0] = info.pObjects[1] = NULL; return; #endif // HL2_EPISODIC } info.pObjects[1] = info.pObjects[0]; info.pObjects[0] = g_PhysWorldObject; // Try to make the world object consistently object0 for ease of implementation info.massScale[0] = info.massScale[1] = 1.0f; // no mass scale on world constraint info.swapped = true; } info.pGroup = GetRagdollConstraintGroup(info.pObjects[0]); if ( !info.pGroup ) { info.pGroup = GetRagdollConstraintGroup(info.pObjects[1]); } } void CPhysConstraint::Activate( void ) { BaseClass::Activate(); if ( HasSpawnFlags( SF_CONSTRAINT_NO_CONNECT_UNTIL_ACTIVATED ) == false ) { if ( !ActivateConstraint() ) { UTIL_Remove(this); } } } IPhysicsConstraintGroup *GetConstraintGroup( string_t systemName ) { CBaseEntity *pMachine = gEntList.FindEntityByName( NULL, systemName ); if ( pMachine ) { CPhysConstraintSystem *pGroup = dynamic_cast(pMachine); if ( pGroup ) { return pGroup->GetVPhysicsGroup(); } } return NULL; } bool CPhysConstraint::ActivateConstraint( void ) { // A constraint attaches two objects to each other. // The constraint is specified in the coordinate frame of the "reference" object // and constrains the "attached" object hl_constraint_info_t info; if ( m_pConstraint ) { // already have a constraint, don't make a new one info.pObjects[0] = m_pConstraint->GetReferenceObject(); info.pObjects[1] = m_pConstraint->GetAttachedObject(); OnConstraintSetup(info); return true; } GetConstraintObjects( info ); if ( !info.pObjects[0] && !info.pObjects[1] ) return false; if ( info.pObjects[0]->IsStatic() && info.pObjects[1]->IsStatic() ) { Warning("Constraint (%s) attached to two static objects (%s and %s)!!!\n", STRING(GetEntityName()), STRING(m_nameAttach1), m_nameAttach2 == NULL_STRING ? "world" : STRING(m_nameAttach2) ); return false; } if ( info.pObjects[0]->GetShadowController() && info.pObjects[1]->GetShadowController() ) { Warning("Constraint (%s) attached to two shadow objects (%s and %s)!!!\n", STRING(GetEntityName()), STRING(m_nameAttach1), m_nameAttach2 == NULL_STRING ? "world" : STRING(m_nameAttach2) ); return false; } IPhysicsConstraintGroup *pGroup = GetConstraintGroup( m_nameSystem ); if ( !pGroup ) { pGroup = info.pGroup; } m_pConstraint = CreateConstraint( pGroup, info ); if ( !m_pConstraint ) return false; m_pConstraint->SetGameData( (void *)this ); if ( pGroup ) { pGroup->Activate(); } OnConstraintSetup(info); return true; } void CPhysConstraint::NotifySystemEvent( CBaseEntity *pNotify, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ) { // don't recurse if ( eventType != NOTIFY_EVENT_TELEPORT || (unsigned int)gpGlobals->tickcount == m_teleportTick ) return; float distance = (params.pTeleport->prevOrigin - pNotify->GetAbsOrigin()).Length(); // no need to follow a small teleport if ( distance <= m_minTeleportDistance ) return; m_teleportTick = gpGlobals->tickcount; PhysTeleportConstrainedEntity( pNotify, m_pConstraint->GetReferenceObject(), m_pConstraint->GetAttachedObject(), params.pTeleport->prevOrigin, params.pTeleport->prevAngles, params.pTeleport->physicsRotate ); } class CPhysHinge : public CPhysConstraint, public IVPhysicsWatcher { DECLARE_CLASS( CPhysHinge, CPhysConstraint ); public: void Spawn( void ); IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) { if ( m_hinge.worldAxisDirection == vec3_origin ) { DevMsg("ERROR: Hinge with bad data!!!\n" ); return NULL; } GetBreakParams( m_hinge.constraint, info ); m_hinge.constraint.strength = 1.0; // BUGBUG: These numbers are very hard to edit // Scale by 1000 to make things easier // CONSIDER: Unify the units of torque around something other // than HL units (kg * in^2 / s ^2) m_hinge.hingeAxis.SetAxisFriction( 0, 0, m_hingeFriction * 1000 ); int hingeAxis; if ( IsWorldHinge( info, &hingeAxis ) ) { info.pObjects[1]->BecomeHinged( hingeAxis ); } else { RemoveSpawnFlags( SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY ); } return physenv->CreateHingeConstraint( info.pObjects[0], info.pObjects[1], pGroup, m_hinge ); } void DrawDebugGeometryOverlays() { if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) { NDebugOverlay::Line(m_hinge.worldPosition, m_hinge.worldPosition + 48 * m_hinge.worldAxisDirection, 0, 255, 0, false, 0 ); } BaseClass::DrawDebugGeometryOverlays(); } void InputSetVelocity( inputdata_t &inputdata ) { if ( !m_pConstraint || !m_pConstraint->GetReferenceObject() || !m_pConstraint->GetAttachedObject() ) return; float speed = inputdata.value.Float(); float massLoad = 1; int numMasses = 0; if ( m_pConstraint->GetReferenceObject()->IsMoveable() ) { massLoad = m_pConstraint->GetReferenceObject()->GetInertia().Length(); numMasses++; m_pConstraint->GetReferenceObject()->Wake(); } if ( m_pConstraint->GetAttachedObject()->IsMoveable() ) { massLoad += m_pConstraint->GetAttachedObject()->GetInertia().Length(); numMasses++; m_pConstraint->GetAttachedObject()->Wake(); } if ( numMasses > 0 ) { massLoad /= (float)numMasses; } float loadscale = m_systemLoadScale != 0 ? m_systemLoadScale : 1; m_pConstraint->SetAngularMotor( speed, speed * loadscale * massLoad * loadscale * (1.0/TICK_INTERVAL) ); } void InputSetHingeFriction( inputdata_t &inputdata ) { m_hingeFriction = inputdata.value.Float(); Msg("Setting hinge friction to %f\n", m_hingeFriction ); m_hinge.hingeAxis.SetAxisFriction( 0, 0, m_hingeFriction * 1000 ); } virtual void Deactivate() { if ( HasSpawnFlags( SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY ) ) { if ( m_pConstraint && m_pConstraint->GetAttachedObject() ) { // NOTE: RemoveHinged() is always safe m_pConstraint->GetAttachedObject()->RemoveHinged(); } } BaseClass::Deactivate(); } void NotifyVPhysicsStateChanged( IPhysicsObject *pPhysics, CBaseEntity *pEntity, bool bAwake ) { #if HINGE_NOTIFY Assert(m_pConstraint); if (!m_pConstraint) return; // if something woke up, start thinking. If everything is asleep, stop thinking. if ( bAwake ) { // Did something wake up when I was not thinking? if ( GetNextThink() == TICK_NEVER_THINK ) { m_soundInfo.StartThinking(this, VelocitySampler::GetRelativeAngularVelocity(m_pConstraint->GetAttachedObject(), m_pConstraint->GetReferenceObject()) , m_hinge.worldAxisDirection ); SetThink(&CPhysHinge::SoundThink); SetNextThink(gpGlobals->curtime + m_soundInfo.getThinkRate()); } } else { // Is everything asleep? If so, stop thinking. if ( GetNextThink() != TICK_NEVER_THINK && m_pConstraint->GetAttachedObject()->IsAsleep() && m_pConstraint->GetReferenceObject()->IsAsleep() ) { m_soundInfo.StopThinking(this); SetNextThink(TICK_NEVER_THINK); } } #endif } #if HINGE_NOTIFY virtual void OnConstraintSetup( hl_constraint_info_t &info ) { CBaseEntity *pEntity0 = info.pObjects[0] ? static_cast(info.pObjects[0]->GetGameData()) : NULL; if ( pEntity0 && !info.pObjects[0]->IsStatic() ) { WatchVPhysicsStateChanges( this, pEntity0 ); } CBaseEntity *pEntity1 = info.pObjects[1] ? static_cast(info.pObjects[1]->GetGameData()) : NULL; if ( pEntity1 && !info.pObjects[1]->IsStatic() ) { WatchVPhysicsStateChanges( this, pEntity1 ); } BaseClass::OnConstraintSetup(info); } void SoundThink( void ); // void Spawn( void ); void Activate( void ); void Precache( void ); #endif DECLARE_DATADESC(); #if HINGE_NOTIFY protected: ConstraintSoundInfo m_soundInfo; #endif private: constraint_hingeparams_t m_hinge; float m_hingeFriction; float m_systemLoadScale; bool IsWorldHinge( const hl_constraint_info_t &info, int *pAxisOut ); }; BEGIN_DATADESC( CPhysHinge ) // Quiet down classcheck // DEFINE_FIELD( m_hinge, FIELD_??? ), DEFINE_KEYFIELD( m_hingeFriction, FIELD_FLOAT, "hingefriction" ), DEFINE_FIELD( m_hinge.worldPosition, FIELD_POSITION_VECTOR ), DEFINE_KEYFIELD( m_hinge.worldAxisDirection, FIELD_VECTOR, "hingeaxis" ), DEFINE_KEYFIELD( m_systemLoadScale, FIELD_FLOAT, "systemloadscale" ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetAngularVelocity", InputSetVelocity ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetHingeFriction", InputSetHingeFriction ), #if HINGE_NOTIFY DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_keyPoints[SimpleConstraintSoundProfile::kMIN_THRESHOLD] , FIELD_FLOAT, "minSoundThreshold" ), DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_keyPoints[SimpleConstraintSoundProfile::kMIN_FULL] , FIELD_FLOAT, "maxSoundThreshold" ), DEFINE_KEYFIELD( m_soundInfo.m_iszTravelSoundFwd, FIELD_SOUNDNAME, "slidesoundfwd" ), DEFINE_KEYFIELD( m_soundInfo.m_iszTravelSoundBack, FIELD_SOUNDNAME, "slidesoundback" ), DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[0], FIELD_SOUNDNAME, "reversalsoundSmall" ), DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[1], FIELD_SOUNDNAME, "reversalsoundMedium" ), DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[2], FIELD_SOUNDNAME, "reversalsoundLarge" ), DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[0] , FIELD_FLOAT, "reversalsoundthresholdSmall" ), DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[1], FIELD_FLOAT, "reversalsoundthresholdMedium" ), DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[2] , FIELD_FLOAT, "reversalsoundthresholdLarge" ), DEFINE_THINKFUNC( SoundThink ), #endif END_DATADESC() LINK_ENTITY_TO_CLASS( phys_hinge, CPhysHinge ); void CPhysHinge::Spawn( void ) { m_hinge.worldPosition = GetLocalOrigin(); m_hinge.worldAxisDirection -= GetLocalOrigin(); VectorNormalize(m_hinge.worldAxisDirection); UTIL_SnapDirectionToAxis( m_hinge.worldAxisDirection ); m_hinge.hingeAxis.SetAxisFriction( 0, 0, 0 ); if ( HasSpawnFlags( SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY ) ) { masscenteroverride_t params; if ( m_nameAttach1 == NULL_STRING ) { params.SnapToAxis( m_nameAttach2, m_hinge.worldPosition, m_hinge.worldAxisDirection ); PhysSetMassCenterOverride( params ); } else if ( m_nameAttach2 == NULL_STRING ) { params.SnapToAxis( m_nameAttach1, m_hinge.worldPosition, m_hinge.worldAxisDirection ); PhysSetMassCenterOverride( params ); } else { RemoveSpawnFlags( SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY ); } } Precache(); } #if HINGE_NOTIFY void CPhysHinge::Activate( void ) { BaseClass::Activate(); m_soundInfo.OnActivate(this); if (m_pConstraint) { m_soundInfo.StartThinking(this, VelocitySampler::GetRelativeAngularVelocity(m_pConstraint->GetAttachedObject(), m_pConstraint->GetReferenceObject()) , m_hinge.worldAxisDirection ); SetThink(&CPhysHinge::SoundThink); SetNextThink( gpGlobals->curtime + m_soundInfo.getThinkRate() ); } } void CPhysHinge::Precache( void ) { BaseClass::Precache(); return m_soundInfo.OnPrecache(this); } #endif static int GetUnitAxisIndex( const Vector &axis ) { bool valid = false; int index = -1; for ( int i = 0; i < 3; i++ ) { if ( axis[i] != 0 ) { if ( fabs(axis[i]) == 1 ) { if ( index < 0 ) { index = i; valid = true; continue; } } valid = false; } } return valid ? index : -1; } bool CPhysHinge::IsWorldHinge( const hl_constraint_info_t &info, int *pAxisOut ) { if ( HasSpawnFlags( SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY ) && info.pObjects[0] == g_PhysWorldObject ) { Vector localHinge; info.pObjects[1]->WorldToLocalVector( &localHinge, m_hinge.worldAxisDirection ); UTIL_SnapDirectionToAxis( localHinge ); int hingeAxis = GetUnitAxisIndex( localHinge ); if ( hingeAxis >= 0 ) { *pAxisOut = hingeAxis; return true; } } return false; } #if HINGE_NOTIFY void CPhysHinge::SoundThink( void ) { Assert(m_pConstraint); if (!m_pConstraint) return; IPhysicsObject * pAttached = m_pConstraint->GetAttachedObject(), *pReference = m_pConstraint->GetReferenceObject(); Assert( pAttached && pReference ); if (pAttached && pReference) { Vector relativeVel = VelocitySampler::GetRelativeAngularVelocity(pAttached,pReference); if (g_debug_constraint_sounds.GetBool()) { NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + (relativeVel), 255, 255, 0, true, 0.1f ); } m_soundInfo.OnThink( this, relativeVel ); SetNextThink(gpGlobals->curtime + m_soundInfo.getThinkRate()); } } #endif class CPhysBallSocket : public CPhysConstraint { public: DECLARE_CLASS( CPhysBallSocket, CPhysConstraint ); IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) { constraint_ballsocketparams_t ballsocket; ballsocket.Defaults(); for ( int i = 0; i < 2; i++ ) { info.pObjects[i]->WorldToLocal( &ballsocket.constraintPosition[i], GetAbsOrigin() ); } GetBreakParams( ballsocket.constraint, info ); ballsocket.constraint.torqueLimit = 0; return physenv->CreateBallsocketConstraint( info.pObjects[0], info.pObjects[1], pGroup, ballsocket ); } }; LINK_ENTITY_TO_CLASS( phys_ballsocket, CPhysBallSocket ); class CPhysSlideConstraint : public CPhysConstraint, public IVPhysicsWatcher { public: DECLARE_CLASS( CPhysSlideConstraint, CPhysConstraint ); DECLARE_DATADESC(); IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ); void InputSetVelocity( inputdata_t &inputdata ) { if ( !m_pConstraint || !m_pConstraint->GetReferenceObject() || !m_pConstraint->GetAttachedObject() ) return; float speed = inputdata.value.Float(); float massLoad = 1; int numMasses = 0; if ( m_pConstraint->GetReferenceObject()->IsMoveable() ) { massLoad = m_pConstraint->GetReferenceObject()->GetMass(); numMasses++; m_pConstraint->GetReferenceObject()->Wake(); } if ( m_pConstraint->GetAttachedObject()->IsMoveable() ) { massLoad += m_pConstraint->GetAttachedObject()->GetMass(); numMasses++; m_pConstraint->GetAttachedObject()->Wake(); } if ( numMasses > 0 ) { massLoad /= (float)numMasses; } float loadscale = m_systemLoadScale != 0 ? m_systemLoadScale : 1; m_pConstraint->SetLinearMotor( speed, speed * loadscale * massLoad * (1.0/TICK_INTERVAL) ); } void DrawDebugGeometryOverlays() { if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) { NDebugOverlay::Box( GetAbsOrigin(), -Vector(8,8,8), Vector(8,8,8), 0, 255, 0, 0, 0 ); NDebugOverlay::Box( m_axisEnd, -Vector(4,4,4), Vector(4,4,4), 0, 0, 255, 0, 0 ); NDebugOverlay::Line( GetAbsOrigin(), m_axisEnd, 255, 255, 0, false, 0 ); } BaseClass::DrawDebugGeometryOverlays(); } void NotifyVPhysicsStateChanged( IPhysicsObject *pPhysics, CBaseEntity *pEntity, bool bAwake ) { #if HINGE_NOTIFY Assert(m_pConstraint); if (!m_pConstraint) return; // if something woke up, start thinking. If everything is asleep, stop thinking. if ( bAwake ) { // Did something wake up when I was not thinking? if ( GetNextThink() == TICK_NEVER_THINK ) { Vector axisDirection = m_axisEnd - GetAbsOrigin(); VectorNormalize( axisDirection ); UTIL_SnapDirectionToAxis( axisDirection ); m_soundInfo.StartThinking(this, VelocitySampler::GetRelativeVelocity(m_pConstraint->GetAttachedObject(), m_pConstraint->GetReferenceObject()), axisDirection ); SetThink(&CPhysSlideConstraint::SoundThink); SetNextThink(gpGlobals->curtime + m_soundInfo.getThinkRate()); } } else { // Is everything asleep? If so, stop thinking. if ( GetNextThink() != TICK_NEVER_THINK && m_pConstraint->GetAttachedObject()->IsAsleep() && m_pConstraint->GetReferenceObject()->IsAsleep() ) { m_soundInfo.StopThinking(this); SetNextThink(TICK_NEVER_THINK); } } #endif } #if HINGE_NOTIFY virtual void OnConstraintSetup( hl_constraint_info_t &info ) { CBaseEntity *pEntity0 = info.pObjects[0] ? static_cast(info.pObjects[0]->GetGameData()) : NULL; if ( pEntity0 && !info.pObjects[0]->IsStatic() ) { WatchVPhysicsStateChanges( this, pEntity0 ); } CBaseEntity *pEntity1 = info.pObjects[1] ? static_cast(info.pObjects[1]->GetGameData()) : NULL; if ( pEntity1 && !info.pObjects[1]->IsStatic() ) { WatchVPhysicsStateChanges( this, pEntity1 ); } BaseClass::OnConstraintSetup(info); } void SoundThink( void ); // void Spawn( void ); void Activate( void ); void Precache( void ); #endif Vector m_axisEnd; float m_slideFriction; float m_systemLoadScale; #if HINGE_NOTIFY protected: ConstraintSoundInfo m_soundInfo; #endif }; LINK_ENTITY_TO_CLASS( phys_slideconstraint, CPhysSlideConstraint ); BEGIN_DATADESC( CPhysSlideConstraint ) DEFINE_KEYFIELD( m_axisEnd, FIELD_POSITION_VECTOR, "slideaxis" ), DEFINE_KEYFIELD( m_slideFriction, FIELD_FLOAT, "slidefriction" ), DEFINE_KEYFIELD( m_systemLoadScale, FIELD_FLOAT, "systemloadscale" ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetVelocity", InputSetVelocity ), #if HINGE_NOTIFY DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_keyPoints[SimpleConstraintSoundProfile::kMIN_THRESHOLD] , FIELD_FLOAT, "minSoundThreshold" ), DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_keyPoints[SimpleConstraintSoundProfile::kMIN_FULL] , FIELD_FLOAT, "maxSoundThreshold" ), DEFINE_KEYFIELD( m_soundInfo.m_iszTravelSoundFwd, FIELD_SOUNDNAME, "slidesoundfwd" ), DEFINE_KEYFIELD( m_soundInfo.m_iszTravelSoundBack, FIELD_SOUNDNAME, "slidesoundback" ), DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[0], FIELD_SOUNDNAME, "reversalsoundSmall" ), DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[1], FIELD_SOUNDNAME, "reversalsoundMedium" ), DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[2], FIELD_SOUNDNAME, "reversalsoundLarge" ), DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[0] , FIELD_FLOAT, "reversalsoundthresholdSmall" ), DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[1], FIELD_FLOAT, "reversalsoundthresholdMedium" ), DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[2] , FIELD_FLOAT, "reversalsoundthresholdLarge" ), DEFINE_THINKFUNC( SoundThink ), #endif END_DATADESC() IPhysicsConstraint *CPhysSlideConstraint::CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) { constraint_slidingparams_t sliding; sliding.Defaults(); GetBreakParams( sliding.constraint, info ); sliding.constraint.strength = 1.0; Vector axisDirection = m_axisEnd - GetAbsOrigin(); VectorNormalize( axisDirection ); UTIL_SnapDirectionToAxis( axisDirection ); sliding.InitWithCurrentObjectState( info.pObjects[0], info.pObjects[1], axisDirection ); sliding.friction = m_slideFriction; if ( m_spawnflags & SF_SLIDE_LIMIT_ENDS ) { Vector position; info.pObjects[1]->GetPosition( &position, NULL ); sliding.limitMin = DotProduct( axisDirection, GetAbsOrigin() ); sliding.limitMax = DotProduct( axisDirection, m_axisEnd ); if ( sliding.limitMax < sliding.limitMin ) { ::V_swap( sliding.limitMin, sliding.limitMax ); } // expand limits to make initial position of the attached object valid float limit = DotProduct( position, axisDirection ); if ( limit < sliding.limitMin ) { sliding.limitMin = limit; } else if ( limit > sliding.limitMax ) { sliding.limitMax = limit; } // offset so that the current position is 0 sliding.limitMin -= limit; sliding.limitMax -= limit; } return physenv->CreateSlidingConstraint( info.pObjects[0], info.pObjects[1], pGroup, sliding ); } #if HINGE_NOTIFY void CPhysSlideConstraint::SoundThink( void ) { Assert(m_pConstraint); if (!m_pConstraint) return; IPhysicsObject * pAttached = m_pConstraint->GetAttachedObject(), *pReference = m_pConstraint->GetReferenceObject(); Assert( pAttached && pReference ); if (pAttached && pReference) { Vector relativeVel = VelocitySampler::GetRelativeVelocity(pAttached,pReference); // project velocity onto my primary axis.: Vector axisDirection = m_axisEnd - GetAbsOrigin(); relativeVel = m_axisEnd * relativeVel.Dot(m_axisEnd)/m_axisEnd.Dot(m_axisEnd); m_soundInfo.OnThink( this, relativeVel ); SetNextThink(gpGlobals->curtime + m_soundInfo.getThinkRate()); } } void CPhysSlideConstraint::Activate( void ) { BaseClass::Activate(); m_soundInfo.OnActivate(this); Vector axisDirection = m_axisEnd - GetAbsOrigin(); VectorNormalize( axisDirection ); UTIL_SnapDirectionToAxis( axisDirection ); m_soundInfo.StartThinking(this, VelocitySampler::GetRelativeVelocity(m_pConstraint->GetAttachedObject(), m_pConstraint->GetReferenceObject()), axisDirection ); SetThink(&CPhysSlideConstraint::SoundThink); SetNextThink(gpGlobals->curtime + m_soundInfo.getThinkRate()); } void CPhysSlideConstraint::Precache() { m_soundInfo.OnPrecache(this); } #endif LINK_ENTITY_TO_CLASS( phys_constraint, CPhysFixed ); //----------------------------------------------------------------------------- // Purpose: Activate/create the constraint //----------------------------------------------------------------------------- IPhysicsConstraint *CPhysFixed::CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) { constraint_fixedparams_t fixed; fixed.Defaults(); fixed.InitWithCurrentObjectState( info.pObjects[0], info.pObjects[1] ); GetBreakParams( fixed.constraint, info ); // constraining to the world means object 1 is fixed if ( info.pObjects[0] == g_PhysWorldObject ) { PhysSetGameFlags( info.pObjects[1], FVPHYSICS_CONSTRAINT_STATIC ); } return physenv->CreateFixedConstraint( info.pObjects[0], info.pObjects[1], pGroup, fixed ); } //----------------------------------------------------------------------------- // Purpose: Breakable pulley w/ropes constraint //----------------------------------------------------------------------------- class CPhysPulley : public CPhysConstraint { DECLARE_CLASS( CPhysPulley, CPhysConstraint ); public: DECLARE_DATADESC(); void DrawDebugGeometryOverlays() { if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) { Vector origin = GetAbsOrigin(); Vector refPos = origin, attachPos = origin; IPhysicsObject *pRef = m_pConstraint->GetReferenceObject(); if ( pRef ) { matrix3x4_t matrix; pRef->GetPositionMatrix( &matrix ); VectorTransform( m_offset[0], matrix, refPos ); } IPhysicsObject *pAttach = m_pConstraint->GetAttachedObject(); if ( pAttach ) { matrix3x4_t matrix; pAttach->GetPositionMatrix( &matrix ); VectorTransform( m_offset[1], matrix, attachPos ); } NDebugOverlay::Line( refPos, origin, 0, 255, 0, false, 0 ); NDebugOverlay::Line( origin, m_position2, 128, 128, 128, false, 0 ); NDebugOverlay::Line( m_position2, attachPos, 0, 255, 0, false, 0 ); NDebugOverlay::Box( origin, -Vector(8,8,8), Vector(8,8,8), 128, 255, 128, 32, 0 ); NDebugOverlay::Box( m_position2, -Vector(8,8,8), Vector(8,8,8), 255, 128, 128, 32, 0 ); } BaseClass::DrawDebugGeometryOverlays(); } IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ); private: Vector m_position2; Vector m_offset[2]; float m_addLength; float m_gearRatio; }; BEGIN_DATADESC( CPhysPulley ) DEFINE_KEYFIELD( m_position2, FIELD_POSITION_VECTOR, "position2" ), DEFINE_AUTO_ARRAY( m_offset, FIELD_VECTOR ), DEFINE_KEYFIELD( m_addLength, FIELD_FLOAT, "addlength" ), DEFINE_KEYFIELD( m_gearRatio, FIELD_FLOAT, "gearratio" ), END_DATADESC() LINK_ENTITY_TO_CLASS( phys_pulleyconstraint, CPhysPulley ); //----------------------------------------------------------------------------- // Purpose: Activate/create the constraint //----------------------------------------------------------------------------- IPhysicsConstraint *CPhysPulley::CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) { constraint_pulleyparams_t pulley; pulley.Defaults(); pulley.pulleyPosition[0] = GetAbsOrigin(); pulley.pulleyPosition[1] = m_position2; matrix3x4_t matrix; Vector world[2]; info.pObjects[0]->GetPositionMatrix( &matrix ); VectorTransform( info.anchorPosition[0], matrix, world[0] ); info.pObjects[1]->GetPositionMatrix( &matrix ); VectorTransform( info.anchorPosition[1], matrix, world[1] ); for ( int i = 0; i < 2; i++ ) { pulley.objectPosition[i] = info.anchorPosition[i]; m_offset[i] = info.anchorPosition[i]; } pulley.totalLength = m_addLength + (world[0] - pulley.pulleyPosition[0]).Length() + ((world[1] - pulley.pulleyPosition[1]).Length() * m_gearRatio); if ( m_gearRatio != 0 ) { pulley.gearRatio = m_gearRatio; } GetBreakParams( pulley.constraint, info ); if ( m_spawnflags & SF_PULLEY_RIGID ) { pulley.isRigid = true; } return physenv->CreatePulleyConstraint( info.pObjects[0], info.pObjects[1], pGroup, pulley ); } //----------------------------------------------------------------------------- // Purpose: Breakable rope/length constraint //----------------------------------------------------------------------------- class CPhysLength : public CPhysConstraint { DECLARE_CLASS( CPhysLength, CPhysConstraint ); public: DECLARE_DATADESC(); void DrawDebugGeometryOverlays() { if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) { Vector origin = GetAbsOrigin(); Vector refPos = origin, attachPos = origin; IPhysicsObject *pRef = m_pConstraint->GetReferenceObject(); if ( pRef ) { matrix3x4_t matrix; pRef->GetPositionMatrix( &matrix ); VectorTransform( m_offset[0], matrix, refPos ); } IPhysicsObject *pAttach = m_pConstraint->GetAttachedObject(); if ( pAttach ) { matrix3x4_t matrix; pAttach->GetPositionMatrix( &matrix ); VectorTransform( m_offset[1], matrix, attachPos ); } Vector dir = attachPos - refPos; float len = VectorNormalize(dir); if ( len > m_totalLength ) { Vector mid = refPos + dir * m_totalLength; NDebugOverlay::Line( refPos, mid, 0, 255, 0, false, 0 ); NDebugOverlay::Line( mid, attachPos, 255, 0, 0, false, 0 ); } else { NDebugOverlay::Line( refPos, attachPos, 0, 255, 0, false, 0 ); } } BaseClass::DrawDebugGeometryOverlays(); } IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ); private: Vector m_offset[2]; Vector m_vecAttach; float m_addLength; float m_minLength; float m_totalLength; }; BEGIN_DATADESC( CPhysLength ) DEFINE_AUTO_ARRAY( m_offset, FIELD_VECTOR ), DEFINE_KEYFIELD( m_addLength, FIELD_FLOAT, "addlength" ), DEFINE_KEYFIELD( m_minLength, FIELD_FLOAT, "minlength" ), DEFINE_KEYFIELD( m_vecAttach, FIELD_POSITION_VECTOR, "attachpoint" ), DEFINE_FIELD( m_totalLength, FIELD_FLOAT ), END_DATADESC() LINK_ENTITY_TO_CLASS( phys_lengthconstraint, CPhysLength ); //----------------------------------------------------------------------------- // Purpose: Activate/create the constraint //----------------------------------------------------------------------------- IPhysicsConstraint *CPhysLength::CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) { constraint_lengthparams_t length; length.Defaults(); Vector position[2]; position[0] = GetAbsOrigin(); position[1] = m_vecAttach; int index = info.swapped ? 1 : 0; length.InitWorldspace( info.pObjects[0], info.pObjects[1], position[index], position[!index] ); length.totalLength += m_addLength; length.minLength = m_minLength; m_totalLength = length.totalLength; if ( HasSpawnFlags(SF_LENGTH_RIGID) ) { length.minLength = length.totalLength; } for ( int i = 0; i < 2; i++ ) { m_offset[i] = length.objectPosition[i]; } GetBreakParams( length.constraint, info ); return physenv->CreateLengthConstraint( info.pObjects[0], info.pObjects[1], pGroup, length ); } //----------------------------------------------------------------------------- // Purpose: Limited ballsocket constraint with toggle-able translation constraints //----------------------------------------------------------------------------- class CRagdollConstraint : public CPhysConstraint { DECLARE_CLASS( CRagdollConstraint, CPhysConstraint ); public: DECLARE_DATADESC(); #if 0 void DrawDebugGeometryOverlays() { if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) { NDebugOverlay::Line( refPos, attachPos, 0, 255, 0, false, 0 ); } BaseClass::DrawDebugGeometryOverlays(); } #endif IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ); private: float m_xmin; // constraint limits in degrees float m_xmax; float m_ymin; float m_ymax; float m_zmin; float m_zmax; float m_xfriction; float m_yfriction; float m_zfriction; }; BEGIN_DATADESC( CRagdollConstraint ) DEFINE_KEYFIELD( m_xmin, FIELD_FLOAT, "xmin" ), DEFINE_KEYFIELD( m_xmax, FIELD_FLOAT, "xmax" ), DEFINE_KEYFIELD( m_ymin, FIELD_FLOAT, "ymin" ), DEFINE_KEYFIELD( m_ymax, FIELD_FLOAT, "ymax" ), DEFINE_KEYFIELD( m_zmin, FIELD_FLOAT, "zmin" ), DEFINE_KEYFIELD( m_zmax, FIELD_FLOAT, "zmax" ), DEFINE_KEYFIELD( m_xfriction, FIELD_FLOAT, "xfriction" ), DEFINE_KEYFIELD( m_yfriction, FIELD_FLOAT, "yfriction" ), DEFINE_KEYFIELD( m_zfriction, FIELD_FLOAT, "zfriction" ), END_DATADESC() LINK_ENTITY_TO_CLASS( phys_ragdollconstraint, CRagdollConstraint ); //----------------------------------------------------------------------------- // Purpose: Activate/create the constraint //----------------------------------------------------------------------------- IPhysicsConstraint *CRagdollConstraint::CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) { constraint_ragdollparams_t ragdoll; ragdoll.Defaults(); matrix3x4_t entityToWorld, worldToEntity; info.pObjects[0]->GetPositionMatrix( &entityToWorld ); MatrixInvert( entityToWorld, worldToEntity ); ConcatTransforms( worldToEntity, EntityToWorldTransform(), ragdoll.constraintToReference ); info.pObjects[1]->GetPositionMatrix( &entityToWorld ); MatrixInvert( entityToWorld, worldToEntity ); ConcatTransforms( worldToEntity, EntityToWorldTransform(), ragdoll.constraintToAttached ); ragdoll.onlyAngularLimits = HasSpawnFlags( SF_RAGDOLL_FREEMOVEMENT ) ? true : false; // FIXME: Why are these friction numbers in different units from what the hinge uses? ragdoll.axes[0].SetAxisFriction( m_xmin, m_xmax, m_xfriction ); ragdoll.axes[1].SetAxisFriction( m_ymin, m_ymax, m_yfriction ); ragdoll.axes[2].SetAxisFriction( m_zmin, m_zmax, m_zfriction ); if ( HasSpawnFlags( SF_CONSTRAINT_START_INACTIVE ) ) { ragdoll.isActive = false; } return physenv->CreateRagdollConstraint( info.pObjects[0], info.pObjects[1], pGroup, ragdoll ); } class CPhysConstraintEvents : public IPhysicsConstraintEvent { void ConstraintBroken( IPhysicsConstraint *pConstraint ) { CBaseEntity *pEntity = (CBaseEntity *)pConstraint->GetGameData(); if ( pEntity ) { IPhysicsConstraintEvent *pConstraintEvent = dynamic_cast( pEntity ); //Msg("Constraint broken %s\n", pEntity->GetDebugName() ); if ( pConstraintEvent ) { pConstraintEvent->ConstraintBroken( pConstraint ); } else { variant_t emptyVariant; pEntity->AcceptInput( "ConstraintBroken", NULL, NULL, emptyVariant, 0 ); } } } }; static CPhysConstraintEvents constraintevents; // registered in physics.cpp IPhysicsConstraintEvent *g_pConstraintEvents = &constraintevents; #if HINGE_NOTIFY //----------------------------------------------------------------------------- // Code for sampler //----------------------------------------------------------------------------- /// Call this in spawn(). (Not a constructor because those are difficult to use in entities.) void VelocitySampler::Initialize(float samplerate) { m_fIdealSampleRate = samplerate; } // This is an old style approach to reversal sounds, from when there was only one. #if 0 bool VelocitySampler::HasReversed(const Vector &relativeVelocity, float thresholdAcceleration) { // first, make sure the velocity has reversed (is more than 90deg off) from last time, or is zero now. // float rVsq = relativeVelocity.LengthSqr(); float vDot = relativeVelocity.Dot(m_prevSample); if (vDot <= 0) // there is a reversal in direction. compute the magnitude of acceleration. { // find the scalar projection of the relative acceleration this fame onto the previous frame's // velocity, and compare that to the threshold. Vector accel = relativeVelocity - m_prevSample; float prevSampleLength = m_prevSample.Length(); float projection = 0; // divide through by dt to get the accel per sec if (prevSampleLength) { projection = -(accel.Dot(m_prevSample) / prevSampleLength) / (gpGlobals->curtime - m_fPrevSampleTime); } else { projection = accel.Length() / (gpGlobals->curtime - m_fPrevSampleTime); } if (g_debug_constraint_sounds.GetBool()) { Msg("Reversal accel is %f/%f\n",projection,thresholdAcceleration); } return ((projection) > thresholdAcceleration); // the scalar projection is negative because the acceleration is against vel } else { return false; } } #endif /// Looks at the force of reversal and compares it to a ladder of thresholds. /// Returns the index of the highest threshold exceeded by the reversal velocity. int VelocitySampler::HasReversed(const Vector &relativeVelocity, const float thresholdAcceleration[], const unsigned short numThresholds) { // first, make sure the velocity has reversed (is more than 90deg off) from last time, or is zero now. // float rVsq = relativeVelocity.LengthSqr(); float vDot = relativeVelocity.Dot(m_prevSample); if (vDot <= 0) // there is a reversal in direction. compute the magnitude of acceleration. { // find the scalar projection of the relative acceleration this fame onto the previous frame's // velocity, and compare that to the threshold. Vector accel = relativeVelocity - m_prevSample; float prevSampleLength = m_prevSample.Length(); float projection = 0; // divide through by dt to get the accel per sec if (prevSampleLength) { // the scalar projection is negative because the acceleration is against vel projection = -(accel.Dot(m_prevSample) / prevSampleLength) / (gpGlobals->curtime - m_fPrevSampleTime); } else { projection = accel.Length() / (gpGlobals->curtime - m_fPrevSampleTime); } if (g_debug_constraint_sounds.GetBool()) { Msg("Reversal accel is %f/%f\n", projection, thresholdAcceleration[0]); } // now find the threshold crossed. int retval; for (retval = numThresholds - 1; retval >= 0 ; --retval) { if (projection > thresholdAcceleration[retval]) break; } return retval; } else { return -1; } } /// small helper function used just below (technique copy-pasted from sound.cpp) inline static bool IsEmpty (const string_t &str) { return (!str || strlen(str.ToCStr()) < 1 ); } void ConstraintSoundInfo::OnActivate( CPhysConstraint *pOuter ) { m_pTravelSound = NULL; m_vSampler.Initialize( getThinkRate() ); ValidateInternals( pOuter ); // make sure sound filenames are not empty m_bPlayTravelSound = !IsEmpty(m_iszTravelSoundFwd) || !IsEmpty(m_iszTravelSoundBack); m_bPlayReversalSound = false; for (int i = 0; i < SimpleConstraintSoundProfile::kREVERSAL_SOUND_ARRAY_SIZE ; ++i) { if ( !IsEmpty(m_iszReversalSounds[i]) ) { // if there is at least one filled sound field, we should try // to play reversals m_bPlayReversalSound = true; break; } } /* SetThink(&CPhysSlideConstraint::SoundThink); SetNextThink(gpGlobals->curtime + m_vSampler.getSampleRate()); */ } /// Maintain consistency of internal datastructures on start void ConstraintSoundInfo::ValidateInternals( CPhysConstraint *pOuter ) { // Make sure the reversal sound thresholds are strictly increasing. for (int i = 1 ; i < SimpleConstraintSoundProfile::kREVERSAL_SOUND_ARRAY_SIZE ; ++i) { // if decreases from small to medium, promote small to medium and warn. if (m_soundProfile.m_reversalSoundThresholds[i] < m_soundProfile.m_reversalSoundThresholds[i-1]) { Warning("Constraint reversal sounds for %s are out of order!", pOuter->GetDebugName() ); m_soundProfile.m_reversalSoundThresholds[i] = m_soundProfile.m_reversalSoundThresholds[i-1]; m_iszReversalSounds[i] = m_iszReversalSounds[i-1]; } } } void ConstraintSoundInfo::OnPrecache( CPhysConstraint *pOuter ) { pOuter->PrecacheScriptSound( m_iszTravelSoundFwd.ToCStr() ); pOuter->PrecacheScriptSound( m_iszTravelSoundBack.ToCStr() ); for (int i = 0 ; i < SimpleConstraintSoundProfile::kREVERSAL_SOUND_ARRAY_SIZE; ++i ) { pOuter->PrecacheScriptSound( m_iszReversalSounds[i].ToCStr() ); } } void ConstraintSoundInfo::OnThink( CPhysConstraint *pOuter, const Vector &relativeVelocity ) { // have we had a hard reversal? int playReversal = m_vSampler.HasReversed( relativeVelocity, m_soundProfile.m_reversalSoundThresholds, SimpleConstraintSoundProfile::kREVERSAL_SOUND_ARRAY_SIZE ); float relativeVelMag = relativeVelocity.Length(); //< magnitude of relative velocity CBaseEntity *pChildEntity = static_cast(pOuter->GetPhysConstraint()->GetAttachedObject()->GetGameData()); // compute sound level float soundVol = this->m_soundProfile.GetVolume(relativeVelMag); if (g_debug_constraint_sounds.GetBool()) { char tempstr[512]; Q_snprintf(tempstr,sizeof(tempstr),"Velocity: %.3f", relativeVelMag ); pChildEntity->EntityText( 0, tempstr, m_vSampler.getSampleRate() ); Q_snprintf(tempstr,sizeof(tempstr),"Sound volume: %.3f", soundVol ); pChildEntity->EntityText( 1, tempstr, m_vSampler.getSampleRate() ); if (playReversal >= 0) { Q_snprintf(tempstr,sizeof(tempstr),"Reversal [%d]", playReversal ); pChildEntity->EntityText(2,tempstr,m_vSampler.getSampleRate()); } } // if we loaded a travel sound if (m_bPlayTravelSound) { if (soundVol > 0) { // if we want to play a sound... if ( m_pTravelSound ) { // if a sound exists, modify it CSoundEnvelopeController::GetController().SoundChangeVolume( m_pTravelSound, soundVol, 0.1f ); } else { // if a sound does not exist, create it bool travellingForward = relativeVelocity.Dot(m_forwardAxis) > 0; CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); CPASAttenuationFilter filter( pChildEntity ); m_pTravelSound = controller.SoundCreate( filter, pChildEntity->entindex(), (travellingForward ? m_iszTravelSoundFwd : m_iszTravelSoundBack).ToCStr() ); controller.Play( m_pTravelSound, soundVol, 100 ); } } else { // if we want to not play sound if ( m_pTravelSound ) { // and it exists, kill it CSoundEnvelopeController::GetController().SoundDestroy( m_pTravelSound ); m_pTravelSound = NULL; } } } if (m_bPlayReversalSound && (playReversal >= 0)) { pChildEntity->EmitSound(m_iszReversalSounds[playReversal].ToCStr()); } m_vSampler.AddSample( relativeVelocity ); } void ConstraintSoundInfo::StartThinking( CPhysConstraint *pOuter, const Vector &relativeVelocity, const Vector &forwardVector ) { m_forwardAxis = forwardVector; m_vSampler.BeginSampling( relativeVelocity ); /* IPhysicsConstraint *pConstraint = pOuter->GetPhysConstraint(); Assert(pConstraint); if (pConstraint) { IPhysicsObject * pAttached = pConstraint->GetAttachedObject(), *pReference = pConstraint->GetReferenceObject(); m_vSampler.BeginSampling( VelocitySampler::GetRelativeVelocity(pAttached,pReference) ); } */ } void ConstraintSoundInfo::StopThinking( CPhysConstraint *pOuter ) { DeleteAllSounds(); } ConstraintSoundInfo::~ConstraintSoundInfo() { DeleteAllSounds(); } // Any sounds envelopes that are active, kill. void ConstraintSoundInfo::DeleteAllSounds() { if ( m_pTravelSound ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pTravelSound ); m_pTravelSound = NULL; } } #endif