//========= Copyright Valve Corporation, All rights reserved. ============// // // npc_blob - experimental, cpu-intensive monster made of lots of smaller elements // //=============================================================================// #include "cbase.h" #include "ai_default.h" #include "ai_task.h" #include "ai_schedule.h" #include "ai_hull.h" #include "soundent.h" #include "game.h" #include "npcevent.h" #include "entitylist.h" #include "activitylist.h" #include "ai_basenpc.h" #include "engine/IEngineSound.h" #include "vstdlib/jobthread.h" #include "saverestore_utlvector.h" #include "eventqueue.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" extern float MOVE_HEIGHT_EPSILON; #define BLOB_MAX_AVOID_ORIGINS 3 ConVar blob_mindist( "blob_mindist", "120.0" ); ConVar blob_element_speed( "blob_element_speed", "187" ); ConVar npc_blob_idle_speed_factor( "npc_blob_idle_speed_factor", "0.5" ); ConVar blob_numelements( "blob_numelements", "20" ); ConVar blob_batchpercent( "blob_batchpercent", "100" ); ConVar blob_radius( "blob_radius", "160" ); //ConVar blob_min_element_speed( "blob_min_element_speed", "50" ); //ConVar blob_max_element_speed( "blob_max_element_speed", "250" ); ConVar npc_blob_use_threading( "npc_blob_use_threading", "1" ); ConVar npc_blob_sin_amplitude( "npc_blob_sin_amplitude", "60.0f" ); ConVar npc_blob_show_centroid( "npc_blob_show_centroid", "0" ); ConVar npc_blob_straggler_dist( "npc_blob_straggler_dist", "240" ); ConVar npc_blob_use_orientation( "npc_blob_use_orientation", "1" ); ConVar npc_blob_use_model( "npc_blob_use_model", "2" ); ConVar npc_blob_think_interval( "npc_blob_think_interval", "0.025" ); #define NPC_BLOB_MODEL "models/headcrab.mdl" //========================================================= // Blob movement rules //========================================================= enum { BLOB_MOVE_SWARM = 0, // Just swarm with the rest of the group BLOB_MOVE_TO_TARGET_LOCATION, // Move to a designated location BLOB_MOVE_TO_TARGET_ENTITY, // Chase the designated entity BLOB_MOVE_DONT_MOVE, // Sit still!!!! }; //========================================================= //========================================================= class CBlobElement : public CBaseAnimating { public: void Precache(); void Spawn(); int DrawDebugTextOverlays(void); void SetElementVelocity( Vector vecVelocity, bool bPlanarOnly ); void AddElementVelocity( Vector vecVelocityAdd, bool bPlanarOnly ); void ModifyVelocityForSurface( float flInterval, float flSpeed ); void SetSinePhase( float flPhase ) { m_flSinePhase = flPhase; } float GetSinePhase() { return m_flSinePhase; } float GetSineAmplitude() { return m_flSineAmplitude; } float GetSineFrequency() { return m_flSineFrequency; } void SetActiveMovementRule( int moveRule ) { m_iMovementRule = moveRule; } int GetActiveMovementRule() { return m_iMovementRule; } void MoveTowardsTargetEntity( float speed ); void SetTargetEntity( CBaseEntity *pEntity ) { m_hTargetEntity = pEntity; } CBaseEntity *GetTargetEntity() { return m_hTargetEntity.Get(); } void MoveTowardsTargetLocation( float speed ); void SetTargetLocation( const Vector &vecLocation ) { m_vecTargetLocation = vecLocation; } void ReconfigureRandomParams(); void EnforceSpeedLimits( float flMinSpeed, float flMaxSpeed ); DECLARE_DATADESC(); public: Vector m_vecPrevOrigin; // Only exists for debugging (isolating stuck elements) int m_iStuckCount; bool m_bOnWall; float m_flDistFromCentroidSqr; int m_iElementNumber; Vector m_vecTargetLocation; float m_flRandomEightyPercent; private: EHANDLE m_hTargetEntity; float m_flSinePhase; float m_flSineAmplitude; float m_flSineFrequency; int m_iMovementRule; }; LINK_ENTITY_TO_CLASS( blob_element, CBlobElement ); //--------------------------------------------------------- // Save/Restore //--------------------------------------------------------- BEGIN_DATADESC( CBlobElement ) DEFINE_FIELD( m_vecPrevOrigin, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_iStuckCount, FIELD_INTEGER ), DEFINE_FIELD( m_bOnWall, FIELD_BOOLEAN ), DEFINE_FIELD( m_flDistFromCentroidSqr, FIELD_FLOAT ), DEFINE_FIELD( m_iElementNumber, FIELD_INTEGER ), DEFINE_FIELD( m_vecTargetLocation, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_hTargetEntity, FIELD_EHANDLE ), DEFINE_FIELD( m_flSinePhase, FIELD_FLOAT ), DEFINE_FIELD( m_flSineAmplitude, FIELD_FLOAT ), DEFINE_FIELD( m_flSineFrequency, FIELD_FLOAT ), DEFINE_FIELD( m_iMovementRule, FIELD_INTEGER ), END_DATADESC() const char *pszBlobModels[] = { "models/gibs/agibs.mdl", "models/props_junk/watermelon01.mdl", "models/w_squeak.mdl", "models/baby_headcrab.mdl" }; const char *GetBlobModelName() { int index = npc_blob_use_model.GetInt(); return pszBlobModels[ index ]; } //--------------------------------------------------------- //--------------------------------------------------------- void CBlobElement::Precache() { PrecacheModel( GetBlobModelName() ); m_flRandomEightyPercent = random->RandomFloat( 0.8f, 1.0f ); } //--------------------------------------------------------- //--------------------------------------------------------- void CBlobElement::Spawn() { Precache(); SetSolid( SOLID_NONE ); SetMoveType( MOVETYPE_FLY ); AddSolidFlags( FSOLID_NOT_STANDABLE | FSOLID_NOT_SOLID ); SetModel( GetBlobModelName() ); UTIL_SetSize( this, vec3_origin, vec3_origin ); QAngle angles(0,0,0); angles.y = random->RandomFloat( 0, 180 ); SetAbsAngles( angles ); AddEffects( EF_NOSHADOW ); ReconfigureRandomParams(); } //--------------------------------------------------------- //--------------------------------------------------------- int CBlobElement::DrawDebugTextOverlays(void) { int text_offset = BaseClass::DrawDebugTextOverlays(); if (m_debugOverlays & OVERLAY_TEXT_BIT) { char tempstr[512]; Q_snprintf(tempstr,sizeof(tempstr), "Element #:%d", m_iElementNumber ); EntityText(text_offset,tempstr,0); text_offset++; } return text_offset; } //--------------------------------------------------------- // This is the official way to set velocity for an element // Do not call SetAbsVelocity() directly, since we also // need to record the last velocity we intended to give the // element, so that we can detect changes after game physics // runs. //--------------------------------------------------------- void CBlobElement::SetElementVelocity( Vector vecVelocity, bool bPlanarOnly ) { SetAbsVelocity( vecVelocity ); } //--------------------------------------------------------- // This is the official way to add velocity to an element. // See SetElementVelocity() for explanation. //--------------------------------------------------------- void CBlobElement::AddElementVelocity( Vector vecVelocityAdd, bool bPlanarOnly ) { Vector vecSum = GetAbsVelocity() + vecVelocityAdd; SetAbsVelocity( vecSum ); } //--------------------------------------------------------- // This function seeks to keep the blob element moving along // multiple different types of surfaces (climbing walls, etc) //--------------------------------------------------------- #define BLOB_TRACE_HEIGHT 8.0f void CBlobElement::ModifyVelocityForSurface( float flInterval, float flSpeed ) { trace_t tr; Vector vecStart = GetAbsOrigin(); Vector up = Vector( 0, 0, BLOB_TRACE_HEIGHT ); Vector vecWishedGoal = vecStart + (GetAbsVelocity() * flInterval); UTIL_TraceLine( vecStart + up, vecWishedGoal + up, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); //NDebugOverlay::Line( tr.startpos, tr.endpos, 255, 0, 0, false, 0.1f ); m_bOnWall = false; if( tr.fraction == 1.0f ) { UTIL_TraceLine( vecWishedGoal + up, vecWishedGoal - (up * 2.0f), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); //NDebugOverlay::Line( tr.startpos, tr.endpos, 255, 255, 0, false, 0.1f ); tr.endpos.z += MOVE_HEIGHT_EPSILON; } else { //NDebugOverlay::Cross3D( GetAbsOrigin(), 16, 255, 255, 0, false, 0.025f ); m_bOnWall = true; if( tr.m_pEnt != NULL && !tr.m_pEnt->IsWorld() ) { IPhysicsObject *pPhysics = tr.m_pEnt->VPhysicsGetObject(); if( pPhysics != NULL ) { Vector vecMassCenter; Vector vecMassCenterWorld; vecMassCenter = pPhysics->GetMassCenterLocalSpace(); pPhysics->LocalToWorld( &vecMassCenterWorld, vecMassCenter ); if( tr.endpos.z > vecMassCenterWorld.z ) { pPhysics->ApplyForceOffset( (-150.0f * m_flRandomEightyPercent) * tr.plane.normal, tr.endpos ); } } } } Vector vecDir = tr.endpos - vecStart; VectorNormalize( vecDir ); SetElementVelocity( vecDir * flSpeed, false ); } //--------------------------------------------------------- // Set velocity that will carry me towards a specified entity // Most often used to move along with the npc_blob that // is directing me. //--------------------------------------------------------- void CBlobElement::MoveTowardsTargetEntity( float speed ) { CBaseEntity *pTarget = m_hTargetEntity.Get(); if( pTarget != NULL ) { // Try to attack my target's enemy directly if I can. CBaseEntity *pTargetEnemy = pTarget->GetEnemy(); if( pTargetEnemy != NULL ) { pTarget = pTargetEnemy; } Vector vecDir = pTarget->WorldSpaceCenter() - GetAbsOrigin(); vecDir.NormalizeInPlace(); SetElementVelocity( vecDir * speed, true ); } else { SetElementVelocity( vec3_origin, true ); } } //--------------------------------------------------------- // Set velocity that will take me towards a specified location. // This is often used to send all blob elements to specific // locations, causing the blob to appear as though it has // formed a specific shape. //--------------------------------------------------------- void CBlobElement::MoveTowardsTargetLocation( float speed ) { Vector vecDir = m_vecTargetLocation - GetAbsOrigin(); float dist = VectorNormalize( vecDir ); //!!!HACKHACK - how about a real way to tell if we've reached our goal? if( dist <= 8.0f ) { SetActiveMovementRule( BLOB_MOVE_DONT_MOVE ); } speed = MIN( dist, speed ); SetElementVelocity( vecDir * speed, true ); } //--------------------------------------------------------- // Pick new random numbers for the parameters that create // variations in movement. //--------------------------------------------------------- void CBlobElement::ReconfigureRandomParams() { m_flSinePhase = random->RandomFloat( 0.01f, 0.9f ); m_flSineFrequency = random->RandomFloat( 10.0f, 20.0f ); m_flSineAmplitude = random->RandomFloat( 0.5f, 1.5f ); } //--------------------------------------------------------- // Adjust velocity if this element is moving faster than // flMaxSpeed or slower than flMinSpeed //--------------------------------------------------------- void CBlobElement::EnforceSpeedLimits( float flMinSpeed, float flMaxSpeed ) { Vector vecVelocity = GetAbsVelocity(); float flSpeed = VectorNormalize( vecVelocity ); if( flSpeed > flMaxSpeed ) { SetElementVelocity( vecVelocity * flMaxSpeed, true ); } else if( flSpeed < flMinSpeed ) { SetElementVelocity( vecVelocity * flMinSpeed, true ); } } //========================================================= // Custom schedules //========================================================= enum { SCHED_MYCUSTOMSCHEDULE = LAST_SHARED_SCHEDULE, }; //========================================================= // Custom tasks //========================================================= enum { TASK_MYCUSTOMTASK = LAST_SHARED_TASK, }; //========================================================= // Custom Conditions //========================================================= enum { COND_MYCUSTOMCONDITION = LAST_SHARED_CONDITION, }; //========================================================= //========================================================= class CNPC_Blob : public CAI_BaseNPC { DECLARE_CLASS( CNPC_Blob, CAI_BaseNPC ); public: CNPC_Blob(); void Precache( void ); void Spawn( void ); Class_T Classify( void ); void RunAI(); void GatherConditions( void ); int SelectSchedule( void ); int GetSoundInterests( void ) { return (SOUND_BUGBAIT); } void ComputeCentroid(); void DoBlobBatchedAI( int iStart, int iEnd ); int ComputeBatchSize(); void AdvanceBatch(); int GetBatchStart(); int GetBatchEnd(); CBlobElement *CreateNewElement(); void InitializeElements(); void RecomputeIdealElementDist(); void RemoveAllElementsExcept( int iExempt ); void RemoveExcessElements( int iNumElements ); void AddNewElements( int iNumElements ); void FormShapeFromPath( string_t iszPathName ); void SetRadius( float flRadius ); DECLARE_DATADESC(); int m_iNumElements; bool m_bInitialized; int m_iBatchStart; Vector m_vecCentroid; float m_flMinElementDist; CUtlVector >m_Elements; DEFINE_CUSTOM_AI; public: void InputFormPathShape( inputdata_t &inputdata ); void InputSetRadius( inputdata_t &inputdata ); void InputChaseEntity( inputdata_t &inputdata ); void InputIsolateElement( inputdata_t &inputdata ); void InputFormHemisphere( inputdata_t &inputdata ); void InputFormTwoSpheres( inputdata_t &inputdata ); public: Vector m_vecAvoidOrigin[ BLOB_MAX_AVOID_ORIGINS ]; float m_flAvoidRadiusSqr; private: int m_iReconfigureElement; int m_iNumAvoidOrigins; bool m_bEatCombineHack; }; LINK_ENTITY_TO_CLASS( npc_blob, CNPC_Blob ); IMPLEMENT_CUSTOM_AI( npc_blob,CNPC_Blob ); //--------------------------------------------------------- // Save/Restore //--------------------------------------------------------- BEGIN_DATADESC( CNPC_Blob ) DEFINE_FIELD( m_iNumElements, FIELD_INTEGER ), DEFINE_FIELD( m_bInitialized, FIELD_BOOLEAN ), DEFINE_FIELD( m_iBatchStart, FIELD_INTEGER ), DEFINE_FIELD( m_vecCentroid, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_flMinElementDist, FIELD_FLOAT ), DEFINE_FIELD( m_iReconfigureElement, FIELD_INTEGER ), DEFINE_UTLVECTOR( m_Elements, FIELD_EHANDLE ), DEFINE_INPUTFUNC( FIELD_STRING, "FormPathShape", InputFormPathShape ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetRadius", InputSetRadius ), DEFINE_INPUTFUNC( FIELD_STRING, "ChaseEntity", InputChaseEntity ), DEFINE_INPUTFUNC( FIELD_INTEGER, "IsolateElement", InputIsolateElement ), DEFINE_INPUTFUNC( FIELD_VOID, "FormHemisphere", InputFormHemisphere ), DEFINE_INPUTFUNC( FIELD_VOID, "FormTwoSpheres", InputFormTwoSpheres ), END_DATADESC() //--------------------------------------------------------- //--------------------------------------------------------- CNPC_Blob::CNPC_Blob() { m_iNumElements = 0; m_bInitialized = false; m_iBatchStart = 0; } //----------------------------------------------------------------------------- // Purpose: Initialize the custom schedules // Input : // Output : //----------------------------------------------------------------------------- void CNPC_Blob::InitCustomSchedules(void) { INIT_CUSTOM_AI(CNPC_Blob); ADD_CUSTOM_TASK(CNPC_Blob, TASK_MYCUSTOMTASK); ADD_CUSTOM_SCHEDULE(CNPC_Blob, SCHED_MYCUSTOMSCHEDULE); ADD_CUSTOM_CONDITION(CNPC_Blob, COND_MYCUSTOMCONDITION); } //----------------------------------------------------------------------------- // Purpose: // // //----------------------------------------------------------------------------- void CNPC_Blob::Precache( void ) { PrecacheModel( NPC_BLOB_MODEL ); UTIL_PrecacheOther( "blob_element" ); BaseClass::Precache(); } //----------------------------------------------------------------------------- // Purpose: // // //----------------------------------------------------------------------------- void CNPC_Blob::Spawn( void ) { Precache(); SetModel( NPC_BLOB_MODEL ); SetHullType(HULL_TINY); SetHullSizeNormal(); SetSolid( SOLID_NONE ); AddSolidFlags( FSOLID_NOT_STANDABLE ); SetMoveType( MOVETYPE_STEP ); SetBloodColor( BLOOD_COLOR_RED ); m_iHealth = INT_MAX; m_flFieldOfView = -1.0f; m_NPCState = NPC_STATE_NONE; CapabilitiesClear(); CapabilitiesAdd( bits_CAP_MOVE_GROUND ); m_Elements.RemoveAll(); NPCInit(); AddEffects( EF_NODRAW ); m_flMinElementDist = blob_mindist.GetFloat(); } //----------------------------------------------------------------------------- // Purpose: // // // Output : //----------------------------------------------------------------------------- Class_T CNPC_Blob::Classify( void ) { return CLASS_PLAYER_ALLY; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Blob::RunAI() { BaseClass::RunAI(); if( !m_bInitialized ) { // m_bInitialized is set to false in the constructor. So this bit of // code runs one time, the first time I think. Msg("I need to initialize\n"); InitializeElements(); m_bInitialized = true; return; } int iIdealNumElements = blob_numelements.GetInt(); if( iIdealNumElements != m_iNumElements ) { int delta = iIdealNumElements - m_iNumElements; if( delta < 0 ) { delta = -delta; delta = MIN(delta, 5 ); RemoveExcessElements( delta ); if( m_iReconfigureElement > m_iNumElements ) { // Start this index over at zero, if it is past the new end of the utlvector. m_iReconfigureElement = 0; } } else { delta = MIN(delta, 5 ); AddNewElements( delta ); } RecomputeIdealElementDist(); } ComputeCentroid(); if( npc_blob_show_centroid.GetBool() ) { NDebugOverlay::Cross3D( m_vecCentroid + Vector( 0, 0, 12 ), 32, 0, 255, 0, false, 0.025f ); } if( npc_blob_use_threading.GetBool() ) { IterRangeParallel( this, &CNPC_Blob::DoBlobBatchedAI, 0, m_Elements.Count() ); } else { DoBlobBatchedAI( 0, m_Elements.Count() ); } if( GetEnemy() != NULL ) { float flEnemyDistSqr = m_vecCentroid.DistToSqr( GetEnemy()->GetAbsOrigin() ); if( flEnemyDistSqr <= Square( 32.0f ) ) { if( GetEnemy()->Classify() == CLASS_COMBINE ) { if( !m_bEatCombineHack ) { variant_t var; var.SetFloat( 0 ); g_EventQueue.AddEvent( GetEnemy(), "HitByBugBait", 0.0f, this, this ); g_EventQueue.AddEvent( GetEnemy(), "SetHealth", var, 3.0f, this, this ); m_bEatCombineHack = true; blob_radius.SetValue( 48.0f ); RecomputeIdealElementDist(); } } else { CTakeDamageInfo info; info.SetAttacker( this ); info.SetInflictor( this ); info.SetDamage( 5 ); info.SetDamageType( DMG_SLASH ); info.SetDamageForce( Vector( 0, 0, 1 ) ); GetEnemy()->TakeDamage( info ); } } } SetNextThink( gpGlobals->curtime + npc_blob_think_interval.GetFloat() ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Blob::GatherConditions( void ) { if( m_bEatCombineHack ) { // We just ate someone. if( !GetEnemy() || !GetEnemy()->IsAlive() ) { m_bEatCombineHack = false; blob_radius.SetValue( 160.0f ); RecomputeIdealElementDist(); } } BaseClass::GatherConditions(); } //----------------------------------------------------------------------------- // Either stand still or chase the enemy, for now. //----------------------------------------------------------------------------- int CNPC_Blob::SelectSchedule( void ) { if( GetEnemy() == NULL ) return SCHED_IDLE_STAND; return SCHED_CHASE_ENEMY; } //----------------------------------------------------------------------------- // Average the origin of all elements to get the centroid for the group //----------------------------------------------------------------------------- void CNPC_Blob::ComputeCentroid() { m_vecCentroid = vec3_origin; for( int i = 0 ; i < m_Elements.Count() ; i++ ) { m_vecCentroid += m_Elements[ i ]->GetAbsOrigin(); } m_vecCentroid /= m_Elements.Count(); } //----------------------------------------------------------------------------- // Run all of the AI for elements within the range iStart to iEnd //----------------------------------------------------------------------------- void CNPC_Blob::DoBlobBatchedAI( int iStart, int iEnd ) { float flInterval = gpGlobals->curtime - GetLastThink(); // Local fields for sin-wave movement variance float flMySine; float flAmplitude = npc_blob_sin_amplitude.GetFloat(); float flMyAmplitude; Vector vecRight; Vector vecForward; // Local fields for attract/repel float minDistSqr = Square( m_flMinElementDist ); float flBlobSpeed = blob_element_speed.GetFloat(); float flSpeed; // Local fields for speed limiting float flMinSpeed = blob_element_speed.GetFloat() * 0.5f; float flMaxSpeed = blob_element_speed.GetFloat() * 1.5f; bool bEnforceSpeedLimit; bool bEnforceRelativePositions; bool bDoMovementVariation; bool bDoOrientation = npc_blob_use_orientation.GetBool(); float flIdleSpeedFactor = npc_blob_idle_speed_factor.GetFloat(); // Group cohesion float flBlobRadiusSqr = Square( blob_radius.GetFloat() + 48.0f ); // Four feet of fudge // Build a right-hand vector along which we'll add some sine wave data to give each // element a unique insect-like undulation along an axis perpendicular to their path, // which makes the entire group look far less orderly if( GetEnemy() != NULL ) { // If I have an enemy, the right-hand vector is perpendicular to a straight line // from the group's centroid to the enemy's origin. vecForward = GetEnemy()->GetAbsOrigin() - m_vecCentroid; VectorNormalize( vecForward ); vecRight.x = vecForward.y; vecRight.y = -vecForward.x; } else { // If there is no enemy, wobble along the axis from the centroid to me. vecForward = GetAbsOrigin() - m_vecCentroid; VectorNormalize( vecForward ); vecRight.x = vecForward.y; vecRight.y = -vecForward.x; } //-- // MAIN LOOP - Run all of the elements in the set iStart to iEnd //-- for( int i = iStart ; i < iEnd ; i++ ) { CBlobElement *pThisElement = m_Elements[ i ]; //-- // Initial movement //-- // Start out with bEnforceSpeedLimit set to false. This is because an element // can't overspeed if it's moving undisturbed towards its target entity or // target location. An element can only under or overspeed when it is repelled // by multiple other elements in the group. See "Relative Positions" below. // // Initialize some 'defaults' that may be changed for each iteration of this loop bEnforceSpeedLimit = false; bEnforceRelativePositions = true; bDoMovementVariation = true; flSpeed = flBlobSpeed; switch( pThisElement->GetActiveMovementRule() ) { case BLOB_MOVE_DONT_MOVE: { pThisElement->SetElementVelocity( vec3_origin, true ); trace_t tr; Vector vecOrigin = pThisElement->GetAbsOrigin(); UTIL_TraceLine( vecOrigin, vecOrigin - Vector( 0, 0, 16), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); if( tr.fraction < 1.0f ) { QAngle angles; VectorAngles( tr.plane.normal, angles ); float flSwap = angles.x; angles.x = -angles.y; angles.y = flSwap; pThisElement->SetAbsAngles( angles ); } } continue; break; case BLOB_MOVE_TO_TARGET_LOCATION: { Vector vecDiff = pThisElement->GetAbsOrigin() - pThisElement->m_vecTargetLocation; if( vecDiff.Length2DSqr() <= Square(80.0f) ) { // Don't shove this guy around any more, let him get to his goal position. flSpeed *= 0.5f; bEnforceRelativePositions = false; bDoMovementVariation = false; } pThisElement->MoveTowardsTargetLocation( flSpeed ); } break; case BLOB_MOVE_TO_TARGET_ENTITY: { if( !IsMoving() && GetEnemy() == NULL ) { if( pThisElement->GetAbsOrigin().DistToSqr( GetAbsOrigin() ) <= flBlobRadiusSqr ) { flSpeed = (flSpeed * flIdleSpeedFactor) * pThisElement->m_flRandomEightyPercent; } } pThisElement->MoveTowardsTargetEntity( flSpeed ); } break; default: Msg("ERROR: Blob Element with unspecified Movement Rule\n"); break; } //--- // Relative positions //-- // Check this element against ALL other elements. If the two elements are closer // than the allowed minimum distance, repel this element away. (The other element // will repel when its AI runs). A single element can be repelled by many other // elements. This is why bEnforceSpeedLimit is set to true if any of the repelling // code runs for this element. Multiple attempts to repel an element in the same // direction will cause overspeed. Conflicting attempts to repel an element in opposite // directions will cause underspeed. Vector vecDir = Vector( 0, 0, 0 ); Vector vecThisElementOrigin = pThisElement->GetAbsOrigin(); if( bEnforceRelativePositions ) { for( int j = 0 ; j < m_Elements.Count() ; j++ ) { // This is the innermost loop! We should optimize here, if anywhere. // If this element is on the wall, then don't be repelled by anyone. Repelling // elements that are trying to climb a wall usually make them look like they // fall off the wall a few times while climbing. if( pThisElement->m_bOnWall ) continue; CBlobElement *pThatElement = m_Elements[ j ]; if( i != j ) { Vector vecThatElementOrigin = pThatElement->GetAbsOrigin(); float distSqr = vecThisElementOrigin.DistToSqr( vecThatElementOrigin ); if( distSqr < minDistSqr ) { // Too close to the other element. Move away. float flRepelSpeed; Vector vecRepelDir = ( vecThisElementOrigin - vecThatElementOrigin ); vecRepelDir.NormalizeInPlace(); flRepelSpeed = (flSpeed * ( 1.0f - ( distSqr / minDistSqr ) ) ) * pThatElement->GetSinePhase(); pThisElement->AddElementVelocity( vecRepelDir * flRepelSpeed, true ); // Since we altered this element's velocity after it was initially set, there's a chance // that the sums of multiple vectors will cause the element to over or underspeed, so // mark it for speed limit enforcement bEnforceSpeedLimit = true; } } } } //-- // Movement variation //-- if( bDoMovementVariation ) { flMySine = sin( gpGlobals->curtime * pThisElement->GetSineFrequency() ); flMyAmplitude = flAmplitude * pThisElement->GetSineAmplitude(); pThisElement->AddElementVelocity( vecRight * (flMySine * flMyAmplitude), true ); } // Avoidance for( int a = 0 ; a < m_iNumAvoidOrigins ; a++ ) { Vector vecAvoidDir = pThisElement->GetAbsOrigin() - m_vecAvoidOrigin[ a ]; if( vecAvoidDir.LengthSqr() <= (m_flAvoidRadiusSqr * pThisElement->m_flRandomEightyPercent) ) { VectorNormalize( vecAvoidDir ); pThisElement->AddElementVelocity( vecAvoidDir * (flSpeed * 2.0f), true ); break; } } //-- // Speed limits //--- if( bEnforceSpeedLimit == true ) { pThisElement->EnforceSpeedLimits( flMinSpeed, flMaxSpeed ); } //-- // Wall crawling //-- pThisElement->ModifyVelocityForSurface( flInterval, flSpeed ); // For identifying stuck elements. pThisElement->m_vecPrevOrigin = pThisElement->GetAbsOrigin(); pThisElement->m_flDistFromCentroidSqr = pThisElement->m_vecPrevOrigin.DistToSqr( m_vecCentroid ); // Orientation if( bDoOrientation ) { QAngle angles; VectorAngles( pThisElement->GetAbsVelocity(), angles ); pThisElement->SetAbsAngles( angles ); } /* //-- // Stragglers/Group integrity // if( pThisElement->m_flDistFromCentroidSqr > flStragglerDistSqr ) { NDebugOverlay::Line( pThisElement->GetAbsOrigin(), m_vecCentroid, 255, 0, 0, false, 0.025f ); } */ } } //----------------------------------------------------------------------------- // Throw out all elements and their entities except for the the specified // index into the UTILVector. This is useful for isolating elements that // get into a bad state. //----------------------------------------------------------------------------- void CNPC_Blob::RemoveAllElementsExcept( int iExempt ) { if( m_Elements.Count() == 1 ) return; m_Elements[ 0 ].Set( m_Elements[ iExempt ].Get() ); for( int i = 1 ; i < m_Elements.Count() ; i++ ) { if( i != iExempt ) { m_Elements[ i ]->SUB_Remove(); } } m_Elements.RemoveMultiple( 1, m_Elements.Count() - 1 ); m_iNumElements = 1; } //----------------------------------------------------------------------------- // Purpose: The blob has too many elements. Locate good candidates and remove // this many elements. //----------------------------------------------------------------------------- void CNPC_Blob::RemoveExcessElements( int iNumElements ) { // For now we're not assessing candidates, just blindly removing. int i; for( i = 0 ; i < iNumElements ; i++ ) { int iLastElement = m_iNumElements - 1; // Nuke the associated entity m_Elements[ iLastElement ]->SUB_Remove(); m_Elements.Remove( iLastElement ); m_iNumElements--; } } //----------------------------------------------------------------------------- // Purpose: This blob has too few elements. Add this many elements by stacking // them on top of existing elements and allowing them to disperse themselves // into the blob. //----------------------------------------------------------------------------- void CNPC_Blob::AddNewElements( int iNumElements ) { int i; // Keep track of how many elements we had when we came into this function. // Since the new elements copy their origins from existing elements, we only want // to copy origins from elements that existed before we came into this function. // Otherwise, the more elements we create while in this function, the more likely it // becomes that several of them will stack on the same origin. int iInitialElements = m_iNumElements; for( i = 0 ; i < iNumElements ; i++ ) { CBlobElement *pElement = CreateNewElement(); if( pElement != NULL ) { // Copy the origin of some element that is not me. This will make the expansion // of the group easier on the eye, since this element will spawn inside of some // other element, and then be pushed out by the blob's repel rules. int iCopyElement = random->RandomInt( 0, iInitialElements - 1 ); pElement->SetAbsOrigin( m_Elements[iCopyElement]->GetAbsOrigin() ); } } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- #define BLOB_MAX_VERTS 128 void CNPC_Blob::FormShapeFromPath( string_t iszPathName ) { Vector vertex[ BLOB_MAX_VERTS ]; int i; int iNumVerts = 0; for ( i = 0 ; i < BLOB_MAX_VERTS ; i++ ) { if( iszPathName == NULL_STRING ) { //Msg("Terminal path\n"); break; } CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, iszPathName ); if( pEntity != NULL ) { bool bClosedPath = false; for( int j = 0 ; j < i ; j++ ) { // Stop if we reach a vertex that's already in the array (closed path) if( vertex[ j ] == pEntity->GetAbsOrigin() ) { //Msg("Closed path!\n"); bClosedPath = true; break; } } vertex[ i ] = pEntity->GetAbsOrigin(); iszPathName = pEntity->m_target; iNumVerts++; if( bClosedPath ) break; } } //Msg("%d verts found in path!\n", iNumVerts); float flPathLength = 0.0f; float flDistribution; for( i = 0 ; i < iNumVerts - 1 ; i++ ) { Vector vecDiff = vertex[ i ] - vertex[ i + 1 ]; flPathLength += vecDiff.Length(); } flDistribution = flPathLength / m_iNumElements; Msg("Path length is %f, distribution is %f\n", flPathLength, flDistribution ); int element = 0; for( i = 0 ; i < iNumVerts - 1 ; i++ ) { //NDebugOverlay::Line( vertex[ i ], vertex[ i + 1 ], 0, 255, 0, false, 10.0f ); Vector vecDiff = vertex[ i + 1 ] - vertex[ i ]; Vector vecStart = vertex[ i ]; float flSegmentLength = VectorNormalize( vecDiff ); float flStep; for( flStep = 0.0f ; flStep < flSegmentLength ; flStep += flDistribution ) { //NDebugOverlay::Cross3D( vecStart + vecDiff * flStep, 16, 255, 255, 255, false, 10.0f ); m_Elements[ element ]->SetTargetLocation( vecStart + vecDiff * flStep ); m_Elements[ element ]->SetActiveMovementRule( BLOB_MOVE_TO_TARGET_LOCATION ); element++; if( element == m_iNumElements ) return; } } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Blob::SetRadius( float flRadius ) { blob_radius.SetValue( flRadius ); RecomputeIdealElementDist(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Blob::InputFormPathShape( inputdata_t &inputdata ) { string_t shape = inputdata.value.StringID(); if( shape == NULL_STRING ) return; //Msg("I'm supposed to form some shape called:%s\n", shape ); FormShapeFromPath( shape ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Blob::InputSetRadius( inputdata_t &inputdata ) { float flNewRadius = inputdata.value.Float(); SetRadius( flNewRadius ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Blob::InputChaseEntity( inputdata_t &inputdata ) { CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, inputdata.value.StringID(), NULL, inputdata.pActivator, inputdata.pCaller ); if ( pEntity ) { for( int i = 0 ; i < m_Elements.Count() ; i++ ) { CBlobElement *pElement = m_Elements[ i ]; pElement->SetTargetEntity( pEntity ); pElement->SetActiveMovementRule( BLOB_MOVE_TO_TARGET_ENTITY ); } } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Blob::InputIsolateElement( inputdata_t &inputdata ) { int iElement = inputdata.value.Int(); RemoveAllElementsExcept( iElement ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Blob::InputFormHemisphere( inputdata_t &inputdata ) { Vector center = GetAbsOrigin(); const float flRadius = 240.0f; Vector vecDir; for( int i = 0 ; i < m_Elements.Count() ; i++ ) { CBlobElement *pElement = m_Elements[ i ]; // Compute a point around my center vecDir.x = random->RandomFloat( -1, 1 ); vecDir.y = random->RandomFloat( -1, 1 ); vecDir.z = random->RandomFloat( 0, 1 ); VectorNormalize( vecDir ); pElement->SetTargetLocation( center + vecDir * flRadius ); pElement->SetActiveMovementRule( BLOB_MOVE_TO_TARGET_LOCATION ); } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Blob::InputFormTwoSpheres( inputdata_t &inputdata ) { Vector center = GetAbsOrigin(); Vector sphere1 = GetAbsOrigin() + Vector( 120.0f, 0, 120.0f ); Vector sphere2 = GetAbsOrigin() + Vector( -120.0f, 0, 120.0f ); const float flRadius = 100.0f; Vector vecDir; int batchSize = m_Elements.Count() / 2; for( int i = 0 ; i < batchSize ; i++ ) { CBlobElement *pElement = m_Elements[ i ]; // Compute a point around my center vecDir.x = random->RandomFloat( -1, 1 ); vecDir.y = random->RandomFloat( -1, 1 ); vecDir.z = random->RandomFloat( -1, 1 ); VectorNormalize( vecDir ); pElement->SetTargetLocation( sphere1 + vecDir * flRadius ); pElement->SetActiveMovementRule( BLOB_MOVE_TO_TARGET_LOCATION ); } for( int i = batchSize ; i < m_Elements.Count() ; i++ ) { CBlobElement *pElement = m_Elements[ i ]; // Compute a point around my center vecDir.x = random->RandomFloat( -1, 1 ); vecDir.y = random->RandomFloat( -1, 1 ); vecDir.z = random->RandomFloat( -1, 1 ); VectorNormalize( vecDir ); pElement->SetTargetLocation( sphere2 + vecDir * flRadius ); pElement->SetActiveMovementRule( BLOB_MOVE_TO_TARGET_LOCATION ); } } //----------------------------------------------------------------------------- // Get the index of the element to start processing with for this batch. //----------------------------------------------------------------------------- int CNPC_Blob::GetBatchStart() { return m_iBatchStart; } //----------------------------------------------------------------------------- // Get the index of the element to stop processing with for this batch. //----------------------------------------------------------------------------- int CNPC_Blob::GetBatchEnd() { int batchDone = m_iBatchStart + ComputeBatchSize(); batchDone = MIN( batchDone, m_Elements.Count() ); return batchDone; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- int CNPC_Blob::ComputeBatchSize() { int batchSize = m_Elements.Count() / ( 100 / blob_batchpercent.GetInt() ); return batchSize; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Blob::AdvanceBatch() { m_iBatchStart += ComputeBatchSize(); if( m_iBatchStart >= m_Elements.Count() ) m_iBatchStart = 0; } //----------------------------------------------------------------------------- // Creates a new blob element from scratch and adds it to the blob //----------------------------------------------------------------------------- CBlobElement *CNPC_Blob::CreateNewElement() { CBlobElement *pElement = static_cast(CreateEntityByName( "blob_element" )); if( pElement != NULL ) { pElement->SetOwnerEntity( this ); pElement->SetSinePhase( fabs( sin(((float)m_iNumElements)/10.0f) ) ); pElement->SetActiveMovementRule( BLOB_MOVE_TO_TARGET_ENTITY ); pElement->SetTargetEntity( this ); pElement->m_iElementNumber = m_iNumElements; m_iNumElements++; pElement->Spawn(); m_Elements.AddToTail( pElement ); return pElement; } Warning("Blob could not spawn new element!\n"); return NULL; } //----------------------------------------------------------------------------- // Create, initialize, and distribute all blob elements //----------------------------------------------------------------------------- void CNPC_Blob::InitializeElements() { // Squirt all of the elements out into a circle int i; QAngle angDistributor( 0, 0, 0 ); int iNumElements = blob_numelements.GetInt(); float step = 360.0f / ((float)iNumElements); for( i = 0 ; i < iNumElements ; i++ ) { Vector vecDir; Vector vecDest; AngleVectors( angDistributor, &vecDir, NULL, NULL ); vecDest = WorldSpaceCenter() + vecDir * 64.0f; CBlobElement *pElement = CreateNewElement(); if( !pElement ) { Msg("Blob could not create all elements!!\n"); return; } trace_t tr; UTIL_TraceLine( vecDest, vecDest + Vector (0, 0, MIN_COORD_FLOAT), MASK_SHOT, pElement, COLLISION_GROUP_NONE, &tr ); pElement->SetAbsOrigin( tr.endpos + Vector( 0, 0, 1 ) ); angDistributor.y += step; } CBaseEntity *pEntity = gEntList.FindEntityByClassname( NULL, "info_target" ); for( i = 0 ; i < BLOB_MAX_AVOID_ORIGINS ; i++ ) { if( pEntity ) { if( pEntity->NameMatches("avoid") ) { m_vecAvoidOrigin[ i ] = pEntity->GetAbsOrigin(); m_flAvoidRadiusSqr = Square( 120.0f ); m_iNumAvoidOrigins++; } pEntity = gEntList.FindEntityByClassname( pEntity, "info_target" ); } else { break; } } Msg("%d avoid origins\n", m_iNumAvoidOrigins ); RecomputeIdealElementDist(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Blob::RecomputeIdealElementDist() { float radius = blob_radius.GetFloat(); float area = M_PI * Square(radius); //Msg("Area of blob is: %f\n", area ); //m_flMinElementDist = 2.75f * sqrt( area / m_iNumElements ); m_flMinElementDist = M_PI * sqrt( area / m_iNumElements ); //Msg("New element dist: %f\n", m_flMinElementDist ); }