//========= Copyright © 1996-2009, Valve Corporation, All rights reserved. ============// // //=============================================================================// #include "cbase.h" #include #include "paint_stream_manager.h" #include "paint_blobs_shared.h" #include "paint_sprayer_shared.h" #include "debugoverlay_shared.h" #include "fmtstr.h" #include "vprof.h" #include "paint_stream_shared.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" #define VPROF_BUDGETGROUP_PAINTBLOB _T("Paintblob") const char* const CPaintStreamManager::m_pPaintMaterialNames[PAINT_POWER_TYPE_COUNT_PLUS_NO_POWER] = { "paintblobs/blob_surface_bounce", "paintblobs/blob_surface_stick", // FIXME: Bring this back for DLC2 "paintblobs/blob_surface_reflect", "paintblobs/blob_surface_speed", "paintblobs/blob_surface_portal", "paintblobs/blob_surface_erase" }; const char* const CPaintStreamManager::s_SoundEffectNames[PAINT_IMPACT_EFFECT_COUNT] = { "Paintblob.Impact", "Paintblob.ImpactDrip" }; //Paint particle impact effect convars ConVar paint_impact_particles_distance_threshold( "paint_impact_particles_distance_threshold", "20.0f", FCVAR_REPLICATED ); ConVar paint_impact_particles_duration( "paint_impact_particles_duration", "0.2f", FCVAR_REPLICATED ); ConVar paint_min_impact_particles( "paint_min_impact_particles", "20", FCVAR_REPLICATED ); ConVar paint_max_impact_particles( "paint_max_impact_particles", "50", FCVAR_REPLICATED ); ConVar paint_impact_accumulate_sound_distance_threshold( "paint_impact_accumulate_sound_distance_threshold", "128.0f", FCVAR_REPLICATED ); ConVar paint_impact_count_to_max_adjusted_volume( "paint_impact_count_to_max_adjusted_volume", "5", FCVAR_REPLICATED ); ConVar paint_impact_count_to_min_adjusted_pitch_after_full_volume( "paint_impact_count_to_min_adjusted_pitch_after_full_volume", "5", FCVAR_REPLICATED ); ConVar min_adjusted_pitch_percentage( "min_adjusted_pitch_percentage", "0.85", FCVAR_REPLICATED ); ConVar draw_paint_splat_particles( "draw_paint_splat_particles", "1", FCVAR_REPLICATED ); ConVar group_paint_impact_effects( "cl_group_paint_impact_effects", "1", FCVAR_REPLICATED ); ConVar debug_paint_impact_effects( "debug_paint_impact_effects", "0", FCVAR_REPLICATED ); ConVar blobs_paused("blobs_paused", "0", FCVAR_CHEAT | FCVAR_REPLICATED ); CPaintStreamManager PaintStreamManager( "PaintStreamManager" ); CPaintStreamManager::CPaintStreamManager( char const *name ) : CAutoGameSystemPerFrame( name ) { } CPaintStreamManager::~CPaintStreamManager( void ) { } void CPaintStreamManager::LevelInitPreEntity() { blobs_paused.SetValue( true ); } void CPaintStreamManager::LevelInitPostEntity() { blobs_paused.SetValue( false ); } void CPaintStreamManager::LevelShutdownPostEntity() { m_PaintImpactParticles.RemoveAll(); if ( m_pBlobPool ) { m_pBlobPool->Clear(); delete m_pBlobPool; m_pBlobPool = NULL; } } void CPaintStreamManager::AllocatePaintBlobPool( int nMaxBlobs ) { int nMaxCount = ( nMaxBlobs ) ? nMaxBlobs : 250; // pre-allocate pool of blobs if ( !m_pBlobPool ) { #ifdef GAME_DLL m_pBlobPool = new CClassMemoryPool< CPaintBlob >( nMaxCount, CUtlMemoryPool::GROW_NONE ); #else if ( !engine->IsClientLocalToActiveServer() ) { m_pBlobPool = new CClassMemoryPool< CPaintBlob >( nMaxCount, CUtlMemoryPool::GROW_NONE ); } #endif } else if ( ( m_pBlobPool->Size() / m_pBlobPool->BlockSize() ) != nMaxBlobs ) { Assert( 0 ); Warning( "CPaintStreamManager::AllocatePaintBlobPool is being called multiple times (for some reasons) with different pool sizes." ); } } void CPaintStreamManager::RemoveAllPaintBlobs( void ) { for( int i = 0; i < IPaintStreamAutoList::AutoList().Count(); ++i ) { CPaintStream *pStream = static_cast< CPaintStream* >( IPaintStreamAutoList::AutoList()[i] ); if( pStream ) { pStream->RemoveAllPaintBlobs(); } } } CPaintBlob* CPaintStreamManager::AllocatePaintBlob( bool bSilent /*= false*/ ) { // don't create when blob is paused if ( blobs_paused.GetBool() ) { return NULL; } #ifdef CLIENT_DLL if ( bSilent ) { return NULL; } #endif return m_pBlobPool->Alloc(); } void CPaintStreamManager::FreeBlob( CPaintBlob* pBlob ) { m_pBlobPool->Free( pBlob ); } const char *CPaintStreamManager::GetPaintMaterialName( int type ) { return m_pPaintMaterialNames[ type ]; } #ifdef CLIENT_DLL void CPaintStreamManager::Update( float frametime ) { PaintStreamUpdate(); //Update the particle and sound impact effects UpdatePaintImpactEffects( frametime, m_PaintImpactParticles ); //Display a list of all the paint impact effects currently playing if( debug_paint_impact_effects.GetBool() ) { int line = 6; float lineHeight = 0.015; float startX = 0.01f; float startY = 0.0f; CFmtStr msg; msg.sprintf( "Paint blob impact particles: %d", m_PaintImpactParticles.Count() ); NDebugOverlay::ScreenText( startX, startY + (line * lineHeight), msg, 0, 255, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER ); line++; } } #else void CPaintStreamManager::PreClientUpdate( void ) { PaintStreamUpdate(); //engine->Con_NPrintf( 0, "num blobs = %d", GetBlobCount() ); //Update the particle and sound impact effects UpdatePaintImpactEffects( gpGlobals->frametime, m_PaintImpactParticles ); //Display a list of all the paint impact effects currently playing if( debug_paint_impact_effects.GetBool() ) { int line = 6; float lineHeight = 0.015; float startX = 0.01f; float startY = 0.0f; CFmtStr msg; msg.sprintf( "Paint blob impact particles: %d", m_PaintImpactParticles.Count() ); NDebugOverlay::ScreenText( startX, startY + (line * lineHeight), msg, 0, 255, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER ); line++; } } #endif void CPaintStreamManager::PaintStreamUpdate() { VPROF_BUDGET( "CPaintStreamManager::PaintStreamUpdate", VPROF_BUDGETGROUP_PAINTBLOB ); #ifdef CLIENT_DLL // if the client is local to server, only update render bounds // let the server do all the work. if ( engine->IsClientLocalToActiveServer() ) { for ( int i = 0; i < IPaintStreamAutoList::AutoList().Count(); ++i ) { CPaintStream *pStream = static_cast< CPaintStream* >( IPaintStreamAutoList::AutoList()[i] ); if ( pStream ) { pStream->UpdateRenderBoundsAndOriginWorldspace(); } } return; } #endif // remove dead blobs from beam list before we set dead blobs to NULL CTrigger_TractorBeam_Shared::RemoveDeadBlobsFromBeams(); // we want to update blob collision for all blobs at once PaintBlobVector_t allBlobs; // preupdate (delete blobs from streams) for ( int i = 0; i < IPaintStreamAutoList::AutoList().Count(); ++i ) { CPaintStream *pStream = static_cast< CPaintStream* >( IPaintStreamAutoList::AutoList()[i] ); if ( pStream ) { pStream->PreUpdateBlobs(); int numCurrentBlobs = allBlobs.Count(); int numNewBlobs = pStream->GetBlobList().Count(); allBlobs.EnsureCount( allBlobs.Count() + numNewBlobs ); V_memcpy( allBlobs.Base() + numCurrentBlobs, pStream->GetBlobList().Base(), numNewBlobs * sizeof( CBasePaintBlob* ) ); } } // copy blobs from all stream and update all of them if ( blobs_paused.GetBool() ) { for ( int i=0; iSetLastUpdateTime( gpGlobals->curtime ); } } else { // update all blobs PaintBlobUpdate( allBlobs ); } // post update for ( int i = 0; i < IPaintStreamAutoList::AutoList().Count(); ++i ) { CPaintStream *pStream = static_cast< CPaintStream* >( IPaintStreamAutoList::AutoList()[i] ); if ( pStream ) { pStream->PostUpdateBlobs(); } } // remove blobs that change beams to correct blob list in beam CTrigger_TractorBeam_Shared::RemoveBlobsFromPreviousBeams(); } void CPaintStreamManager::UpdatePaintImpactEffects( float flDeltaTime, PaintBlobImpactEffectVector_t &paintImpactEffects ) { //Update the paint blob impact effects for( int i = 0; i < paintImpactEffects.Count(); ++i ) { PaintBlobImpactEffect_t *pEffect = &paintImpactEffects[i]; if ( pEffect ) { //Decrement the timer of the effect pEffect->flTime -= flDeltaTime; //Remove the effect if it has finished playing if( pEffect->flTime <= 0.0f ) { paintImpactEffects.FastRemove( i-- ); } } } } void CPaintStreamManager::CreatePaintImpactParticles( const Vector &vecPosition, const Vector &vecNormal, int paintType ) { //Check if the impact particle effect should be played if( ShouldPlayImpactEffect( vecPosition, m_PaintImpactParticles, paint_min_impact_particles.GetInt(), paint_max_impact_particles.GetInt(), paint_impact_particles_distance_threshold.GetFloat() * paint_impact_particles_distance_threshold.GetFloat() ) ) { PlayPaintImpactParticles( vecPosition, vecNormal, paintType ); } } bool CPaintStreamManager::ShouldPlayImpactEffect( const Vector& vecPosition, PaintBlobImpactEffectVector_t &paintImpactEffects, int minEffects, int maxEffects, float flDistanceThresholdSqr ) { if( !group_paint_impact_effects.GetBool() ) { return true; } int iImpactEffectCount = paintImpactEffects.Count(); //If we are below the min threshold then play the paint impact effect if( iImpactEffectCount < minEffects ) { return true; } //Don't play any paint impact effect if we are above the max else if( iImpactEffectCount >= maxEffects ) { return false; } int iEffectIndex = 0; //Don't play the effect if it's too close to another paint impact effect for ( iEffectIndex = 0; iEffectIndex < iImpactEffectCount; ++iEffectIndex ) { PaintBlobImpactEffect_t *pEffect = &paintImpactEffects[iEffectIndex]; if ( pEffect ) { //Check if this effect is too close to a effect already playing if ( vecPosition.DistToSqr( pEffect->vecPosition ) < ( flDistanceThresholdSqr ) ) { return false; } } } //OK to play the effect return true; } struct SplatParticlesForPaint_t { int nPaintType; const char *lpszParticleSystemName; }; SplatParticlesForPaint_t paintSplatCallbacks[] = { { BOUNCE_POWER, "paint_splat_bounce_01" }, { REFLECT_POWER,"paint_splat_stick_01" }, // FIXME: Bring this back for DLC2 { REFLECT_POWER,"paint_splat_reflect_01" }, { SPEED_POWER, "paint_splat_speed_01" }, { PORTAL_POWER, "paint_splat_erase_01" }, { NO_POWER, "paint_splat_erase_01" }, }; void PaintSplatEffect( const Vector& vecPosition, const Vector& vecNormal, int paintType ) { Assert( paintType >= 0 && paintType < ARRAYSIZE( paintSplatCallbacks ) ); Assert( paintSplatCallbacks[paintType].nPaintType == paintType ); QAngle angle; VectorAngles( -vecNormal, angle ); CBasePlayer *pPlayer = NULL; #ifdef GAME_DLL if ( !engine->IsDedicatedServer() ) { pPlayer = UTIL_GetLocalPlayerOrListenServerHost(); } #else pPlayer = GetSplitScreenViewPlayer(); #endif if ( pPlayer ) { CSingleUserRecipientFilter filter( pPlayer ); DispatchParticleEffect( paintSplatCallbacks[paintType].lpszParticleSystemName, vecPosition, angle, NULL, -1, &filter ); } } void CPaintStreamManager::PlayPaintImpactParticles( const Vector &vecPosition, const Vector &vecNormal, int paintType ) { //Play the particle effect for the impact if( draw_paint_splat_particles.GetBool() ) { PaintSplatEffect( vecPosition, vecNormal, paintType ); } //Add the effect to the list int iEffectIndex = m_PaintImpactParticles.AddToTail(); m_PaintImpactParticles[iEffectIndex].vecPosition = vecPosition; m_PaintImpactParticles[iEffectIndex].flTime = paint_impact_particles_duration.GetFloat(); } float CPaintStreamManager::PlayPaintImpactSound( const Vector &vecPosition, PaintImpactEffect impactEffect ) { //Emit the sound for the impact #ifdef GAME_DLL CBasePlayer *pRecipient = UTIL_GetLocalPlayerOrListenServerHost(); if ( pRecipient == NULL ) { return 0.0f; } CSingleUserRecipientFilter filter( pRecipient ); #else CLocalPlayerFilter filter; #endif const char* soundName = GetPaintSoundEffectName( impactEffect ); CBaseEntity::EmitSound( filter, 0, soundName, &vecPosition ); return CBaseEntity::GetSoundDuration( soundName, NULL ); } typedef CUtlVectorFixedGrowable< Vector, 16 > AccumulatedSoundPositionVector; struct AccumulatedImpactSound { CSoundParameters soundParams; AccumulatedSoundPositionVector positions; float volumeIncreasePerImpact; int pitchDecreasePerFullVolumeImpact; int minAdjustedPitch; void Initialize( const Vector& center, const char* soundName ); }; void AccumulatedImpactSound::Initialize( const Vector& center, const char* soundName ) { positions.AddToTail( center ); if( CBaseEntity::GetParametersForSound( soundName, soundParams, NULL ) ) { const int impactsToMinPitch = paint_impact_count_to_min_adjusted_pitch_after_full_volume.GetInt(); minAdjustedPitch = min_adjusted_pitch_percentage.GetFloat() * soundParams.pitch + 0.5f; const int deltaToMinPitch = soundParams.pitch - minAdjustedPitch; pitchDecreasePerFullVolumeImpact = static_cast(deltaToMinPitch) / impactsToMinPitch + 0.5f; const int impactsToFullVolume = paint_impact_count_to_max_adjusted_volume.GetInt(); const float deltaToFullVolume = VOL_NORM - soundParams.volume; volumeIncreasePerImpact = deltaToFullVolume / impactsToFullVolume; } else { Assert(!"GetParametersForSound() failed."); } } typedef CUtlVectorFixedGrowable< AccumulatedImpactSound, 32 > AccumulatedImpactSoundVector; void CPaintStreamManager::PlayMultiplePaintImpactSounds( TimeStampVector& channelTimeStamps, int maxChannels, const PaintImpactPositionVector& positions, PaintImpactEffect impactEffect ) { const char* soundName = GetPaintSoundEffectName( impactEffect ); Assert( positions.Count() > 0 && soundName != NULL ); const int maxChannelsToAdd = imax( maxChannels - channelTimeStamps.Count(), 0 ); if( positions.Count() > 0 && soundName != NULL && maxChannelsToAdd > 0 ) { AccumulatedImpactSoundVector accumulatedSounds; accumulatedSounds.AddToTail(); accumulatedSounds.Tail().Initialize( positions.Head(), soundName ); const float maxRadiusSq = Sqr( paint_impact_accumulate_sound_distance_threshold.GetFloat() ); // For each position for( int positionIndex = 1; positionIndex < positions.Count(); ++positionIndex ) { // Check if the position is close enough to the center of an accumulated impact sound // Note: "Center" is just the first position in the list. const Vector& soundPosition = positions[positionIndex]; bool positionAccumulated = false; for( int accumSoundIndex = 0; accumSoundIndex < accumulatedSounds.Count(); ++accumSoundIndex ) { AccumulatedImpactSound& sound = accumulatedSounds[accumSoundIndex]; const Vector& center = sound.positions.Head(); if( (center - soundPosition).LengthSqr() < maxRadiusSq ) { sound.positions.AddToTail( soundPosition ); int& pitch = sound.soundParams.pitch; float& volume = sound.soundParams.volume; const int adjustedPitch = pitch - isel( VOL_NORM - volume, sound.pitchDecreasePerFullVolumeImpact, 0 ); pitch = imax( adjustedPitch, sound.minAdjustedPitch ); volume = fpmin( volume + sound.volumeIncreasePerImpact, VOL_NORM ); positionAccumulated = true; break; } } if( !positionAccumulated && accumulatedSounds.Count() < maxChannelsToAdd ) { accumulatedSounds.AddToTail(); accumulatedSounds.Tail().Initialize( soundPosition, soundName ); } } // Play each accumulated sound for( int accumSoundIndex = 0; accumSoundIndex < accumulatedSounds.Count(); ++accumSoundIndex ) { AccumulatedImpactSound& sound = accumulatedSounds[accumSoundIndex]; // Find the average position const AccumulatedSoundPositionVector& soundPositions = sound.positions; Vector averagedCenter = std::accumulate( soundPositions.Base(), soundPositions.Base() + soundPositions.Count(), vec3_origin ); averagedCenter /= soundPositions.Count(); // Emit the sound EmitSound_t emitParams( sound.soundParams ); emitParams.m_pOrigin = &averagedCenter; const float duration = PlayPaintImpactSound( emitParams ); // Update the number of used channels channelTimeStamps.AddToTail( gpGlobals->curtime + duration ); } } } const char* CPaintStreamManager::GetPaintSoundEffectName( unsigned int impactEffect ) { return impactEffect < PAINT_IMPACT_EFFECT_COUNT ? s_SoundEffectNames[impactEffect] : NULL; } float CPaintStreamManager::PlayPaintImpactSound( const EmitSound_t& emitParams ) { //Emit the sound for the impact #ifdef GAME_DLL CBasePlayer *pRecipient = UTIL_GetLocalPlayerOrListenServerHost(); if ( pRecipient == NULL ) { return 0.0f; } CSingleUserRecipientFilter filter( pRecipient ); #else CLocalPlayerFilter filter; #endif CBaseEntity::EmitSound( filter, 0, emitParams ); return CBaseEntity::GetSoundDuration( emitParams.m_pSoundName, NULL ); // This will generate a "should use game_sounds.txt" warning, but the sound name comes from game_sounds.txt. The warning is benign. }