//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Implements a screen shake effect that can also shake physics objects. // // NOTE: UTIL_ScreenShake() will only shake players who are on the ground // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "shake.h" #include "physics_saverestore.h" #include "rope.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" class CPhysicsShake : public IMotionEvent { DECLARE_SIMPLE_DATADESC(); public: virtual simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) { Vector contact; if ( !pObject->GetContactPoint( &contact, NULL ) ) return SIM_NOTHING; // fudge the force a bit to make it more dramatic pObject->CalculateForceOffset( m_force * (1.0f + pObject->GetMass()*0.4f), contact, &linear, &angular ); return SIM_LOCAL_FORCE; } Vector m_force; }; BEGIN_SIMPLE_DATADESC( CPhysicsShake ) DEFINE_FIELD( m_force, FIELD_VECTOR ), END_DATADESC() class CEnvShake : public CPointEntity { private: float m_Amplitude; float m_Frequency; float m_Duration; float m_Radius; // radius of 0 means all players float m_stopTime; float m_nextShake; float m_currentAmp; Vector m_maxForce; IPhysicsMotionController *m_pShakeController; CPhysicsShake m_shakeCallback; DECLARE_DATADESC(); public: DECLARE_CLASS( CEnvShake, CPointEntity ); ~CEnvShake( void ); virtual void Spawn( void ); virtual void OnRestore( void ); inline float Amplitude( void ) { return m_Amplitude; } inline float Frequency( void ) { return m_Frequency; } inline float Duration( void ) { return m_Duration; } float Radius( bool bPlayers = true ); inline void SetAmplitude( float amplitude ) { m_Amplitude = amplitude; } inline void SetFrequency( float frequency ) { m_Frequency = frequency; } inline void SetDuration( float duration ) { m_Duration = duration; } inline void SetRadius( float radius ) { m_Radius = radius; } int DrawDebugTextOverlays(void); // Input handlers void InputStartShake( inputdata_t &inputdata ); void InputStopShake( inputdata_t &inputdata ); void InputAmplitude( inputdata_t &inputdata ); void InputFrequency( inputdata_t &inputdata ); // Causes the camera/physics shakes to happen: void ApplyShake( ShakeCommand_t command ); void Think( void ); }; LINK_ENTITY_TO_CLASS( env_shake, CEnvShake ); BEGIN_DATADESC( CEnvShake ) DEFINE_KEYFIELD( m_Amplitude, FIELD_FLOAT, "amplitude" ), DEFINE_KEYFIELD( m_Frequency, FIELD_FLOAT, "frequency" ), DEFINE_KEYFIELD( m_Duration, FIELD_FLOAT, "duration" ), DEFINE_KEYFIELD( m_Radius, FIELD_FLOAT, "radius" ), DEFINE_FIELD( m_stopTime, FIELD_TIME ), DEFINE_FIELD( m_nextShake, FIELD_TIME ), DEFINE_FIELD( m_currentAmp, FIELD_FLOAT ), DEFINE_FIELD( m_maxForce, FIELD_VECTOR ), DEFINE_PHYSPTR( m_pShakeController ), DEFINE_EMBEDDED( m_shakeCallback ), DEFINE_INPUTFUNC( FIELD_VOID, "StartShake", InputStartShake ), DEFINE_INPUTFUNC( FIELD_VOID, "StopShake", InputStopShake ), DEFINE_INPUTFUNC( FIELD_FLOAT, "Amplitude", InputAmplitude ), DEFINE_INPUTFUNC( FIELD_FLOAT, "Frequency", InputFrequency ), END_DATADESC() #define SF_SHAKE_EVERYONE 0x0001 // Don't check radius #define SF_SHAKE_INAIR 0x0004 // Shake players in air #define SF_SHAKE_PHYSICS 0x0008 // Shake physically (not just camera) #define SF_SHAKE_ROPES 0x0010 // Shake ropes too. #define SF_SHAKE_NO_VIEW 0x0020 // DON'T shake the view (only ropes and/or physics objects) #define SF_SHAKE_NO_RUMBLE 0x0040 // DON'T Rumble the XBox Controller //----------------------------------------------------------------------------- // Purpose: Destructor. //----------------------------------------------------------------------------- CEnvShake::~CEnvShake( void ) { if ( m_pShakeController ) { physenv->DestroyMotionController( m_pShakeController ); } } float CEnvShake::Radius(bool bPlayers) { // The radius for players is zero if SF_SHAKE_EVERYONE is set if ( bPlayers && HasSpawnFlags(SF_SHAKE_EVERYONE)) return 0; return m_Radius; } //----------------------------------------------------------------------------- // Purpose: Sets default member values when spawning. //----------------------------------------------------------------------------- void CEnvShake::Spawn( void ) { SetSolid( SOLID_NONE ); SetMoveType( MOVETYPE_NONE ); if ( GetSpawnFlags() & SF_SHAKE_EVERYONE ) { m_Radius = 0; } if ( HasSpawnFlags( SF_SHAKE_NO_VIEW ) && !HasSpawnFlags( SF_SHAKE_PHYSICS ) && !HasSpawnFlags( SF_SHAKE_ROPES ) ) { DevWarning( "env_shake %s with \"Don't shake view\" spawnflag set without \"Shake physics\" or \"Shake ropes\" spawnflags set.", GetDebugName() ); } } //----------------------------------------------------------------------------- // Purpose: Restore the motion controller //----------------------------------------------------------------------------- void CEnvShake::OnRestore( void ) { BaseClass::OnRestore(); if ( m_pShakeController ) { m_pShakeController->SetEventHandler( &m_shakeCallback ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CEnvShake::ApplyShake( ShakeCommand_t command ) { if ( !HasSpawnFlags( SF_SHAKE_NO_VIEW ) || !HasSpawnFlags( SF_SHAKE_NO_RUMBLE ) ) { bool air = (GetSpawnFlags() & SF_SHAKE_INAIR) ? true : false; UTIL_ScreenShake( GetAbsOrigin(), Amplitude(), Frequency(), Duration(), Radius(), command, air ); } if ( GetSpawnFlags() & SF_SHAKE_ROPES ) { CRopeKeyframe::ShakeRopes( GetAbsOrigin(), Radius(false), Frequency() ); } if ( GetSpawnFlags() & SF_SHAKE_PHYSICS ) { if ( !m_pShakeController ) { m_pShakeController = physenv->CreateMotionController( &m_shakeCallback ); } // do physics shake switch( command ) { case SHAKE_START: case SHAKE_START_NORUMBLE: case SHAKE_START_RUMBLEONLY: { m_stopTime = gpGlobals->curtime + Duration(); m_nextShake = 0; m_pShakeController->ClearObjects(); SetNextThink( gpGlobals->curtime ); m_currentAmp = Amplitude(); CBaseEntity *list[1024]; float radius = Radius(false); // probably checked "Shake Everywhere" do a big radius if ( !radius ) { radius = 512; } Vector extents = Vector(radius, radius, radius); extents.z = MAX(extents.z, 100); Vector mins = GetAbsOrigin() - extents; Vector maxs = GetAbsOrigin() + extents; int count = UTIL_EntitiesInBox( list, 1024, mins, maxs, 0 ); for ( int i = 0; i < count; i++ ) { // // Only shake physics entities that players can see. This is one frame out of date // so it's possible that we could miss objects if a player changed PVS this frame. // if ( ( list[i]->GetMoveType() == MOVETYPE_VPHYSICS ) ) { IPhysicsObject *pPhys = list[i]->VPhysicsGetObject(); if ( pPhys && pPhys->IsMoveable() ) { m_pShakeController->AttachObject( pPhys, false ); pPhys->Wake(); } } } } break; case SHAKE_STOP: m_pShakeController->ClearObjects(); break; case SHAKE_AMPLITUDE: m_currentAmp = Amplitude(); case SHAKE_FREQUENCY: m_pShakeController->WakeObjects(); break; } } } //----------------------------------------------------------------------------- // Purpose: Input handler that starts the screen shake. //----------------------------------------------------------------------------- void CEnvShake::InputStartShake( inputdata_t &inputdata ) { if ( HasSpawnFlags( SF_SHAKE_NO_RUMBLE ) ) { ApplyShake( SHAKE_START_NORUMBLE ); } else if ( HasSpawnFlags( SF_SHAKE_NO_VIEW ) ) { ApplyShake( SHAKE_START_RUMBLEONLY ); } else { ApplyShake( SHAKE_START ); } } //----------------------------------------------------------------------------- // Purpose: Input handler that stops the screen shake. //----------------------------------------------------------------------------- void CEnvShake::InputStopShake( inputdata_t &inputdata ) { ApplyShake( SHAKE_STOP ); } //----------------------------------------------------------------------------- // Purpose: Handles changes to the shake amplitude from an external source. //----------------------------------------------------------------------------- void CEnvShake::InputAmplitude( inputdata_t &inputdata ) { SetAmplitude( inputdata.value.Float() ); ApplyShake( SHAKE_AMPLITUDE ); } //----------------------------------------------------------------------------- // Purpose: Handles changes to the shake frequency from an external source. //----------------------------------------------------------------------------- void CEnvShake::InputFrequency( inputdata_t &inputdata ) { SetFrequency( inputdata.value.Float() ); ApplyShake( SHAKE_FREQUENCY ); } //----------------------------------------------------------------------------- // Purpose: Calculates the physics shake values //----------------------------------------------------------------------------- void CEnvShake::Think( void ) { int i; if ( gpGlobals->curtime > m_nextShake ) { // Higher frequency means we recalc the extents more often and perturb the display again m_nextShake = gpGlobals->curtime + (1.0f / Frequency()); // Compute random shake extents (the shake will settle down from this) for (i = 0; i < 2; i++ ) { m_maxForce[i] = random->RandomFloat( -1, 1 ); } // make the force it point mostly up m_maxForce.z = 4; VectorNormalize( m_maxForce ); m_maxForce *= m_currentAmp * 400; // amplitude is the acceleration of a 100kg object } float fraction = ( m_stopTime - gpGlobals->curtime ) / Duration(); if ( fraction < 0 ) { m_pShakeController->ClearObjects(); return; } float freq = 0; // Ramp up frequency over duration if ( fraction ) { freq = (Frequency() / fraction); } // square fraction to approach zero more quickly fraction *= fraction; // Sine wave that slowly settles to zero fraction = fraction * sin( gpGlobals->curtime * freq ); // Add to view origin for ( i = 0; i < 3; i++ ) { // store the force in the controller callback m_shakeCallback.m_force[i] = m_maxForce[i] * fraction; } // Drop amplitude a bit, less for higher frequency shakes m_currentAmp -= m_currentAmp * ( gpGlobals->frametime / (Duration() * Frequency()) ); SetNextThink( gpGlobals->curtime ); } //------------------------------------------------------------------------------ // Purpose: Console command to cause a screen shake. //------------------------------------------------------------------------------ void CC_Shake( void ) { CBasePlayer *pPlayer = UTIL_GetCommandClient(); if (pPlayer) { UTIL_ScreenShake( pPlayer->WorldSpaceCenter(), 25.0, 150.0, 1.0, 750, SHAKE_START ); } } //----------------------------------------------------------------------------- // Purpose: Draw any debug text overlays // Returns current text offset from the top //----------------------------------------------------------------------------- int CEnvShake::DrawDebugTextOverlays( void ) { int text_offset = BaseClass::DrawDebugTextOverlays(); if (m_debugOverlays & OVERLAY_TEXT_BIT) { char tempstr[512]; // print amplitude Q_snprintf(tempstr,sizeof(tempstr)," magnitude: %f", m_Amplitude); EntityText(text_offset,tempstr,0); text_offset++; // print frequency Q_snprintf(tempstr,sizeof(tempstr)," frequency: %f", m_Frequency); EntityText(text_offset,tempstr,0); text_offset++; // print duration Q_snprintf(tempstr,sizeof(tempstr)," duration: %f", m_Duration); EntityText(text_offset,tempstr,0); text_offset++; // print radius Q_snprintf(tempstr,sizeof(tempstr)," radius: %f", m_Radius); EntityText(text_offset,tempstr,0); text_offset++; } return text_offset; } static ConCommand shake("shake", CC_Shake, "Shake the screen.", FCVAR_CHEAT );