//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Rumble effects mixer for XBox // // $NoKeywords: $ // //=============================================================================// #include "cbase.h" #include "c_rumble.h" #include "rumble_shared.h" #include "inputsystem/iinputsystem.h" ConVar cl_rumblescale( "cl_rumblescale", "1.0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX, "Scale sensitivity of rumble effects (0 to 1.0)" ); ConVar cl_debugrumble( "cl_debugrumble", "0", FCVAR_ARCHIVE, "Turn on rumble debugging spew" ); #define MAX_RUMBLE_CHANNELS 3 // Max concurrent rumble effects #define NUM_WAVE_SAMPLES 30 // Effects play at 10hz typedef struct { float amplitude_left[NUM_WAVE_SAMPLES]; float amplitude_right[NUM_WAVE_SAMPLES]; int numSamples; } RumbleWaveform_t; //========================================================= // Structure for a rumble effect channel. This is akin to // a sound channel that is playing a sound. //========================================================= typedef struct { float starttime; // When did this effect start playing? (gpGlobals->curtime) int waveformIndex; // Type of effect waveform used (an enum from rumble_shared.h) int priority; // How important this effect is (for making replacement decisions) bool in_use; // Is this channel in use?? (true if effect is currently playing, false if done or otherwise available) unsigned char rumbleFlags; // Flags pertaining to the effect currently playing on this channel. float scale; // Some effects are updated while they are running. } RumbleChannel_t; //========================================================= // This structure contains parameters necessary to generate // a sine or sawtooth waveform. //========================================================= typedef struct tagWaveGenParams { float cycles; // AKA frequency float amplitudescale; bool leftChannel; // If false, generating for the right channel float maxAmplitude; // Clamping float minAmplitude; void Set( float c_cycles, float c_amplitudescale, bool c_leftChannel, float c_minAmplitude, float c_maxAmplitude ) { cycles = c_cycles; amplitudescale = c_amplitudescale; leftChannel = c_leftChannel; minAmplitude = c_minAmplitude; maxAmplitude = c_maxAmplitude; } // CTOR tagWaveGenParams( float c_cycles, float c_amplitudescale, bool c_leftChannel, float c_minAmplitude, float c_maxAmplitude ) { Set( c_cycles, c_amplitudescale, c_leftChannel, c_minAmplitude, c_maxAmplitude ); } } WaveGenParams_t; //--------------------------------------------------------- //--------------------------------------------------------- void TerminateWaveform( RumbleWaveform_t *pWaveform, int samples ) { if( samples <= NUM_WAVE_SAMPLES ) { pWaveform->numSamples = samples; } } //--------------------------------------------------------- //--------------------------------------------------------- void EaseInWaveform( RumbleWaveform_t *pWaveform, int samples, bool left ) { float step = 1.0f / ((float)samples); float factor = 0.0f; for( int i = 0 ; i < samples ; i++ ) { if( left ) { pWaveform->amplitude_left[i] *= factor; } else { pWaveform->amplitude_right[i] *= factor; } factor += step; } } //--------------------------------------------------------- //--------------------------------------------------------- void EaseOutWaveform( RumbleWaveform_t *pWaveform, int samples, bool left ) { float step = 1.0f / ((float)samples); float factor = 0.0f; int i = NUM_WAVE_SAMPLES - 1; for( int j = 0 ; j < samples ; j++ ) { if( left ) { pWaveform->amplitude_left[i] *= factor; } else { pWaveform->amplitude_right[i] *= factor; } factor += step; i--; } } //--------------------------------------------------------- //--------------------------------------------------------- void GenerateSawtoothEffect( RumbleWaveform_t *pWaveform, const WaveGenParams_t ¶ms ) { float delta = params.maxAmplitude - params.minAmplitude; int waveLength = NUM_WAVE_SAMPLES / params.cycles; float vstep = (delta / waveLength); float amplitude = params.minAmplitude; for( int i = 0 ; i < NUM_WAVE_SAMPLES ; i++ ) { if( params.leftChannel ) { pWaveform->amplitude_left[i] = amplitude; } else { pWaveform->amplitude_right[i] = amplitude; } amplitude += vstep; if( amplitude > params.maxAmplitude ) { amplitude = params.minAmplitude; } } } //--------------------------------------------------------- //--------------------------------------------------------- void GenerateSquareWaveEffect( RumbleWaveform_t *pWaveform, const WaveGenParams_t ¶ms ) { int i = 0; int j; int steps = ((float)NUM_WAVE_SAMPLES) / (params.cycles*2.0f); while( i < NUM_WAVE_SAMPLES ) { for( j = 0 ; j < steps && i < NUM_WAVE_SAMPLES; j++ ) { if( params.leftChannel ) { pWaveform->amplitude_left[i++] = params.minAmplitude; } else { pWaveform->amplitude_right[i++] = params.minAmplitude; } } for( j = 0 ; j < steps && i < NUM_WAVE_SAMPLES; j++ ) { if( params.leftChannel ) { pWaveform->amplitude_left[i++] = params.maxAmplitude; } else { pWaveform->amplitude_right[i++] = params.maxAmplitude; } } } } //--------------------------------------------------------- // If you pass a numSamples, this wave will only be that many // samples long. //--------------------------------------------------------- void GenerateFlatEffect( RumbleWaveform_t *pWaveform, const WaveGenParams_t ¶ms ) { for( int i = 0 ; i < NUM_WAVE_SAMPLES ; i++ ) { if( params.leftChannel ) { pWaveform->amplitude_left[i] = params.maxAmplitude; } else { pWaveform->amplitude_right[i] = params.maxAmplitude; } } } //--------------------------------------------------------- //--------------------------------------------------------- void GenerateSineWaveEffect( RumbleWaveform_t *pWaveform, const WaveGenParams_t ¶ms ) { float step = (360.0f * (params.cycles * 0.5f) ) / ((float)NUM_WAVE_SAMPLES); float degrees = 180.0f + step; // 180 to start at 0 for( int i = 0 ; i < NUM_WAVE_SAMPLES ; i++ ) { float radians = DEG2RAD(degrees); float value = fabs( sin(radians) ); value *= params.amplitudescale; if( value < params.minAmplitude ) value = params.minAmplitude; if( value > params.maxAmplitude ) value = params.maxAmplitude; if( params.leftChannel ) { pWaveform->amplitude_left[i] = value; } else { pWaveform->amplitude_right[i] = value; } degrees += step; } } //========================================================= //========================================================= class CRumbleEffects { public: CRumbleEffects() { Init(); } void Init(); void SetOutputEnabled( bool bEnable ); void StartEffect( unsigned char effectIndex, unsigned char rumbleData, unsigned char rumbleFlags ); void StopEffect( int effectIndex ); void StopAllEffects(); void ComputeAmplitudes( RumbleChannel_t *pChannel, float curtime, float *pLeft, float *pRight ); void UpdateEffects( float curtime ); void UpdateScreenShakeRumble( float shake, float balance ); RumbleChannel_t *FindExistingChannel( int index ); RumbleChannel_t *FindAvailableChannel( int priority ); public: RumbleChannel_t m_Channels[ MAX_RUMBLE_CHANNELS ]; RumbleWaveform_t m_Waveforms[ NUM_RUMBLE_EFFECTS ]; float m_flScreenShake; bool m_bOutputEnabled; }; CRumbleEffects g_RumbleEffects; //--------------------------------------------------------- //--------------------------------------------------------- void CRumbleEffects::Init() { SetOutputEnabled( true ); int i; for( i = 0 ; i < MAX_RUMBLE_CHANNELS ; i++ ) { m_Channels[i].in_use = false; m_Channels[i].priority = 0; } // Every effect defaults to this many samples. Call TerminateWaveform() to trim these. for ( i = 0 ; i < NUM_RUMBLE_EFFECTS ; i++ ) { m_Waveforms[i].numSamples = NUM_WAVE_SAMPLES; } // Jeep Idle WaveGenParams_t params( 1, 1.0f, false, 0.0f, 0.15f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_JEEP_ENGINE_LOOP], params ); // Pistol params.Set( 1, 1.0f, false, 0.0f, 0.5f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_PISTOL], params ); TerminateWaveform( &m_Waveforms[RUMBLE_PISTOL], 3 ); // SMG1 params.Set( 1, 1.0f, true, 0.0f, 0.2f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_SMG1], params ); params.Set( 1, 1.0f, false, 0.0f, 0.4f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_SMG1], params ); TerminateWaveform( &m_Waveforms[RUMBLE_SMG1], 3 ); // AR2 params.Set( 1, 1.0f, true, 0.0f, 0.5f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_AR2], params ); params.Set( 1, 1.0f, false, 0.0f, 0.3f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_AR2], params ); TerminateWaveform( &m_Waveforms[RUMBLE_AR2], 3 ); // AR2 Alt params.Set( 1, 1.0f, true, 0.0, 0.5f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_AR2_ALT_FIRE], params ); EaseInWaveform( &m_Waveforms[RUMBLE_AR2_ALT_FIRE], 5, true ); params.Set( 1, 1.0f, false, 0.0, 0.7f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_AR2_ALT_FIRE], params ); EaseInWaveform( &m_Waveforms[RUMBLE_AR2_ALT_FIRE], 5, false ); TerminateWaveform( &m_Waveforms[RUMBLE_AR2_ALT_FIRE], 7 ); // 357 params.Set( 1, 1.0f, true, 0.0f, 0.75f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_357], params ); params.Set( 1, 1.0f, false, 0.0f, 0.75f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_357], params ); TerminateWaveform( &m_Waveforms[RUMBLE_357], 2 ); // Shotgun params.Set( 1, 1.0f, true, 0.0f, 0.8f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_SHOTGUN_SINGLE], params ); params.Set( 1, 1.0f, false, 0.0f, 0.8f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_SHOTGUN_SINGLE], params ); TerminateWaveform( &m_Waveforms[RUMBLE_SHOTGUN_SINGLE], 3 ); params.Set( 1, 1.0f, true, 0.0f, 1.0f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_SHOTGUN_DOUBLE], params ); params.Set( 1, 1.0f, false, 0.0f, 1.0f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_SHOTGUN_DOUBLE], params ); TerminateWaveform( &m_Waveforms[RUMBLE_SHOTGUN_DOUBLE], 3 ); // RPG Missile params.Set( 1, 1.0f, false, 0.0f, 0.3f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_RPG_MISSILE], params ); EaseOutWaveform( &m_Waveforms[RUMBLE_RPG_MISSILE], 30, false ); TerminateWaveform( &m_Waveforms[RUMBLE_RPG_MISSILE], 6 ); // Physcannon open forks params.Set( 1, 1.0f, false, 0.0f, 0.25f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_PHYSCANNON_OPEN], params ); TerminateWaveform( &m_Waveforms[RUMBLE_PHYSCANNON_OPEN], 4 ); // Physcannon holding something params.Set( 1, 1.0f, true, 0.0f, 0.2f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_PHYSCANNON_LOW], params ); params.Set( 6, 1.0f, false, 0.0f, 0.25f ); GenerateSquareWaveEffect( &m_Waveforms[RUMBLE_PHYSCANNON_LOW], params ); // Crowbar params.Set( 1, 1.0f, false, 0.0f, 0.35f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_CROWBAR_SWING], params ); EaseOutWaveform( &m_Waveforms[RUMBLE_CROWBAR_SWING], 30, false ); TerminateWaveform( &m_Waveforms[RUMBLE_CROWBAR_SWING], 4 ); // Airboat gun params.Set( 1, 1.0f, false, 0.0f, 0.4f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_AIRBOAT_GUN], params ); params.Set( 12, 1.0f, true, 0.0f, 0.5f ); GenerateSawtoothEffect( &m_Waveforms[RUMBLE_AIRBOAT_GUN], params ); // Generic flat effects. params.Set( 1, 1.0f, true, 0.0f, 1.0f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_FLAT_LEFT], params ); params.Set( 1, 1.0f, false, 0.0f, 1.0f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_FLAT_RIGHT], params ); params.Set( 1, 1.0f, true, 0.0f, 1.0f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_FLAT_BOTH], params ); params.Set( 1, 1.0f, false, 0.0f, 1.0f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_FLAT_BOTH], params ); // Impact from a long fall params.Set( 1, 1.0f, false, 0.0f, 0.5f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_FALL_LONG], params ); params.Set( 1, 1.0f, true, 0.0f, 0.5f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_FALL_LONG], params ); TerminateWaveform( &m_Waveforms[RUMBLE_FALL_LONG], 3 ); // Impact from a short fall params.Set( 1, 1.0f, false, 0.0f, 0.3f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_FALL_SHORT], params ); params.Set( 1, 1.0f, true, 0.0f, 0.3f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_FALL_SHORT], params ); TerminateWaveform( &m_Waveforms[RUMBLE_FALL_SHORT], 2 ); // Portalgun left (blue) shot params.Set( 1, 1.0f, true, 0.0f, 0.3f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_PORTALGUN_LEFT], params ); TerminateWaveform( &m_Waveforms[RUMBLE_PORTALGUN_LEFT], 2 ); // Portalgun right (red) shot params.Set( 1, 1.0f, false, 0.0f, 0.3f ); GenerateFlatEffect( &m_Waveforms[RUMBLE_PORTALGUN_RIGHT], params ); TerminateWaveform( &m_Waveforms[RUMBLE_PORTALGUN_RIGHT], 2 ); // Portal failed to place feedback params.Set( 12, 1.0f, true, 0.0f, 1.0f ); GenerateSquareWaveEffect( &m_Waveforms[RUMBLE_PORTAL_PLACEMENT_FAILURE], params ); params.Set( 12, 1.0f, false, 0.0f, 1.0f ); GenerateSquareWaveEffect( &m_Waveforms[RUMBLE_PORTAL_PLACEMENT_FAILURE], params ); TerminateWaveform( &m_Waveforms[RUMBLE_PORTAL_PLACEMENT_FAILURE], 6 ); } //--------------------------------------------------------- //--------------------------------------------------------- RumbleChannel_t *CRumbleEffects::FindExistingChannel( int index ) { RumbleChannel_t *pChannel; for( int i = 0 ; i < MAX_RUMBLE_CHANNELS ; i++ ) { pChannel = &m_Channels[i]; if( pChannel->in_use && pChannel->waveformIndex == index ) { // This effect is already playing. Provide this channel for the // effect to be re-started on. return pChannel; } } return NULL; } //--------------------------------------------------------- // priority - the priority of the effect we want to play. //--------------------------------------------------------- RumbleChannel_t *CRumbleEffects::FindAvailableChannel( int priority ) { RumbleChannel_t *pChannel; int i; for( i = 0 ; i < MAX_RUMBLE_CHANNELS ; i++ ) { pChannel = &m_Channels[i]; if( !pChannel->in_use ) { return pChannel; } } int lowestPriority = priority; RumbleChannel_t *pBestChannel = NULL; float oldestChannel = FLT_MAX; // All channels already in use. Find a channel to slam. for( i = 0 ; i < MAX_RUMBLE_CHANNELS ; i++ ) { pChannel = &m_Channels[i]; if( (pChannel->rumbleFlags & RUMBLE_FLAG_LOOP) ) continue; if( pChannel->priority < lowestPriority ) { // Always happily slam a lower priority sound. pBestChannel = pChannel; lowestPriority = pChannel->priority; } else if( pChannel->priority == lowestPriority ) { // Priority is the same, so replace the oldest. if( pBestChannel ) { // If we already have a channel of the same priority to discard, make sure we discard the oldest. float age = gpGlobals->curtime - pChannel->starttime; if( age > oldestChannel ) { pBestChannel = pChannel; oldestChannel = age; } } else { // Take it. pBestChannel = pChannel; oldestChannel = gpGlobals->curtime - pChannel->starttime; } } } return pBestChannel; // Can still be NULL if we couldn't find a channel to slam. } //--------------------------------------------------------- //--------------------------------------------------------- void CRumbleEffects::SetOutputEnabled( bool bEnable ) { m_bOutputEnabled = bEnable; if( !bEnable ) { // Tell the hardware to shut down motors right now, in case this gets called // and some other process blocks us before the next rumble system update. m_flScreenShake = 0.0f; inputsystem->StopRumble(); } } //--------------------------------------------------------- //--------------------------------------------------------- void CRumbleEffects::StartEffect( unsigned char effectIndex, unsigned char rumbleData, unsigned char rumbleFlags ) { if( effectIndex == RUMBLE_STOP_ALL ) { StopAllEffects(); return; } if( rumbleFlags & RUMBLE_FLAG_STOP ) { StopEffect( effectIndex ); return; } int priority = 1; RumbleChannel_t *pChannel = NULL; if( (rumbleFlags & RUMBLE_FLAG_RESTART) ) { // Try to find any active instance of this effect and replace it. pChannel = FindExistingChannel( effectIndex ); } if( (rumbleFlags & RUMBLE_FLAG_ONLYONE) ) { pChannel = FindExistingChannel( effectIndex ); if( pChannel ) { // Bail out. An instance of this effect is already playing. return; } } if( (rumbleFlags & RUMBLE_FLAG_UPDATE_SCALE) ) { pChannel = FindExistingChannel( effectIndex ); if( pChannel ) { pChannel->scale = ((float)rumbleData) / 100.0f; } // It's possible to return without finding a rumble to update. // This means you tried to update a rumble you never started. return; } if( !pChannel ) { pChannel = FindAvailableChannel( priority ); } if( pChannel ) { pChannel->waveformIndex = effectIndex; pChannel->priority = 1; pChannel->starttime = gpGlobals->curtime; pChannel->in_use = true; pChannel->rumbleFlags = rumbleFlags; if( rumbleFlags & RUMBLE_FLAG_INITIAL_SCALE ) { pChannel->scale = ((float)rumbleData) / 100.0f; } else { pChannel->scale = 1.0f; } } if( (rumbleFlags & RUMBLE_FLAG_RANDOM_AMPLITUDE) ) { pChannel->scale = random->RandomFloat( 0.1f, 1.0f ); } } //--------------------------------------------------------- // Find all playing effects of this type and stop them. //--------------------------------------------------------- void CRumbleEffects::StopEffect( int effectIndex ) { for( int i = 0 ; i < MAX_RUMBLE_CHANNELS ; i++ ) { if( m_Channels[i].in_use && m_Channels[i].waveformIndex == effectIndex ) { m_Channels[i].in_use = false; } } } //--------------------------------------------------------- //--------------------------------------------------------- void CRumbleEffects::StopAllEffects() { for( int i = 0 ; i < MAX_RUMBLE_CHANNELS ; i++ ) { m_Channels[i].in_use = false; } m_flScreenShake = 0.0f; } //--------------------------------------------------------- //--------------------------------------------------------- void CRumbleEffects::ComputeAmplitudes( RumbleChannel_t *pChannel, float curtime, float *pLeft, float *pRight ) { // How long has this waveform been playing? float elapsed = curtime - pChannel->starttime; if( elapsed >= (NUM_WAVE_SAMPLES/10) ) { if( (pChannel->rumbleFlags & RUMBLE_FLAG_LOOP) ) { // This effect loops. Just fixup the start time and recompute elapsed. pChannel->starttime = curtime; elapsed = curtime - pChannel->starttime; } else { // This effect is done! Should it loop? *pLeft = 0; *pRight = 0; pChannel->in_use = false; return; } } // Figure out which sample we're playing FROM. int seconds = ((int) elapsed); int sample = (int)(elapsed*10.0f); // Get the fraction bit. float fraction = elapsed - seconds; float left, right; if( sample == m_Waveforms[pChannel->waveformIndex].numSamples ) { // This effect is done. Send zeroes to the mixer for this // final frame and then turn the channel off. (Unless it loops!) if( (pChannel->rumbleFlags & RUMBLE_FLAG_LOOP) ) { // Loop this effect pChannel->starttime = gpGlobals->curtime; // Send the first sample. left = m_Waveforms[pChannel->waveformIndex].amplitude_left[0]; right = m_Waveforms[pChannel->waveformIndex].amplitude_right[0]; } else { left = 0.0f; right = 0.0f; pChannel->in_use = false; } } else { // Use values for the last sample that we have passed left = m_Waveforms[pChannel->waveformIndex].amplitude_left[sample]; right = m_Waveforms[pChannel->waveformIndex].amplitude_right[sample]; } left *= pChannel->scale; right *= pChannel->scale; if( cl_debugrumble.GetBool() ) { Msg("Seconds:%d Fraction:%f Sample:%d L:%f R:%f\n", seconds, fraction, sample, left, right ); } if( !m_bOutputEnabled ) { // Send zeroes to stop any current rumbling, and to keep it silenced. left = 0; right = 0; } *pLeft = left; *pRight = right; } //--------------------------------------------------------- //--------------------------------------------------------- void CRumbleEffects::UpdateScreenShakeRumble( float shake, float balance ) { if( m_bOutputEnabled ) { m_flScreenShake = shake; } else { // Silence m_flScreenShake = 0.0f; } } //--------------------------------------------------------- //--------------------------------------------------------- void CRumbleEffects::UpdateEffects( float curtime ) { float fLeftMotor = 0.0f; float fRightMotor = 0.0f; for( int i = 0 ; i < MAX_RUMBLE_CHANNELS ; i++ ) { // Expire old channels RumbleChannel_t *pChannel = & m_Channels[i]; if( pChannel->in_use ) { float left, right; ComputeAmplitudes( pChannel, curtime, &left, &right ); fLeftMotor += left; fRightMotor += right; } } // Add in any screenshake float shakeLeft = 0.0f; float shakeRight = 0.0f; if( m_flScreenShake != 0.0f ) { if( m_flScreenShake < 0.0f ) { shakeLeft = fabs( m_flScreenShake ); } else { shakeRight = m_flScreenShake; } } fLeftMotor += shakeLeft; fRightMotor += shakeRight; fLeftMotor *= cl_rumblescale.GetFloat(); fRightMotor *= cl_rumblescale.GetFloat(); if( engine->IsPaused() ) { // Send nothing when paused. fLeftMotor = 0.0f; fRightMotor = 0.0f; } inputsystem->SetRumble( fLeftMotor, fRightMotor ); } //--------------------------------------------------------- //--------------------------------------------------------- void StopAllRumbleEffects( void ) { g_RumbleEffects.StopAllEffects(); inputsystem->StopRumble(); } //--------------------------------------------------------- //--------------------------------------------------------- void RumbleEffect( unsigned char effectIndex, unsigned char rumbleData, unsigned char rumbleFlags ) { g_RumbleEffects.StartEffect( effectIndex, rumbleData, rumbleFlags ); } //--------------------------------------------------------- //--------------------------------------------------------- void UpdateRumbleEffects() { C_BasePlayer *localPlayer = C_BasePlayer::GetLocalPlayer(); if( !localPlayer || !localPlayer->IsAlive() ) { StopAllRumbleEffects(); return; } g_RumbleEffects.UpdateEffects( gpGlobals->curtime ); } //--------------------------------------------------------- //--------------------------------------------------------- void UpdateScreenShakeRumble( float shake, float balance ) { C_BasePlayer *localPlayer = C_BasePlayer::GetLocalPlayer(); if( !localPlayer || !localPlayer->IsAlive() ) { return; } g_RumbleEffects.UpdateScreenShakeRumble( shake, balance ); } //--------------------------------------------------------- //--------------------------------------------------------- void EnableRumbleOutput( bool bEnable ) { g_RumbleEffects.SetOutputEnabled( bEnable ); }