//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ // //=============================================================================// // nav_area.cpp // AI Navigation areas // Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 #include "cbase.h" #include "tier0/vprof.h" #include "tier0/tslist.h" #include "tier1/utlhash.h" #include "vstdlib/jobthread.h" #include "nav_mesh.h" #include "nav_node.h" #include "nav_pathfind.h" #include "nav_colors.h" #include "fmtstr.h" #include "props_shared.h" #include "func_breakablesurf.h" #ifdef TERROR #include "func_elevator.h" #include "AmbientLight.h" #endif #include "Color.h" #include "collisionutils.h" #include "functorutils.h" #include "team.h" #include "nav_entities.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" extern void HintMessageToAllPlayers( const char *message ); unsigned int CNavArea::m_nextID = 1; NavAreaVector TheNavAreas; unsigned int CNavArea::m_masterMarker = 1; CNavArea *CNavArea::m_openList = NULL; CNavArea *CNavArea::m_openListTail = NULL; bool CNavArea::m_isReset = false; uint32 CNavArea::s_nCurrVisTestCounter = 0; ConVar nav_coplanar_slope_limit( "nav_coplanar_slope_limit", "0.99", FCVAR_CHEAT ); ConVar nav_coplanar_slope_limit_displacement( "nav_coplanar_slope_limit_displacement", "0.7", FCVAR_CHEAT ); ConVar nav_split_place_on_ground( "nav_split_place_on_ground", "0", FCVAR_CHEAT, "If true, nav areas will be placed flush with the ground when split." ); ConVar nav_area_bgcolor( "nav_area_bgcolor", "0 0 0 30", FCVAR_CHEAT, "RGBA color to draw as the background color for nav areas while editing." ); ConVar nav_corner_adjust_adjacent( "nav_corner_adjust_adjacent", "18", FCVAR_CHEAT, "radius used to raise/lower corners in nearby areas when raising/lowering corners." ); ConVar nav_show_light_intensity( "nav_show_light_intensity", "0", FCVAR_CHEAT ); ConVar nav_debug_blocked( "nav_debug_blocked", "0", FCVAR_CHEAT ); ConVar nav_show_contiguous( "nav_show_continguous", "0", FCVAR_CHEAT, "Highlight non-contiguous connections" ); const float DEF_NAV_VIEW_DISTANCE = 1500.0; ConVar nav_max_view_distance( "nav_max_view_distance", "6000", FCVAR_CHEAT, "Maximum range for precomputed nav mesh visibility (0 = default 1500 units)" ); ConVar nav_update_visibility_on_edit( "nav_update_visibility_on_edit", "0", FCVAR_CHEAT, "If nonzero editing the mesh will incrementally recompue visibility" ); ConVar nav_potentially_visible_dot_tolerance( "nav_potentially_visible_dot_tolerance", "0.98", FCVAR_CHEAT ); ConVar nav_show_potentially_visible( "nav_show_potentially_visible", "0", FCVAR_CHEAT, "Show areas that are potentially visible from the current nav area" ); Color s_selectedSetColor( 255, 255, 200, 96 ); Color s_selectedSetBorderColor( 100, 100, 0, 255 ); Color s_dragSelectionSetBorderColor( 50, 50, 50, 255 ); static void SelectedSetColorChaged( IConVar *var, const char *pOldValue, float flOldValue ) { ConVarRef colorVar( var->GetName() ); Color *color = &s_selectedSetColor; if ( FStrEq( var->GetName(), "nav_selected_set_border_color" ) ) { color = &s_selectedSetBorderColor; } // Xbox compiler needs these to be in this explicit form // likely due to sscanf expecting word aligned boundaries int r = color->r(); int g = color->r(); int b = color->b(); int a = color->a(); int numFound = sscanf( colorVar.GetString(), "%d %d %d %d", &r, &g, &b, &a ); (*color)[0] = r; (*color)[1] = g; (*color)[2] = b; if ( numFound > 3 ) { (*color)[3] = a; } } ConVar nav_selected_set_color( "nav_selected_set_color", "255 255 200 96", FCVAR_CHEAT, "Color used to draw the selected set background while editing.", false, 0.0f, false, 0.0f, SelectedSetColorChaged ); ConVar nav_selected_set_border_color( "nav_selected_set_border_color", "100 100 0 255", FCVAR_CHEAT, "Color used to draw the selected set borders while editing.", false, 0.0f, false, 0.0f, SelectedSetColorChaged ); //-------------------------------------------------------------------------------------------------------------- CMemoryStack CNavVectorNoEditAllocator::m_memory; void *CNavVectorNoEditAllocator::m_pCurrent; int CNavVectorNoEditAllocator::m_nBytesCurrent; CNavVectorNoEditAllocator::CNavVectorNoEditAllocator() { m_pCurrent = NULL; m_nBytesCurrent = 0; } void CNavVectorNoEditAllocator::Reset() { m_memory.FreeAll(); m_pCurrent = NULL; m_nBytesCurrent = 0; } void *CNavVectorNoEditAllocator::Alloc( size_t nSize ) { if ( !m_memory.GetBase() ) { m_memory.Init( 1024*1024, 0, 0, 4 ); } m_pCurrent = (int *)m_memory.Alloc( nSize ); m_nBytesCurrent = nSize; return m_pCurrent; } void *CNavVectorNoEditAllocator::Realloc( void *pMem, size_t nSize ) { if ( pMem != m_pCurrent ) { Assert( 0 ); Error( "Nav mesh cannot be mutated after load\n" ); } if ( nSize > (size_t)m_nBytesCurrent ) { m_memory.Alloc( nSize - m_nBytesCurrent ); m_nBytesCurrent = nSize; } return m_pCurrent; } void CNavVectorNoEditAllocator::Free( void *pMem ) { } size_t CNavVectorNoEditAllocator::GetSize( void *pMem ) { if ( pMem != m_pCurrent ) { Assert( 0 ); Error( "Nav mesh cannot be mutated after load\n" ); } return m_nBytesCurrent; } //-------------------------------------------------------------------------------------------------------------- void CNavArea::CompressIDs( void ) { m_nextID = 1; FOR_EACH_VEC( TheNavAreas, id ) { CNavArea *area = TheNavAreas[id]; area->m_id = m_nextID++; // remove and re-add the area from the nav mesh to update the hashed ID TheNavMesh->RemoveNavArea( area ); TheNavMesh->AddNavArea( area ); } } //-------------------------------------------------------------------------------------------------------------- /** * Constructor used during normal runtime. */ CNavArea::CNavArea( void ) { m_marker = 0; m_nearNavSearchMarker = 0; m_damagingTickCount = 0; m_openMarker = 0; m_parent = NULL; m_parentHow = GO_NORTH; m_attributeFlags = 0; m_place = TheNavMesh->GetNavPlace(); m_isUnderwater = false; m_avoidanceObstacleHeight = 0.0f; m_totalCost = 0.0f; ResetNodes(); int i; for ( i=0; i 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) { m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); } else { m_invDxCorners = m_invDyCorners = 0; } m_neZ = corner.z; m_swZ = otherCorner.z; CalcDebugID(); } //-------------------------------------------------------------------------------------------------------------- /** * Build a nav area given the positions of its four corners. */ void CNavArea::Build( const Vector &nwCorner, const Vector &neCorner, const Vector &seCorner, const Vector &swCorner ) { m_nwCorner = nwCorner; m_seCorner = seCorner; m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; m_neZ = neCorner.z; m_swZ = swCorner.z; if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) { m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); } else { m_invDxCorners = m_invDyCorners = 0; } CalcDebugID(); } //-------------------------------------------------------------------------------------------------------------- /** * Used during generation phase to build nav areas from sampled nodes. */ void CNavArea::Build( CNavNode *nwNode, CNavNode *neNode, CNavNode *seNode, CNavNode *swNode ) { m_nwCorner = *nwNode->GetPosition(); m_seCorner = *seNode->GetPosition(); m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; m_neZ = neNode->GetPosition()->z; m_swZ = swNode->GetPosition()->z; m_node[ NORTH_WEST ] = nwNode; m_node[ NORTH_EAST ] = neNode; m_node[ SOUTH_EAST ] = seNode; m_node[ SOUTH_WEST ] = swNode; if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) { m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); } else { m_invDxCorners = m_invDyCorners = 0; } // mark internal nodes as part of this area AssignNodes( this ); CalcDebugID(); } //-------------------------------------------------------------------------------------------------------------- // Return a computed extent (XY is in m_nwCorner and m_seCorner, Z is computed) void CNavArea::GetExtent( Extent *extent ) const { extent->lo = m_nwCorner; extent->hi = m_seCorner; extent->lo.z = MIN( extent->lo.z, m_nwCorner.z ); extent->lo.z = MIN( extent->lo.z, m_seCorner.z ); extent->lo.z = MIN( extent->lo.z, m_neZ ); extent->lo.z = MIN( extent->lo.z, m_swZ ); extent->hi.z = MAX( extent->hi.z, m_nwCorner.z ); extent->hi.z = MAX( extent->hi.z, m_seCorner.z ); extent->hi.z = MAX( extent->hi.z, m_neZ ); extent->hi.z = MAX( extent->hi.z, m_swZ ); } //-------------------------------------------------------------------------------------------------------------- // returns the closest node along the given edge to the given point CNavNode *CNavArea::FindClosestNode( const Vector &pos, NavDirType dir ) const { if ( !HasNodes() ) return NULL; CUtlVector< CNavNode * > nodes; GetNodes( dir, &nodes ); CNavNode *bestNode = NULL; float bestDistanceSq = FLT_MAX; for ( int i=0; iGetPosition() ); if ( distSq < bestDistanceSq ) { bestDistanceSq = distSq; bestNode = nodes[i]; } } return bestNode; } //-------------------------------------------------------------------------------------------------------------- // build a vector of nodes along the given direction void CNavArea::GetNodes( NavDirType dir, CUtlVector< CNavNode * > *nodes ) const { if ( !nodes ) return; nodes->RemoveAll(); NavCornerType startCorner; NavCornerType endCorner; NavDirType traversalDirection; switch ( dir ) { case NORTH: startCorner = NORTH_WEST; endCorner = NORTH_EAST; traversalDirection = EAST; break; case SOUTH: startCorner = SOUTH_WEST; endCorner = SOUTH_EAST; traversalDirection = EAST; break; case EAST: startCorner = NORTH_EAST; endCorner = SOUTH_EAST; traversalDirection = SOUTH; break; case WEST: startCorner = NORTH_WEST; endCorner = SOUTH_WEST; traversalDirection = SOUTH; break; default: return; } CNavNode *node; for ( node = m_node[ startCorner ]; node && node != m_node[ endCorner ]; node = node->GetConnectedNode( traversalDirection ) ) { nodes->AddToTail( node ); } if ( node && node == m_node[ endCorner ] ) { nodes->AddToTail( node ); } } //-------------------------------------------------------------------------------------------------------------- class ForgetArea { public: ForgetArea( CNavArea *area ) { m_area = area; } bool operator() ( CBasePlayer *player ) { player->OnNavAreaRemoved( m_area ); return true; } bool operator() ( CBaseCombatCharacter *player ) { player->OnNavAreaRemoved( m_area ); return true; } CNavArea *m_area; }; //-------------------------------------------------------------------------------------------------------------- class AreaDestroyNotification { CNavArea *m_area; public: AreaDestroyNotification( CNavArea *area ) { m_area = area; } bool operator()( CNavLadder *ladder ) { ladder->OnDestroyNotify( m_area ); return true; } bool operator()( CNavArea *area ) { if ( area != m_area ) { area->OnDestroyNotify( m_area ); } return true; } }; //-------------------------------------------------------------------------------------------------------------- /** * Destructor */ CNavArea::~CNavArea() { // spot encounters aren't owned by anything else, so free them up here m_spotEncounters.PurgeAndDeleteElements(); // if we are resetting the system, don't bother cleaning up - all areas are being destroyed if (m_isReset) return; // tell the other areas and ladders we are going away AreaDestroyNotification notification( this ); TheNavMesh->ForAllAreas( notification ); TheNavMesh->ForAllLadders( notification ); // remove the area from the grid TheNavMesh->RemoveNavArea( this ); // make sure no players keep a pointer to this area ForgetArea forget( this ); ForEachActor( forget ); } //-------------------------------------------------------------------------------------------------------------- /** * Find elevator connections between areas */ void CNavArea::ConnectElevators( void ) { m_elevator = NULL; m_attributeFlags &= ~NAV_MESH_HAS_ELEVATOR; m_elevatorAreas.RemoveAll(); #ifdef TERROR // connect elevators CFuncElevator *elevator = NULL; while( ( elevator = (CFuncElevator *)gEntList.FindEntityByClassname( elevator, "func_elevator" ) ) != NULL ) { if ( elevator->GetNumFloors() < 2 ) { // broken elevator continue; } Extent elevatorExtent; elevator->CollisionProp()->WorldSpaceSurroundingBounds( &elevatorExtent.lo, &elevatorExtent.hi ); if ( IsOverlapping( elevatorExtent ) ) { // overlaps in 2D - check that this area is within the shaft of the elevator const Vector ¢er = GetCenter(); for( int f=0; fGetNumFloors(); ++f ) { const FloorInfo *floor = elevator->GetFloor( f ); const float tolerance = 30.0f; if ( center.z <= floor->height + tolerance && center.z >= floor->height - tolerance ) { if ( m_elevator ) { Warning( "Multiple elevators overlap navigation area #%d\n", GetID() ); break; } // this area is part of an elevator system m_elevator = elevator; m_attributeFlags |= NAV_MESH_HAS_ELEVATOR; // find the largest area overlapping this elevator on each other floor for( int of=0; ofGetNumFloors(); ++of ) { if ( of == f ) { // we are on this floor continue; } const FloorInfo *otherFloor = elevator->GetFloor( of ); // find the largest area at this floor CNavArea *floorArea = NULL; float floorAreaSize = 0.0f; FOR_EACH_VEC( TheNavAreas, it ) { CNavArea *area = TheNavAreas[ it ]; if ( area->IsOverlapping( elevatorExtent ) ) { if ( area->GetCenter().z <= otherFloor->height + tolerance && area->GetCenter().z >= otherFloor->height - tolerance ) { float size = area->GetSizeX() * area->GetSizeY(); if ( size > floorAreaSize ) { floorArea = area; floorAreaSize = size; } } } } if ( floorArea ) { // add this area to the set of areas reachable via elevator NavConnect con; con.area = floorArea; con.length = ( floorArea->GetCenter() - GetCenter() ).Length(); m_elevatorAreas.AddToTail( con ); } else { Warning( "Floor %d ('%s') of elevator at ( %3.2f, %3.2f, %3.2f ) has no matching navigation areas\n", of, otherFloor->name.ToCStr(), elevator->GetAbsOrigin().x, elevator->GetAbsOrigin().y, elevator->GetAbsOrigin().z ); } } // we found our floor break; } } } } #endif // TERROR } //-------------------------------------------------------------------------------------------------------------- /** * Invoked when map is initially loaded */ void CNavArea::OnServerActivate( void ) { ConnectElevators(); m_damagingTickCount = 0; ClearAllNavCostEntities(); } //-------------------------------------------------------------------------------------------------------------- /** * Invoked for each area when the round restarts */ void CNavArea::OnRoundRestart( void ) { // need to redo this here since func_elevators are deleted and recreated at round restart ConnectElevators(); m_damagingTickCount = 0; ClearAllNavCostEntities(); } #ifdef DEBUG_AREA_PLAYERCOUNTS //-------------------------------------------------------------------------------------------------------------- void CNavArea::IncrementPlayerCount( int teamID, int entIndex ) { ConColorMsg( Color( 128, 255, 128, 255 ), "%f: Adding ent %d (team %d) to area %d\n", gpGlobals->curtime, entIndex, teamID, GetID() ); teamID = teamID % MAX_NAV_TEAMS; Assert( !m_playerEntIndices[teamID].HasElement( entIndex ) ); if ( !m_playerEntIndices[teamID].HasElement( entIndex ) ) { m_playerEntIndices[teamID].AddToTail( entIndex ); } if (m_playerCount[ teamID ] == 255) { Warning( "CNavArea::IncrementPlayerCount: Overflow\n" ); return; } ++m_playerCount[ teamID ]; } //-------------------------------------------------------------------------------------------------------------- void CNavArea::DecrementPlayerCount( int teamID, int entIndex ) { ConColorMsg( Color( 128, 128, 255, 255 ), "%f: Removing ent %d (team %d) from area %d\n", gpGlobals->curtime, entIndex, teamID, GetID() ); teamID = teamID % MAX_NAV_TEAMS; Assert( m_playerEntIndices[teamID].HasElement( entIndex ) ); m_playerEntIndices[teamID].FindAndFastRemove( entIndex ); if (m_playerCount[ teamID ] == 0) { Warning( "CNavArea::IncrementPlayerCount: Underflow\n" ); return; } --m_playerCount[ teamID ]; } #endif // DEBUG_AREA_PLAYERCOUNTS //-------------------------------------------------------------------------------------------------------------- /** * This is invoked at the start of an incremental nav generation on pre-existing areas. */ void CNavArea::ResetNodes( void ) { for ( int i=0; iGetCenter() - GetCenter() ).Length(); m_connect[ dir ].AddToTail( con ); m_incomingConnect[ dir ].FindAndRemove( con ); NavDirType dirOpposite = OppositeDirection( dir ); con.area = this; if ( area->m_connect[ dirOpposite ].Find( con ) == area->m_connect[ dirOpposite ].InvalidIndex() ) { area->AddIncomingConnection( this, dirOpposite ); } //static char *dirName[] = { "NORTH", "EAST", "SOUTH", "WEST" }; //CONSOLE_ECHO( " Connected area #%d to #%d, %s\n", m_id, area->m_id, dirName[ dir ] ); } //-------------------------------------------------------------------------------------------------------------- /** * Connect this area to given ladder */ void CNavArea::ConnectTo( CNavLadder *ladder ) { float center = (ladder->m_top.z + ladder->m_bottom.z) * 0.5f; Disconnect( ladder ); // just in case if ( GetCenter().z > center ) { AddLadderDown( ladder ); } else { AddLadderUp( ladder ); } } //-------------------------------------------------------------------------------------------------------------- /** * Disconnect this area from given area */ void CNavArea::Disconnect( CNavArea *area ) { NavConnect connect; connect.area = area; for( int i = 0; iIsConnected( this, dirOpposite ) ) { AddIncomingConnection( area, dir ); } else { connect.area = this; area->m_incomingConnect[ dirOpposite ].FindAndRemove( connect ); } } } } //-------------------------------------------------------------------------------------------------------------- /** * Disconnect this area from given ladder */ void CNavArea::Disconnect( CNavLadder *ladder ) { NavLadderConnect con; con.ladder = ladder; for( int i=0; iGetPosition(); m_seCorner = *m_node[ SOUTH_EAST ]->GetPosition(); m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; m_neZ = m_node[ NORTH_EAST ]->GetPosition()->z; m_swZ = m_node[ SOUTH_WEST ]->GetPosition()->z; if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) { m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); } else { m_invDxCorners = m_invDyCorners = 0; } // reassign the adjacent area's internal nodes to the final area adjArea->AssignNodes( this ); // merge adjacency links - we gain all the connections that adjArea had MergeAdjacentConnections( adjArea ); // remove subsumed adjacent area TheNavAreas.FindAndRemove( adjArea ); TheNavMesh->OnEditDestroyNotify( adjArea ); TheNavMesh->DestroyArea( adjArea ); } //-------------------------------------------------------------------------------------------------------------- class LadderConnectionReplacement { CNavArea *m_originalArea; CNavArea *m_replacementArea; public: LadderConnectionReplacement( CNavArea *originalArea, CNavArea *replacementArea ) { m_originalArea = originalArea; m_replacementArea = replacementArea; } bool operator()( CNavLadder *ladder ) { if ( ladder->m_topForwardArea == m_originalArea ) ladder->m_topForwardArea = m_replacementArea; if ( ladder->m_topRightArea == m_originalArea ) ladder->m_topRightArea = m_replacementArea; if ( ladder->m_topLeftArea == m_originalArea ) ladder->m_topLeftArea = m_replacementArea; if ( ladder->m_topBehindArea == m_originalArea ) ladder->m_topBehindArea = m_replacementArea; if ( ladder->m_bottomArea == m_originalArea ) ladder->m_bottomArea = m_replacementArea; return true; } }; //-------------------------------------------------------------------------------------------------------------- /** * For merging with "adjArea" - pick up all of "adjArea"s connections */ void CNavArea::MergeAdjacentConnections( CNavArea *adjArea ) { // merge adjacency links - we gain all the connections that adjArea had int dir; for( dir = 0; dirm_connect[ dir ], it ) { NavConnect connect = adjArea->m_connect[ dir ][ it ]; if (connect.area != adjArea && connect.area != this) ConnectTo( connect.area, (NavDirType)dir ); } } // remove any references from this area to the adjacent area, since it is now part of us Disconnect( adjArea ); // Change other references to adjArea to refer instead to us // We can't just replace existing connections, as several adjacent areas may have been merged into one, // resulting in a large area adjacent to all of them ending up with multiple redunandant connections // into the merged area, one for each of the adjacent subsumed smaller ones. // If an area has a connection to the merged area, we must remove all references to adjArea, and add // a single connection to us. FOR_EACH_VEC( TheNavAreas, it ) { CNavArea *area = TheNavAreas[ it ]; if (area == this || area == adjArea) continue; for( dir = 0; dirm_connect[ dir ], cit ) { NavConnect connect = area->m_connect[ dir ][ cit ]; if (connect.area == adjArea) { connected = true; break; } } if (connected) { // remove all references to adjArea area->Disconnect( adjArea ); // remove all references to the new area area->Disconnect( this ); // add a single connection to the new area area->ConnectTo( this, (NavDirType) dir ); } } } // We gain all ladder connections adjArea had for( dir=0; dirm_ladder[ dir ], it ) { ConnectTo( adjArea->m_ladder[ dir ][ it ].ladder ); } } // All ladders that point to adjArea should point to us now LadderConnectionReplacement replacement( adjArea, this ); TheNavMesh->ForAllLadders( replacement ); } //-------------------------------------------------------------------------------------------------------------- /** * Assign internal nodes to the given area * NOTE: "internal" nodes do not include the east or south border nodes */ void CNavArea::AssignNodes( CNavArea *area ) { CNavNode *horizLast = m_node[ NORTH_EAST ]; for( CNavNode *vertNode = m_node[ NORTH_WEST ]; vertNode != m_node[ SOUTH_WEST ]; vertNode = vertNode->GetConnectedNode( SOUTH ) ) { for( CNavNode *horizNode = vertNode; horizNode != horizLast; horizNode = horizNode->GetConnectedNode( EAST ) ) { horizNode->AssignArea( area ); } horizLast = horizLast->GetConnectedNode( SOUTH ); } } //-------------------------------------------------------------------------------------------------------------- class SplitNotification { CNavArea *m_originalArea; CNavArea *m_alphaArea; CNavArea *m_betaArea; public: SplitNotification( CNavArea *originalArea, CNavArea *alphaArea, CNavArea *betaArea ) { m_originalArea = originalArea; m_alphaArea = alphaArea; m_betaArea = betaArea; } bool operator()( CNavLadder *ladder ) { ladder->OnSplit( m_originalArea, m_alphaArea, m_betaArea ); return true; } }; //-------------------------------------------------------------------------------------------------------------- /** * Split this area into two areas at the given edge. * Preserve all adjacency connections. * NOTE: This does not update node connections, only areas. */ bool CNavArea::SplitEdit( bool splitAlongX, float splitEdge, CNavArea **outAlpha, CNavArea **outBeta ) { CNavArea *alpha = NULL; CNavArea *beta = NULL; if (splitAlongX) { // +-----+->X // | A | // +-----+ // | B | // +-----+ // | // Y // don't do split if at edge of area if (splitEdge <= m_nwCorner.y + 1.0f) return false; if (splitEdge >= m_seCorner.y - 1.0f) return false; alpha = TheNavMesh->CreateArea(); alpha->m_nwCorner = m_nwCorner; alpha->m_seCorner.x = m_seCorner.x; alpha->m_seCorner.y = splitEdge; alpha->m_seCorner.z = GetZ( alpha->m_seCorner ); beta = TheNavMesh->CreateArea(); beta->m_nwCorner.x = m_nwCorner.x; beta->m_nwCorner.y = splitEdge; beta->m_nwCorner.z = GetZ( beta->m_nwCorner ); beta->m_seCorner = m_seCorner; alpha->ConnectTo( beta, SOUTH ); beta->ConnectTo( alpha, NORTH ); FinishSplitEdit( alpha, SOUTH ); FinishSplitEdit( beta, NORTH ); } else { // +--+--+->X // | | | // | A|B | // | | | // +--+--+ // | // Y // don't do split if at edge of area if (splitEdge <= m_nwCorner.x + 1.0f) return false; if (splitEdge >= m_seCorner.x - 1.0f) return false; alpha = TheNavMesh->CreateArea(); alpha->m_nwCorner = m_nwCorner; alpha->m_seCorner.x = splitEdge; alpha->m_seCorner.y = m_seCorner.y; alpha->m_seCorner.z = GetZ( alpha->m_seCorner ); beta = TheNavMesh->CreateArea(); beta->m_nwCorner.x = splitEdge; beta->m_nwCorner.y = m_nwCorner.y; beta->m_nwCorner.z = GetZ( beta->m_nwCorner ); beta->m_seCorner = m_seCorner; alpha->ConnectTo( beta, EAST ); beta->ConnectTo( alpha, WEST ); FinishSplitEdit( alpha, EAST ); FinishSplitEdit( beta, WEST ); } if ( !TheNavMesh->IsGenerating() && nav_split_place_on_ground.GetBool() ) { alpha->PlaceOnGround( NUM_CORNERS ); beta->PlaceOnGround( NUM_CORNERS ); } // For every ladder we pointed to, alpha or beta should point to it, based on // their distance to the ladder int dir; for( dir=0; dirm_top; // doesn't matter if we choose top or bottom float alphaDistance = alpha->GetDistanceSquaredToPoint( ladderPos ); float betaDistance = beta->GetDistanceSquaredToPoint( ladderPos ); if ( alphaDistance < betaDistance ) { alpha->ConnectTo( ladder ); } else { beta->ConnectTo( ladder ); } } } // For every ladder that pointed to us, connect that ladder to the closer of alpha and beta SplitNotification notify( this, alpha, beta ); TheNavMesh->ForAllLadders( notify ); // return new areas if (outAlpha) *outAlpha = alpha; if (outBeta) *outBeta = beta; TheNavMesh->OnEditCreateNotify( alpha ); TheNavMesh->OnEditCreateNotify( beta ); if ( TheNavMesh->IsInSelectedSet( this ) ) { TheNavMesh->AddToSelectedSet( alpha ); TheNavMesh->AddToSelectedSet( beta ); } // remove original area TheNavMesh->OnEditDestroyNotify( this ); TheNavAreas.FindAndRemove( this ); TheNavMesh->RemoveFromSelectedSet( this ); TheNavMesh->DestroyArea( this ); return true; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if given ladder is connected in given direction * @todo Formalize "asymmetric" flag on connections */ bool CNavArea::IsConnected( const CNavLadder *ladder, CNavLadder::LadderDirectionType dir ) const { FOR_EACH_VEC( m_ladder[ dir ], it ) { if ( ladder == m_ladder[ dir ][ it ].ladder ) { return true; } } return false; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if given area is connected in given direction * if dir == NUM_DIRECTIONS, check all directions (direction is unknown) * @todo Formalize "asymmetric" flag on connections */ bool CNavArea::IsConnected( const CNavArea *area, NavDirType dir ) const { // we are connected to ourself if (area == this) return true; if (dir == NUM_DIRECTIONS) { // search all directions for( int d=0; dm_topBehindArea == area || ladder->m_topForwardArea == area || ladder->m_topLeftArea == area || ladder->m_topRightArea == area) return true; } FOR_EACH_VEC( m_ladder[ CNavLadder::LADDER_DOWN ], dit ) { CNavLadder *ladder = m_ladder[ CNavLadder::LADDER_DOWN ][ dit ].ladder; if (ladder->m_bottomArea == area) return true; } } else { // check specific direction FOR_EACH_VEC( m_connect[ dir ], it ) { if (area == m_connect[ dir ][ it ].area) return true; } } return false; } //-------------------------------------------------------------------------------------------------------------- /** * Compute change in actual ground height from this area to given area */ float CNavArea::ComputeGroundHeightChange( const CNavArea *area ) { VPROF_BUDGET( "CNavArea::ComputeHeightChange", "NextBot" ); Vector closeFrom, closeTo; area->GetClosestPointOnArea( GetCenter(), &closeTo ); GetClosestPointOnArea( area->GetCenter(), &closeFrom ); // find actual ground height at each point in case // areas are below/above actual terrain float toZ, fromZ; if ( TheNavMesh->GetSimpleGroundHeight( closeTo + Vector( 0, 0, StepHeight ), &toZ ) == false ) { return 0.0f; } if ( TheNavMesh->GetSimpleGroundHeight( closeFrom + Vector( 0, 0, StepHeight ), &fromZ ) == false ) { return 0.0f; } return toZ - fromZ; } //-------------------------------------------------------------------------------------------------------------- /** * The area 'source' is connected to us along our 'incomingEdgeDir' edge */ void CNavArea::AddIncomingConnection( CNavArea *source, NavDirType incomingEdgeDir ) { NavConnect con; con.area = source; if ( m_incomingConnect[ incomingEdgeDir ].Find( con ) == m_incomingConnect[ incomingEdgeDir ].InvalidIndex() ) { con.length = ( source->GetCenter() - GetCenter() ).Length(); m_incomingConnect[ incomingEdgeDir ].AddToTail( con ); } } //-------------------------------------------------------------------------------------------------------------- /** * Given the portion of the original area, update its internal data * The "ignoreEdge" direction defines the side of the original area that the new area does not include */ void CNavArea::FinishSplitEdit( CNavArea *newArea, NavDirType ignoreEdge ) { newArea->InheritAttributes( this ); newArea->m_center.x = (newArea->m_nwCorner.x + newArea->m_seCorner.x)/2.0f; newArea->m_center.y = (newArea->m_nwCorner.y + newArea->m_seCorner.y)/2.0f; newArea->m_center.z = (newArea->m_nwCorner.z + newArea->m_seCorner.z)/2.0f; newArea->m_neZ = GetZ( newArea->m_seCorner.x, newArea->m_nwCorner.y ); newArea->m_swZ = GetZ( newArea->m_nwCorner.x, newArea->m_seCorner.y ); if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) { newArea->m_invDxCorners = 1.0f / ( newArea->m_seCorner.x - newArea->m_nwCorner.x ); newArea->m_invDyCorners = 1.0f / ( newArea->m_seCorner.y - newArea->m_nwCorner.y ); } else { newArea->m_invDxCorners = newArea->m_invDyCorners = 0; } // connect to adjacent areas for( int d=0; dIsOverlappingX( adj )) { newArea->ConnectTo( adj, (NavDirType)d ); // add reciprocal connection if needed if (adj->IsConnected( this, OppositeDirection( (NavDirType)d ))) adj->ConnectTo( newArea, OppositeDirection( (NavDirType)d ) ); } break; case EAST: case WEST: if (newArea->IsOverlappingY( adj )) { newArea->ConnectTo( adj, (NavDirType)d ); // add reciprocal connection if needed if (adj->IsConnected( this, OppositeDirection( (NavDirType)d ))) adj->ConnectTo( newArea, OppositeDirection( (NavDirType)d ) ); } break; } for ( int a = 0; a < m_incomingConnect[d].Count(); a++ ) { CNavArea *adj = m_incomingConnect[d][a].area; switch( d ) { case NORTH: case SOUTH: if (newArea->IsOverlappingX( adj )) { adj->ConnectTo( newArea, OppositeDirection( (NavDirType)d ) ); } break; case EAST: case WEST: if (newArea->IsOverlappingY( adj )) { adj->ConnectTo( newArea, OppositeDirection( (NavDirType)d ) ); } break; } } } } TheNavAreas.AddToTail( newArea ); TheNavMesh->AddNavArea( newArea ); // Assign nodes if ( HasNodes() ) { // first give it all our nodes... newArea->m_node[ NORTH_WEST ] = m_node[ NORTH_WEST ]; newArea->m_node[ NORTH_EAST ] = m_node[ NORTH_EAST ]; newArea->m_node[ SOUTH_EAST ] = m_node[ SOUTH_EAST ]; newArea->m_node[ SOUTH_WEST ] = m_node[ SOUTH_WEST ]; // ... then pull in one edge... NavDirType dir = NUM_DIRECTIONS; NavCornerType corner[2] = { NUM_CORNERS, NUM_CORNERS }; switch ( ignoreEdge ) { case NORTH: dir = SOUTH; corner[0] = NORTH_WEST; corner[1] = NORTH_EAST; break; case SOUTH: dir = NORTH; corner[0] = SOUTH_WEST; corner[1] = SOUTH_EAST; break; case EAST: dir = WEST; corner[0] = NORTH_EAST; corner[1] = SOUTH_EAST; break; case WEST: dir = EAST; corner[0] = NORTH_WEST; corner[1] = SOUTH_WEST; break; } while ( !newArea->IsOverlapping( *newArea->m_node[ corner[0] ]->GetPosition(), GenerationStepSize/2 ) ) { for ( int i=0; i<2; ++i ) { Assert( newArea->m_node[ corner[i] ] ); Assert( newArea->m_node[ corner[i] ]->GetConnectedNode( dir ) ); newArea->m_node[ corner[i] ] = newArea->m_node[ corner[i] ]->GetConnectedNode( dir ); } } // assign internal nodes... newArea->AssignNodes( newArea ); // ... and grab the node heights for our corner heights. newArea->m_neZ = newArea->m_node[ NORTH_EAST ]->GetPosition()->z; newArea->m_nwCorner.z = newArea->m_node[ NORTH_WEST ]->GetPosition()->z; newArea->m_swZ = newArea->m_node[ SOUTH_WEST ]->GetPosition()->z; newArea->m_seCorner.z = newArea->m_node[ SOUTH_EAST ]->GetPosition()->z; } } //-------------------------------------------------------------------------------------------------------------- /** * Create a new area between this area and given area */ bool CNavArea::SpliceEdit( CNavArea *other ) { CNavArea *newArea = NULL; Vector nw, ne, se, sw; if (m_nwCorner.x > other->m_seCorner.x) { // 'this' is east of 'other' float top = MAX( m_nwCorner.y, other->m_nwCorner.y ); float bottom = MIN( m_seCorner.y, other->m_seCorner.y ); nw.x = other->m_seCorner.x; nw.y = top; nw.z = other->GetZ( nw ); se.x = m_nwCorner.x; se.y = bottom; se.z = GetZ( se ); ne.x = se.x; ne.y = nw.y; ne.z = GetZ( ne ); sw.x = nw.x; sw.y = se.y; sw.z = other->GetZ( sw ); newArea = TheNavMesh->CreateArea(); if (newArea == NULL) { Warning( "SpliceEdit: Out of memory.\n" ); return false; } newArea->Build( nw, ne, se, sw ); this->ConnectTo( newArea, WEST ); newArea->ConnectTo( this, EAST ); other->ConnectTo( newArea, EAST ); newArea->ConnectTo( other, WEST ); } else if (m_seCorner.x < other->m_nwCorner.x) { // 'this' is west of 'other' float top = MAX( m_nwCorner.y, other->m_nwCorner.y ); float bottom = MIN( m_seCorner.y, other->m_seCorner.y ); nw.x = m_seCorner.x; nw.y = top; nw.z = GetZ( nw ); se.x = other->m_nwCorner.x; se.y = bottom; se.z = other->GetZ( se ); ne.x = se.x; ne.y = nw.y; ne.z = other->GetZ( ne ); sw.x = nw.x; sw.y = se.y; sw.z = GetZ( sw ); newArea = TheNavMesh->CreateArea(); if (newArea == NULL) { Warning( "SpliceEdit: Out of memory.\n" ); return false; } newArea->Build( nw, ne, se, sw ); this->ConnectTo( newArea, EAST ); newArea->ConnectTo( this, WEST ); other->ConnectTo( newArea, WEST ); newArea->ConnectTo( other, EAST ); } else // 'this' overlaps in X { if (m_nwCorner.y > other->m_seCorner.y) { // 'this' is south of 'other' float left = MAX( m_nwCorner.x, other->m_nwCorner.x ); float right = MIN( m_seCorner.x, other->m_seCorner.x ); nw.x = left; nw.y = other->m_seCorner.y; nw.z = other->GetZ( nw ); se.x = right; se.y = m_nwCorner.y; se.z = GetZ( se ); ne.x = se.x; ne.y = nw.y; ne.z = other->GetZ( ne ); sw.x = nw.x; sw.y = se.y; sw.z = GetZ( sw ); newArea = TheNavMesh->CreateArea(); if (newArea == NULL) { Warning( "SpliceEdit: Out of memory.\n" ); return false; } newArea->Build( nw, ne, se, sw ); this->ConnectTo( newArea, NORTH ); newArea->ConnectTo( this, SOUTH ); other->ConnectTo( newArea, SOUTH ); newArea->ConnectTo( other, NORTH ); } else if (m_seCorner.y < other->m_nwCorner.y) { // 'this' is north of 'other' float left = MAX( m_nwCorner.x, other->m_nwCorner.x ); float right = MIN( m_seCorner.x, other->m_seCorner.x ); nw.x = left; nw.y = m_seCorner.y; nw.z = GetZ( nw ); se.x = right; se.y = other->m_nwCorner.y; se.z = other->GetZ( se ); ne.x = se.x; ne.y = nw.y; ne.z = GetZ( ne ); sw.x = nw.x; sw.y = se.y; sw.z = other->GetZ( sw ); newArea = TheNavMesh->CreateArea(); if (newArea == NULL) { Warning( "SpliceEdit: Out of memory.\n" ); return false; } newArea->Build( nw, ne, se, sw ); this->ConnectTo( newArea, SOUTH ); newArea->ConnectTo( this, NORTH ); other->ConnectTo( newArea, NORTH ); newArea->ConnectTo( other, SOUTH ); } else { // areas overlap return false; } } newArea->InheritAttributes( this, other ); TheNavAreas.AddToTail( newArea ); TheNavMesh->AddNavArea( newArea ); TheNavMesh->OnEditCreateNotify( newArea ); return true; } //-------------------------------------------------------------------------------------------------------------- /** * Calculates a constant ID for an area at this location, for debugging */ void CNavArea::CalcDebugID() { if ( m_debugid == 0 ) { // calculate a debug ID which will be constant for this nav area across generation runs int coord[6] = { (int) m_nwCorner.x, (int) m_nwCorner.x, (int) m_nwCorner.z, (int) m_seCorner.x, (int) m_seCorner.y, (int) m_seCorner.z }; m_debugid = CRC32_ProcessSingleBuffer( &coord, sizeof( coord ) ); } } //-------------------------------------------------------------------------------------------------------------- /** * Merge this area and given adjacent area */ bool CNavArea::MergeEdit( CNavArea *adj ) { // can only merge if attributes of both areas match // check that these areas can be merged const float tolerance = 1.0f; bool merge = false; if (fabs( m_nwCorner.x - adj->m_nwCorner.x ) < tolerance && fabs( m_seCorner.x - adj->m_seCorner.x ) < tolerance) merge = true; if (fabs( m_nwCorner.y - adj->m_nwCorner.y ) < tolerance && fabs( m_seCorner.y - adj->m_seCorner.y ) < tolerance) merge = true; if (merge == false) return false; Vector originalNWCorner = m_nwCorner; Vector originalSECorner = m_seCorner; // update extent if (m_nwCorner.x > adj->m_nwCorner.x || m_nwCorner.y > adj->m_nwCorner.y) m_nwCorner = adj->m_nwCorner; if (m_seCorner.x < adj->m_seCorner.x || m_seCorner.y < adj->m_seCorner.y) m_seCorner = adj->m_seCorner; m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) { m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); } else { m_invDxCorners = m_invDyCorners = 0; } if (m_seCorner.x > originalSECorner.x || m_nwCorner.y < originalNWCorner.y) m_neZ = adj->GetZ( m_seCorner.x, m_nwCorner.y ); else m_neZ = GetZ( m_seCorner.x, m_nwCorner.y ); if (m_nwCorner.x < originalNWCorner.x || m_seCorner.y > originalSECorner.y) m_swZ = adj->GetZ( m_nwCorner.x, m_seCorner.y ); else m_swZ = GetZ( m_nwCorner.x, m_seCorner.y ); // merge adjacency links - we gain all the connections that adjArea had MergeAdjacentConnections( adj ); InheritAttributes( adj ); // remove subsumed adjacent area TheNavAreas.FindAndRemove( adj ); TheNavMesh->OnEditDestroyNotify( adj ); TheNavMesh->DestroyArea( adj ); TheNavMesh->OnEditCreateNotify( this ); return true; } //-------------------------------------------------------------------------------------------------------------- void CNavArea::InheritAttributes( CNavArea *first, CNavArea *second ) { if ( first && second ) { SetAttributes( first->GetAttributes() | second->GetAttributes() ); // if both areas have the same place, the new area inherits it if ( first->GetPlace() == second->GetPlace() ) { SetPlace( first->GetPlace() ); } else if ( first->GetPlace() == UNDEFINED_PLACE ) { SetPlace( second->GetPlace() ); } else if ( second->GetPlace() == UNDEFINED_PLACE ) { SetPlace( first->GetPlace() ); } else { // both have valid, but different places - pick on at random if ( RandomInt( 0, 100 ) < 50 ) SetPlace( first->GetPlace() ); else SetPlace( second->GetPlace() ); } } else if ( first ) { SetAttributes( GetAttributes() | first->GetAttributes() ); if ( GetPlace() == UNDEFINED_PLACE ) { SetPlace( first->GetPlace() ); } } } //-------------------------------------------------------------------------------------------------------------- void ApproachAreaAnalysisPrep( void ) { } //-------------------------------------------------------------------------------------------------------------- void CleanupApproachAreaAnalysisPrep( void ) { } //-------------------------------------------------------------------------------------------------------------- /** * Remove "analyzed" data from nav area */ void CNavArea::Strip( void ) { m_spotEncounters.PurgeAndDeleteElements(); // this calls delete on each element } //-------------------------------------------------------------------------------------------------------------- /** * Return true if area is more or less square. * This is used when merging to prevent long, thin, areas being created. */ bool CNavArea::IsRoughlySquare( void ) const { float aspect = GetSizeX() / GetSizeY(); const float maxAspect = 3.01; const float minAspect = 1.0f / maxAspect; if (aspect < minAspect || aspect > maxAspect) return false; return true; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if 'pos' is within 2D extents of area. */ bool CNavArea::IsOverlapping( const Vector &pos, float tolerance ) const { if (pos.x + tolerance >= m_nwCorner.x && pos.x - tolerance <= m_seCorner.x && pos.y + tolerance >= m_nwCorner.y && pos.y - tolerance <= m_seCorner.y) return true; return false; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if 'area' overlaps our 2D extents */ bool CNavArea::IsOverlapping( const CNavArea *area ) const { if (area->m_nwCorner.x < m_seCorner.x && area->m_seCorner.x > m_nwCorner.x && area->m_nwCorner.y < m_seCorner.y && area->m_seCorner.y > m_nwCorner.y) return true; return false; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if 'extent' overlaps our 2D extents */ bool CNavArea::IsOverlapping( const Extent &extent ) const { return ( extent.lo.x < m_seCorner.x && extent.hi.x > m_nwCorner.x && extent.lo.y < m_seCorner.y && extent.hi.y > m_nwCorner.y ); } //-------------------------------------------------------------------------------------------------------------- /** * Return true if 'area' overlaps our X extent */ bool CNavArea::IsOverlappingX( const CNavArea *area ) const { if (area->m_nwCorner.x < m_seCorner.x && area->m_seCorner.x > m_nwCorner.x) return true; return false; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if 'area' overlaps our Y extent */ bool CNavArea::IsOverlappingY( const CNavArea *area ) const { if (area->m_nwCorner.y < m_seCorner.y && area->m_seCorner.y > m_nwCorner.y) return true; return false; } //-------------------------------------------------------------------------------------------------------------- class COverlapCheck { public: COverlapCheck( const CNavArea *me, const Vector &pos ) : m_pos( pos ) { m_me = me; m_myZ = me->GetZ( pos ); } bool operator() ( CNavArea *area ) { // skip self if ( area == m_me ) return true; // check 2D overlap if ( !area->IsOverlapping( m_pos ) ) return true; float theirZ = area->GetZ( m_pos ); if ( theirZ > m_pos.z ) { // they are above the point return true; } if ( theirZ > m_myZ ) { // we are below an area that is beneath the given position return false; } return true; } const CNavArea *m_me; float m_myZ; const Vector &m_pos; }; //-------------------------------------------------------------------------------------------------------------- /** * Return true if given point is on or above this area, but no others */ bool CNavArea::Contains( const Vector &pos ) const { // check 2D overlap if (!IsOverlapping( pos )) return false; // the point overlaps us, check that it is above us, but not above any areas that overlap us float myZ = GetZ( pos ); // if the nav area is above the given position, fail // allow nav area to be as much as a step height above the given position if (myZ - StepHeight > pos.z) return false; Extent areaExtent; GetExtent( &areaExtent ); COverlapCheck overlap( this, pos ); return TheNavMesh->ForAllAreasOverlappingExtent( overlap, areaExtent ); } //-------------------------------------------------------------------------------------------------------------- /** * Returns true if area completely contains other area */ bool CNavArea::Contains( const CNavArea *area ) const { return ( ( m_nwCorner.x <= area->m_nwCorner.x ) && ( m_seCorner.x >= area->m_seCorner.x ) && ( m_nwCorner.y <= area->m_nwCorner.y ) && ( m_seCorner.y >= area->m_seCorner.y ) && ( m_nwCorner.z <= area->m_nwCorner.z ) && ( m_seCorner.z >= area->m_seCorner.z ) ); } //-------------------------------------------------------------------------------------------------------------- void CNavArea::ComputeNormal( Vector *normal, bool alternate ) const { if ( !normal ) return; Vector u, v; if ( !alternate ) { u.x = m_seCorner.x - m_nwCorner.x; u.y = 0.0f; u.z = m_neZ - m_nwCorner.z; v.x = 0.0f; v.y = m_seCorner.y - m_nwCorner.y; v.z = m_swZ - m_nwCorner.z; } else { u.x = m_nwCorner.x - m_seCorner.x; u.y = 0.0f; u.z = m_swZ - m_seCorner.z; v.x = 0.0f; v.y = m_nwCorner.y - m_seCorner.y; v.z = m_neZ - m_seCorner.z; } *normal = CrossProduct( u, v ); normal->NormalizeInPlace(); } //-------------------------------------------------------------------------------------------------------------- /** * Removes all connections in directions to left and right of specified direction */ void CNavArea::RemoveOrthogonalConnections( NavDirType dir ) { NavDirType dirToRemove[2]; dirToRemove[0] = DirectionLeft( dir ); dirToRemove[1] = DirectionRight( dir ); for ( int i = 0; i < 2; i++ ) { dir = dirToRemove[i]; while ( GetAdjacentCount( dir ) > 0 ) { CNavArea *adj = GetAdjacentArea( dir, 0 ); Disconnect( adj ); adj->Disconnect( this ); } } } //-------------------------------------------------------------------------------------------------------------- /** * Return true if the area is approximately flat, using normals computed from opposite corners */ bool CNavArea::IsFlat( void ) const { Vector normal, otherNormal; ComputeNormal( &normal ); ComputeNormal( &otherNormal, true ); float tolerance = nav_coplanar_slope_limit.GetFloat(); if ( ( m_node[ NORTH_WEST ] && m_node[ NORTH_WEST ]->IsOnDisplacement() ) || ( m_node[ NORTH_EAST ] && m_node[ NORTH_EAST ]->IsOnDisplacement() ) || ( m_node[ SOUTH_EAST ] && m_node[ SOUTH_EAST ]->IsOnDisplacement() ) || ( m_node[ SOUTH_WEST ] && m_node[ SOUTH_WEST ]->IsOnDisplacement() ) ) { tolerance = nav_coplanar_slope_limit_displacement.GetFloat(); } if (DotProduct( normal, otherNormal ) > tolerance) return true; return false; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if this area and given area are approximately co-planar */ bool CNavArea::IsCoplanar( const CNavArea *area ) const { Vector u, v; bool isOnDisplacement = ( m_node[ NORTH_WEST ] && m_node[ NORTH_WEST ]->IsOnDisplacement() ) || ( m_node[ NORTH_EAST ] && m_node[ NORTH_EAST ]->IsOnDisplacement() ) || ( m_node[ SOUTH_EAST ] && m_node[ SOUTH_EAST ]->IsOnDisplacement() ) || ( m_node[ SOUTH_WEST ] && m_node[ SOUTH_WEST ]->IsOnDisplacement() ); if ( !isOnDisplacement && !IsFlat() ) return false; bool areaIsOnDisplacement = ( area->m_node[ NORTH_WEST ] && area->m_node[ NORTH_WEST ]->IsOnDisplacement() ) || ( area->m_node[ NORTH_EAST ] && area->m_node[ NORTH_EAST ]->IsOnDisplacement() ) || ( area->m_node[ SOUTH_EAST ] && area->m_node[ SOUTH_EAST ]->IsOnDisplacement() ) || ( area->m_node[ SOUTH_WEST ] && area->m_node[ SOUTH_WEST ]->IsOnDisplacement() ); if ( !areaIsOnDisplacement && !area->IsFlat() ) return false; // compute our unit surface normal Vector normal, otherNormal; ComputeNormal( &normal ); area->ComputeNormal( &otherNormal ); // can only merge areas that are nearly planar, to ensure areas do not differ from underlying geometry much float tolerance = nav_coplanar_slope_limit.GetFloat(); if ( ( m_node[ NORTH_WEST ] && m_node[ NORTH_WEST ]->IsOnDisplacement() ) || ( m_node[ NORTH_EAST ] && m_node[ NORTH_EAST ]->IsOnDisplacement() ) || ( m_node[ SOUTH_EAST ] && m_node[ SOUTH_EAST ]->IsOnDisplacement() ) || ( m_node[ SOUTH_WEST ] && m_node[ SOUTH_WEST ]->IsOnDisplacement() ) ) { tolerance = nav_coplanar_slope_limit_displacement.GetFloat(); } if (DotProduct( normal, otherNormal ) > tolerance) return true; return false; } //-------------------------------------------------------------------------------------------------------------- /** * Return Z of area at (x,y) of 'pos' * Trilinear interpolation of Z values at quad edges. * NOTE: pos->z is not used. */ float CNavArea::GetZ( float x, float y ) const RESTRICT { // guard against division by zero due to degenerate areas #ifdef _X360 // do the compare-against-zero on the integer unit to avoid a fcmp // IEEE754 float positive zero is simply 0x00. There is also a // floating-point negative zero (-0.0f == 0x80000000), but given // how m_inv is computed earlier, that's not a possible value for // it here, so we don't have to check for that. // // oddly, the compiler isn't smart enough to do this on its own if ( *reinterpret_cast(&m_invDxCorners) == 0 || *reinterpret_cast(&m_invDyCorners) == 0 ) return m_neZ; #else if (m_invDxCorners == 0.0f || m_invDyCorners == 0.0f) return m_neZ; #endif float u = (x - m_nwCorner.x) * m_invDxCorners; float v = (y - m_nwCorner.y) * m_invDyCorners; // clamp Z values to (x,y) volume u = fsel( u, u, 0 ); // u >= 0 ? u : 0 u = fsel( u - 1.0f, 1.0f, u ); // u >= 1 ? 1 : u v = fsel( v, v, 0 ); // v >= 0 ? v : 0 v = fsel( v - 1.0f, 1.0f, v ); // v >= 1 ? 1 : v float northZ = m_nwCorner.z + u * (m_neZ - m_nwCorner.z); float southZ = m_swZ + u * (m_seCorner.z - m_swZ); return northZ + v * (southZ - northZ); } //-------------------------------------------------------------------------------------------------------------- /** * Return closest point to 'pos' on 'area'. * Returned point is in 'close'. */ void CNavArea::GetClosestPointOnArea( const Vector * RESTRICT pPos, Vector *close ) const RESTRICT { float x, y, z; // Using fsel rather than compares, as much faster on 360 [7/28/2008 tom] x = fsel( pPos->x - m_nwCorner.x, pPos->x, m_nwCorner.x ); x = fsel( x - m_seCorner.x, m_seCorner.x, x ); y = fsel( pPos->y - m_nwCorner.y, pPos->y, m_nwCorner.y ); y = fsel( y - m_seCorner.y, m_seCorner.y, y ); z = GetZ( x, y ); close->Init( x, y, z ); } //-------------------------------------------------------------------------------------------------------------- /** * Return shortest distance squared between point and this area */ float CNavArea::GetDistanceSquaredToPoint( const Vector &pos ) const { if (pos.x < m_nwCorner.x) { if (pos.y < m_nwCorner.y) { // position is north-west of area return (m_nwCorner - pos).LengthSqr(); } else if (pos.y > m_seCorner.y) { // position is south-west of area Vector d; d.x = m_nwCorner.x - pos.x; d.y = m_seCorner.y - pos.y; d.z = m_swZ - pos.z; return d.LengthSqr(); } else { // position is west of area float d = m_nwCorner.x - pos.x; return d * d; } } else if (pos.x > m_seCorner.x) { if (pos.y < m_nwCorner.y) { // position is north-east of area Vector d; d.x = m_seCorner.x - pos.x; d.y = m_nwCorner.y - pos.y; d.z = m_neZ - pos.z; return d.LengthSqr(); } else if (pos.y > m_seCorner.y) { // position is south-east of area return (m_seCorner - pos).LengthSqr(); } else { // position is east of area float d = pos.x - m_seCorner.x; return d * d; } } else if (pos.y < m_nwCorner.y) { // position is north of area float d = m_nwCorner.y - pos.y; return d * d; } else if (pos.y > m_seCorner.y) { // position is south of area float d = pos.y - m_seCorner.y; return d * d; } else { // position is inside of 2D extent of area - find delta Z float z = GetZ( pos ); float d = z - pos.z; return d * d; } } //-------------------------------------------------------------------------------------------------------------- CNavArea *CNavArea::GetRandomAdjacentArea( NavDirType dir ) const { int count = m_connect[ dir ].Count(); int which = RandomInt( 0, count-1 ); int i = 0; FOR_EACH_VEC( m_connect[ dir ], it ) { if (i == which) return m_connect[ dir ][ it ].area; ++i; } return NULL; } //-------------------------------------------------------------------------------------------------------------- // Build a vector of all adjacent areas void CNavArea::CollectAdjacentAreas( CUtlVector< CNavArea * > *adjVector ) const { for( int d=0; dAddToTail( m_connect[d].Element(i).area ); } } } //-------------------------------------------------------------------------------------------------------------- /** * Compute "portal" between two adjacent areas. * Return center of portal opening, and half-width defining sides of portal from center. * NOTE: center->z is unset. */ void CNavArea::ComputePortal( const CNavArea *to, NavDirType dir, Vector *center, float *halfWidth ) const { if ( dir == NORTH || dir == SOUTH ) { if ( dir == NORTH ) { center->y = m_nwCorner.y; } else { center->y = m_seCorner.y; } float left = MAX( m_nwCorner.x, to->m_nwCorner.x ); float right = MIN( m_seCorner.x, to->m_seCorner.x ); // clamp to our extent in case areas are disjoint if ( left < m_nwCorner.x ) { left = m_nwCorner.x; } else if ( left > m_seCorner.x ) { left = m_seCorner.x; } if ( right < m_nwCorner.x ) { right = m_nwCorner.x; } else if ( right > m_seCorner.x ) { right = m_seCorner.x; } center->x = ( left + right )/2.0f; *halfWidth = ( right - left )/2.0f; } else // EAST or WEST { if ( dir == WEST ) { center->x = m_nwCorner.x; } else { center->x = m_seCorner.x; } float top = MAX( m_nwCorner.y, to->m_nwCorner.y ); float bottom = MIN( m_seCorner.y, to->m_seCorner.y ); // clamp to our extent in case areas are disjoint if ( top < m_nwCorner.y ) { top = m_nwCorner.y; } else if ( top > m_seCorner.y ) { top = m_seCorner.y; } if ( bottom < m_nwCorner.y ) { bottom = m_nwCorner.y; } else if ( bottom > m_seCorner.y ) { bottom = m_seCorner.y; } center->y = (top + bottom)/2.0f; *halfWidth = (bottom - top)/2.0f; } center->z = GetZ( center->x, center->y ); } //-------------------------------------------------------------------------------------------------------------- // compute largest portal to adjacent area, returning direction NavDirType CNavArea::ComputeLargestPortal( const CNavArea *to, Vector *center, float *halfWidth ) const { NavDirType bestDir = NUM_DIRECTIONS; Vector bestCenter( vec3_origin ); float bestHalfWidth = 0.0f; Vector centerDir = to->GetCenter() - GetCenter(); for ( int i=0; i= 0.0f ) continue; break; case SOUTH: // +y if ( centerDir.y <= 0.0f ) continue; break; case WEST: // -x if ( centerDir.x >= 0.0f ) continue; break; case EAST: // +x if ( centerDir.x <= 0.0f ) continue; break; } ComputePortal( to, testDir, &testCenter, &testHalfWidth ); if ( testHalfWidth > bestHalfWidth ) { bestDir = testDir; bestCenter = testCenter; bestHalfWidth = testHalfWidth; } } *center = bestCenter; *halfWidth = bestHalfWidth; return bestDir; } //-------------------------------------------------------------------------------------------------------------- /** * Compute closest point within the "portal" between to adjacent areas. */ void CNavArea::ComputeClosestPointInPortal( const CNavArea *to, NavDirType dir, const Vector &fromPos, Vector *closePos ) const { // const float margin = 0.0f; //GenerationStepSize/2.0f; // causes trouble with very small/narrow nav areas const float margin = GenerationStepSize; if ( dir == NORTH || dir == SOUTH ) { if ( dir == NORTH ) { closePos->y = m_nwCorner.y; } else { closePos->y = m_seCorner.y; } float left = MAX( m_nwCorner.x, to->m_nwCorner.x ); float right = MIN( m_seCorner.x, to->m_seCorner.x ); // clamp to our extent in case areas are disjoint // no good - need to push into to area for margins /* if (left < m_nwCorner.x) left = m_nwCorner.x; else if (left > m_seCorner.x) left = m_seCorner.x; if (right < m_nwCorner.x) right = m_nwCorner.x; else if (right > m_seCorner.x) right = m_seCorner.x; */ // keep margin if against edge /// @todo Need better check whether edge is outer edge or not - partial overlap is missed float leftMargin = ( to->IsEdge( WEST ) ) ? ( left + margin ) : left; float rightMargin = ( to->IsEdge( EAST ) ) ? ( right - margin ) : right; // if area is narrow, margins may have crossed if ( leftMargin > rightMargin ) { // use midline float mid = ( left + right )/2.0f; leftMargin = mid; rightMargin = mid; } // limit x to within portal if ( fromPos.x < leftMargin ) { closePos->x = leftMargin; } else if ( fromPos.x > rightMargin ) { closePos->x = rightMargin; } else { closePos->x = fromPos.x; } } else // EAST or WEST { if ( dir == WEST ) { closePos->x = m_nwCorner.x; } else { closePos->x = m_seCorner.x; } float top = MAX( m_nwCorner.y, to->m_nwCorner.y ); float bottom = MIN( m_seCorner.y, to->m_seCorner.y ); // clamp to our extent in case areas are disjoint // no good - need to push into to area for margins /* if (top < m_nwCorner.y) top = m_nwCorner.y; else if (top > m_seCorner.y) top = m_seCorner.y; if (bottom < m_nwCorner.y) bottom = m_nwCorner.y; else if (bottom > m_seCorner.y) bottom = m_seCorner.y; */ // keep margin if against edge float topMargin = ( to->IsEdge( NORTH ) ) ? ( top + margin ) : top; float bottomMargin = ( to->IsEdge( SOUTH ) ) ? ( bottom - margin ) : bottom; // if area is narrow, margins may have crossed if ( topMargin > bottomMargin ) { // use midline float mid = ( top + bottom )/2.0f; topMargin = mid; bottomMargin = mid; } // limit y to within portal if ( fromPos.y < topMargin ) { closePos->y = topMargin; } else if ( fromPos.y > bottomMargin ) { closePos->y = bottomMargin; } else { closePos->y = fromPos.y; } } closePos->z = GetZ( closePos->x, closePos->y ); } //-------------------------------------------------------------------------------------------------------------- /** * Return true if the given area and 'other' share a colinear edge (ie: no drop-down or step/jump/climb) */ bool CNavArea::IsContiguous( const CNavArea *other ) const { VPROF_BUDGET( "CNavArea::IsContiguous", "NextBot" ); // find which side it is connected on int dir; for( dir=0; dirComputePortal( this, OppositeDirection( (NavDirType)dir ), &otherEdge, &halfWidth ); // must use stepheight because rough terrain can have gaps/cracks between adjacent nav areas return ( myEdge - otherEdge ).IsLengthLessThan( StepHeight ); } //-------------------------------------------------------------------------------------------------------------- /** * Return height change between edges of adjacent nav areas (not actual underlying ground) */ float CNavArea::ComputeAdjacentConnectionHeightChange( const CNavArea *destinationArea ) const { VPROF_BUDGET( "CNavArea::ComputeAdjacentConnectionHeightChange", "NextBot" ); // find which side it is connected on int dir; for( dir=0; dirComputePortal( this, OppositeDirection( (NavDirType)dir ), &otherEdge, &halfWidth ); return otherEdge.z - myEdge.z; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if there are no bi-directional links on the given side */ bool CNavArea::IsEdge( NavDirType dir ) const { FOR_EACH_VEC( m_connect[ dir ], it ) { const NavConnect connect = m_connect[ dir ][ it ]; if (connect.area->IsConnected( this, OppositeDirection( dir ) )) return false; } return true; } //-------------------------------------------------------------------------------------------------------------- /** * Return direction from this area to the given point */ NavDirType CNavArea::ComputeDirection( Vector *point ) const { if (point->x >= m_nwCorner.x && point->x <= m_seCorner.x) { if (point->y < m_nwCorner.y) return NORTH; else if (point->y > m_seCorner.y) return SOUTH; } else if (point->y >= m_nwCorner.y && point->y <= m_seCorner.y) { if (point->x < m_nwCorner.x) return WEST; else if (point->x > m_seCorner.x) return EAST; } // find closest direction Vector to = *point - m_center; if (fabs(to.x) > fabs(to.y)) { if (to.x > 0.0f) return EAST; return WEST; } else { if (to.y > 0.0f) return SOUTH; return NORTH; } return NUM_DIRECTIONS; } //-------------------------------------------------------------------------------------------------------------- bool CNavArea::GetCornerHotspot( NavCornerType corner, Vector hotspot[NUM_CORNERS] ) const { Vector nw = GetCorner( NORTH_WEST ); Vector ne = GetCorner( NORTH_EAST ); Vector sw = GetCorner( SOUTH_WEST ); Vector se = GetCorner( SOUTH_EAST ); float size = 9.0f; size = MIN( size, GetSizeX()/3 ); // make sure the hotspot doesn't extend outside small areas size = MIN( size, GetSizeY()/3 ); switch ( corner ) { case NORTH_WEST: hotspot[0] = nw; hotspot[1] = hotspot[0] + Vector( size, 0, 0 ); hotspot[2] = hotspot[0] + Vector( size, size, 0 ); hotspot[3] = hotspot[0] + Vector( 0, size, 0 ); break; case NORTH_EAST: hotspot[0] = ne; hotspot[1] = hotspot[0] + Vector( -size, 0, 0 ); hotspot[2] = hotspot[0] + Vector( -size, size, 0 ); hotspot[3] = hotspot[0] + Vector( 0, size, 0 ); break; case SOUTH_WEST: hotspot[0] = sw; hotspot[1] = hotspot[0] + Vector( size, 0, 0 ); hotspot[2] = hotspot[0] + Vector( size, -size, 0 ); hotspot[3] = hotspot[0] + Vector( 0, -size, 0 ); break; case SOUTH_EAST: hotspot[0] = se; hotspot[1] = hotspot[0] + Vector( -size, 0, 0 ); hotspot[2] = hotspot[0] + Vector( -size, -size, 0 ); hotspot[3] = hotspot[0] + Vector( 0, -size, 0 ); break; default: return false; } for ( int i=1; iGetEditVectors( &eyePos, &eyeForward ); Ray_t ray; ray.Init( eyePos, eyePos + 10000.0f * eyeForward, vec3_origin, vec3_origin ); float dist = IntersectRayWithTriangle( ray, hotspot[0], hotspot[1], hotspot[2], false ); if ( dist > 0 ) { return true; } dist = IntersectRayWithTriangle( ray, hotspot[2], hotspot[3], hotspot[0], false ); if ( dist > 0 ) { return true; } return false; } //-------------------------------------------------------------------------------------------------------------- NavCornerType CNavArea::GetCornerUnderCursor( void ) const { Vector eyePos, eyeForward; TheNavMesh->GetEditVectors( &eyePos, &eyeForward ); for ( int i=0; iIsEditMode( CNavMesh::PLACE_PAINTING ) ) { useAttributeColors = false; if ( m_place == UNDEFINED_PLACE ) { color = NavNoPlaceColor; } else if ( TheNavMesh->GetNavPlace() == m_place ) { color = NavSamePlaceColor; } else { color = NavDifferentPlaceColor; } } else { // normal edit mode if ( this == TheNavMesh->GetMarkedArea() ) { useAttributeColors = false; color = NavMarkedColor; } else if ( this == TheNavMesh->GetSelectedArea() ) { color = NavSelectedColor; } else { color = NavNormalColor; } } if ( IsDegenerate() ) { static IntervalTimer blink; static bool blinkOn = false; if (blink.GetElapsedTime() > 1.0f) { blink.Reset(); blinkOn = !blinkOn; } useAttributeColors = false; if (blinkOn) color = NavDegenerateFirstColor; else color = NavDegenerateSecondColor; NDebugOverlay::Text( GetCenter(), UTIL_VarArgs( "Degenerate area %d", GetID() ), true, DebugDuration ); } Vector nw, ne, sw, se; nw = m_nwCorner; se = m_seCorner; ne.x = se.x; ne.y = nw.y; ne.z = m_neZ; sw.x = nw.x; sw.y = se.y; sw.z = m_swZ; if ( nav_show_light_intensity.GetBool() ) { for ( int i=0; i 0 ) { const Vector offset( 0, 0, 0.8f ); NDebugOverlay::Triangle( nw+offset, se+offset, ne+offset, bgcolor[0], bgcolor[1], bgcolor[2], bgcolor[3], true, DebugDuration ); NDebugOverlay::Triangle( se+offset, nw+offset, sw+offset, bgcolor[0], bgcolor[1], bgcolor[2], bgcolor[3], true, DebugDuration ); } } const float inset = 0.2f; nw.x += inset; nw.y += inset; ne.x -= inset; ne.y += inset; sw.x += inset; sw.y -= inset; se.x -= inset; se.y -= inset; if ( GetAttributes() & NAV_MESH_TRANSIENT ) { NavDrawDashedLine( nw, ne, color ); NavDrawDashedLine( ne, se, color ); NavDrawDashedLine( se, sw, color ); NavDrawDashedLine( sw, nw, color ); } else { NavDrawLine( nw, ne, color ); NavDrawLine( ne, se, color ); NavDrawLine( se, sw, color ); NavDrawLine( sw, nw, color ); } if ( this == TheNavMesh->GetMarkedArea() && TheNavMesh->m_markedCorner != NUM_CORNERS ) { Vector p[NUM_CORNERS]; GetCornerHotspot( TheNavMesh->m_markedCorner, p ); NavDrawLine( p[1], p[2], NavMarkedColor ); NavDrawLine( p[2], p[3], NavMarkedColor ); } if ( this != TheNavMesh->GetMarkedArea() && this == TheNavMesh->GetSelectedArea() && TheNavMesh->IsEditMode( CNavMesh::NORMAL ) ) { NavCornerType bestCorner = GetCornerUnderCursor(); Vector p[NUM_CORNERS]; if ( GetCornerHotspot( bestCorner, p ) ) { NavDrawLine( p[1], p[2], NavSelectedColor ); NavDrawLine( p[2], p[3], NavSelectedColor ); } } if (GetAttributes() & NAV_MESH_CROUCH) { if ( useAttributeColors ) color = NavAttributeCrouchColor; NavDrawLine( nw, se, color ); } if (GetAttributes() & NAV_MESH_JUMP) { if ( useAttributeColors ) color = NavAttributeJumpColor; if ( !(GetAttributes() & NAV_MESH_CROUCH) ) { NavDrawLine( nw, se, color ); } NavDrawLine( ne, sw, color ); } if (GetAttributes() & NAV_MESH_PRECISE) { if ( useAttributeColors ) color = NavAttributePreciseColor; float size = 8.0f; Vector up( m_center.x, m_center.y - size, m_center.z ); Vector down( m_center.x, m_center.y + size, m_center.z ); NavDrawLine( up, down, color ); Vector left( m_center.x - size, m_center.y, m_center.z ); Vector right( m_center.x + size, m_center.y, m_center.z ); NavDrawLine( left, right, color ); } if (GetAttributes() & NAV_MESH_NO_JUMP) { if ( useAttributeColors ) color = NavAttributeNoJumpColor; float size = 8.0f; Vector up( m_center.x, m_center.y - size, m_center.z ); Vector down( m_center.x, m_center.y + size, m_center.z ); Vector left( m_center.x - size, m_center.y, m_center.z ); Vector right( m_center.x + size, m_center.y, m_center.z ); NavDrawLine( up, right, color ); NavDrawLine( right, down, color ); NavDrawLine( down, left, color ); NavDrawLine( left, up, color ); } if (GetAttributes() & NAV_MESH_STAIRS) { if ( useAttributeColors ) color = NavAttributeStairColor; float northZ = ( GetCorner( NORTH_WEST ).z + GetCorner( NORTH_EAST ).z ) / 2.0f; float southZ = ( GetCorner( SOUTH_WEST ).z + GetCorner( SOUTH_EAST ).z ) / 2.0f; float westZ = ( GetCorner( NORTH_WEST ).z + GetCorner( SOUTH_WEST ).z ) / 2.0f; float eastZ = ( GetCorner( NORTH_EAST ).z + GetCorner( SOUTH_EAST ).z ) / 2.0f; float deltaEastWest = abs( westZ - eastZ ); float deltaNorthSouth = abs( northZ - southZ ); float stepSize = StepHeight / 2.0f; float t; if ( deltaEastWest > deltaNorthSouth ) { float inc = stepSize / GetSizeX(); for( t = 0.0f; t <= 1.0f; t += inc ) { float x = m_nwCorner.x + t * GetSizeX(); NavDrawLine( Vector( x, m_nwCorner.y, GetZ( x, m_nwCorner.y ) ), Vector( x, m_seCorner.y, GetZ( x, m_seCorner.y ) ), color ); } } else { float inc = stepSize / GetSizeY(); for( t = 0.0f; t <= 1.0f; t += inc ) { float y = m_nwCorner.y + t * GetSizeY(); NavDrawLine( Vector( m_nwCorner.x, y, GetZ( m_nwCorner.x, y ) ), Vector( m_seCorner.x, y, GetZ( m_seCorner.x, y ) ), color ); } } } // Stop is represented by an octagon if (GetAttributes() & NAV_MESH_STOP) { if ( useAttributeColors ) color = NavAttributeStopColor; float dist = 8.0f; float length = dist/2.5f; Vector start, end; start = m_center + Vector( dist, -length, 0 ); end = m_center + Vector( dist, length, 0 ); NavDrawLine( start, end, color ); start = m_center + Vector( dist, length, 0 ); end = m_center + Vector( length, dist, 0 ); NavDrawLine( start, end, color ); start = m_center + Vector( -dist, -length, 0 ); end = m_center + Vector( -dist, length, 0 ); NavDrawLine( start, end, color ); start = m_center + Vector( -dist, length, 0 ); end = m_center + Vector( -length, dist, 0 ); NavDrawLine( start, end, color ); start = m_center + Vector( -length, dist, 0 ); end = m_center + Vector( length, dist, 0 ); NavDrawLine( start, end, color ); start = m_center + Vector( -dist, -length, 0 ); end = m_center + Vector( -length, -dist, 0 ); NavDrawLine( start, end, color ); start = m_center + Vector( -length, -dist, 0 ); end = m_center + Vector( length, -dist, 0 ); NavDrawLine( start, end, color ); start = m_center + Vector( length, -dist, 0 ); end = m_center + Vector( dist, -length, 0 ); NavDrawLine( start, end, color ); } // Walk is represented by an arrow if (GetAttributes() & NAV_MESH_WALK) { if ( useAttributeColors ) color = NavAttributeWalkColor; float size = 8.0f; NavDrawHorizontalArrow( m_center + Vector( -size, 0, 0 ), m_center + Vector( size, 0, 0 ), 4, color ); } // Walk is represented by a double arrow if (GetAttributes() & NAV_MESH_RUN) { if ( useAttributeColors ) color = NavAttributeRunColor; float size = 8.0f; float dist = 4.0f; NavDrawHorizontalArrow( m_center + Vector( -size, dist, 0 ), m_center + Vector( size, dist, 0 ), 4, color ); NavDrawHorizontalArrow( m_center + Vector( -size, -dist, 0 ), m_center + Vector( size, -dist, 0 ), 4, color ); } // Avoid is represented by an exclamation point if (GetAttributes() & NAV_MESH_AVOID) { if ( useAttributeColors ) color = NavAttributeAvoidColor; float topHeight = 8.0f; float topWidth = 3.0f; float bottomHeight = 3.0f; float bottomWidth = 2.0f; NavDrawTriangle( m_center, m_center + Vector( -topWidth, topHeight, 0 ), m_center + Vector( +topWidth, topHeight, 0 ), color ); NavDrawTriangle( m_center + Vector( 0, -bottomHeight, 0 ), m_center + Vector( -bottomWidth, -bottomHeight*2, 0 ), m_center + Vector( bottomWidth, -bottomHeight*2, 0 ), color ); } if ( IsBlocked( TEAM_ANY ) || HasAvoidanceObstacle() || IsDamaging() ) { NavEditColor color = (IsBlocked( TEAM_ANY ) && ( m_attributeFlags & NAV_MESH_NAV_BLOCKER ) ) ? NavBlockedByFuncNavBlockerColor : NavBlockedByDoorColor; const float blockedInset = 4.0f; nw.x += blockedInset; nw.y += blockedInset; ne.x -= blockedInset; ne.y += blockedInset; sw.x += blockedInset; sw.y -= blockedInset; se.x -= blockedInset; se.y -= blockedInset; NavDrawLine( nw, ne, color ); NavDrawLine( ne, se, color ); NavDrawLine( se, sw, color ); NavDrawLine( sw, nw, color ); } } //-------------------------------------------------------------------------------------------------------- /** * Draw area as a filled rect of the given color */ void CNavArea::DrawFilled( int r, int g, int b, int a, float deltaT, bool noDepthTest, float margin ) const { Vector nw = GetCorner( NORTH_WEST ) + Vector( margin, margin, 0.0f ); Vector ne = GetCorner( NORTH_EAST ) + Vector( -margin, margin, 0.0f ); Vector sw = GetCorner( SOUTH_WEST ) + Vector( margin, -margin, 0.0f ); Vector se = GetCorner( SOUTH_EAST ) + Vector( -margin, -margin, 0.0f ); if ( a == 0 ) { NDebugOverlay::Line( nw, ne, r, g, b, true, deltaT ); NDebugOverlay::Line( nw, sw, r, g, b, true, deltaT ); NDebugOverlay::Line( sw, se, r, g, b, true, deltaT ); NDebugOverlay::Line( se, ne, r, g, b, true, deltaT ); } else { NDebugOverlay::Triangle( nw, se, ne, r, g, b, a, noDepthTest, deltaT ); NDebugOverlay::Triangle( se, nw, sw, r, g, b, a, noDepthTest, deltaT ); } // backside // NDebugOverlay::Triangle( nw, ne, se, r, g, b, a, noDepthTest, deltaT ); // NDebugOverlay::Triangle( se, sw, nw, r, g, b, a, noDepthTest, deltaT ); } //-------------------------------------------------------------------------------------------------------- void CNavArea::DrawSelectedSet( const Vector &shift ) const { const float deltaT = NDEBUG_PERSIST_TILL_NEXT_SERVER; int r = s_selectedSetColor.r(); int g = s_selectedSetColor.g(); int b = s_selectedSetColor.b(); int a = s_selectedSetColor.a(); Vector nw = GetCorner( NORTH_WEST ) + shift; Vector ne = GetCorner( NORTH_EAST ) + shift; Vector sw = GetCorner( SOUTH_WEST ) + shift; Vector se = GetCorner( SOUTH_EAST ) + shift; NDebugOverlay::Triangle( nw, se, ne, r, g, b, a, true, deltaT ); NDebugOverlay::Triangle( se, nw, sw, r, g, b, a, true, deltaT ); r = s_selectedSetBorderColor.r(); g = s_selectedSetBorderColor.g(); b = s_selectedSetBorderColor.b(); NDebugOverlay::Line( nw, ne, r, g, b, true, deltaT ); NDebugOverlay::Line( nw, sw, r, g, b, true, deltaT ); NDebugOverlay::Line( sw, se, r, g, b, true, deltaT ); NDebugOverlay::Line( se, ne, r, g, b, true, deltaT ); } //-------------------------------------------------------------------------------------------------------- void CNavArea::DrawDragSelectionSet( Color &dragSelectionSetColor ) const { const float deltaT = NDEBUG_PERSIST_TILL_NEXT_SERVER; int r = dragSelectionSetColor.r(); int g = dragSelectionSetColor.g(); int b = dragSelectionSetColor.b(); int a = dragSelectionSetColor.a(); Vector nw = GetCorner( NORTH_WEST ); Vector ne = GetCorner( NORTH_EAST ); Vector sw = GetCorner( SOUTH_WEST ); Vector se = GetCorner( SOUTH_EAST ); NDebugOverlay::Triangle( nw, se, ne, r, g, b, a, true, deltaT ); NDebugOverlay::Triangle( se, nw, sw, r, g, b, a, true, deltaT ); r = s_dragSelectionSetBorderColor.r(); g = s_dragSelectionSetBorderColor.g(); b = s_dragSelectionSetBorderColor.b(); NDebugOverlay::Line( nw, ne, r, g, b, true, deltaT ); NDebugOverlay::Line( nw, sw, r, g, b, true, deltaT ); NDebugOverlay::Line( sw, se, r, g, b, true, deltaT ); NDebugOverlay::Line( se, ne, r, g, b, true, deltaT ); } //-------------------------------------------------------------------------------------------------------------- /** * Draw navigation areas and edit them */ void CNavArea::DrawHidingSpots( void ) const { const HidingSpotVector *hidingSpots = GetHidingSpots(); FOR_EACH_VEC( (*hidingSpots), it ) { const HidingSpot *spot = (*hidingSpots)[ it ]; NavEditColor color; if (spot->IsIdealSniperSpot()) { color = NavIdealSniperColor; } else if (spot->IsGoodSniperSpot()) { color = NavGoodSniperColor; } else if (spot->HasGoodCover()) { color = NavGoodCoverColor; } else { color = NavExposedColor; } NavDrawLine( spot->GetPosition(), spot->GetPosition() + Vector( 0, 0, 50 ), color ); } } //-------------------------------------------------------------------------------------------------------------- /** * Draw ourselves and adjacent areas */ void CNavArea::DrawConnectedAreas( void ) const { int i; CBasePlayer *player = UTIL_GetListenServerHost(); if (player == NULL) return; // draw self if (TheNavMesh->IsEditMode( CNavMesh::PLACE_PAINTING )) { Draw(); } else { Draw(); DrawHidingSpots(); } // draw connected ladders { FOR_EACH_VEC( m_ladder[ CNavLadder::LADDER_UP ], it ) { CNavLadder *ladder = m_ladder[ CNavLadder::LADDER_UP ][ it ].ladder; ladder->DrawLadder(); if ( !ladder->IsConnected( this, CNavLadder::LADDER_DOWN ) ) { NavDrawLine( m_center, ladder->m_bottom + Vector( 0, 0, GenerationStepSize ), NavConnectedOneWayColor ); } } } { FOR_EACH_VEC( m_ladder[ CNavLadder::LADDER_DOWN ], it ) { CNavLadder *ladder = m_ladder[ CNavLadder::LADDER_DOWN ][ it ].ladder; ladder->DrawLadder(); if ( !ladder->IsConnected( this, CNavLadder::LADDER_UP ) ) { NavDrawLine( m_center, ladder->m_top, NavConnectedOneWayColor ); } } } // draw connected areas for( i=0; iDraw(); if ( !TheNavMesh->IsEditMode( CNavMesh::PLACE_PAINTING ) ) { adj->DrawHidingSpots(); Vector from, to; Vector hookPos; float halfWidth; float size = 5.0f; ComputePortal( adj, dir, &hookPos, &halfWidth ); switch( dir ) { case NORTH: from = hookPos + Vector( 0.0f, size, 0.0f ); to = hookPos + Vector( 0.0f, -size, 0.0f ); break; case SOUTH: from = hookPos + Vector( 0.0f, -size, 0.0f ); to = hookPos + Vector( 0.0f, size, 0.0f ); break; case EAST: from = hookPos + Vector( -size, 0.0f, 0.0f ); to = hookPos + Vector( +size, 0.0f, 0.0f ); break; case WEST: from = hookPos + Vector( size, 0.0f, 0.0f ); to = hookPos + Vector( -size, 0.0f, 0.0f ); break; } from.z = GetZ( from ); to.z = adj->GetZ( to ); Vector drawTo; adj->GetClosestPointOnArea( to, &drawTo ); if ( nav_show_contiguous.GetBool() ) { if ( IsContiguous( adj ) ) NavDrawLine( from, drawTo, NavConnectedContiguous ); else NavDrawLine( from, drawTo, NavConnectedNonContiguous ); } else { if ( adj->IsConnected( this, OppositeDirection( dir ) ) ) NavDrawLine( from, drawTo, NavConnectedTwoWaysColor ); else NavDrawLine( from, drawTo, NavConnectedOneWayColor ); } } } } } //-------------------------------------------------------------------------------------------------------------- /** * Add to open list in decreasing value order */ void CNavArea::AddToOpenList( void ) { Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); if ( IsOpen() ) { // already on list return; } // mark as being on open list for quick check m_openMarker = m_masterMarker; // if list is empty, add and return if ( m_openList == NULL ) { m_openList = this; m_openListTail = this; this->m_prevOpen = NULL; this->m_nextOpen = NULL; return; } // insert self in ascending cost order // Since costs are positive, IEEE754 let's us compare as integers (see http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm) CNavArea *area, *last = NULL; int thisCostBits = *reinterpret_cast(&m_totalCost); Assert ( m_totalCost >= 0.0f ); for( area = m_openList; area; area = area->m_nextOpen ) { Assert ( area->GetTotalCost() >= 0.0f ); int thoseCostBits = *reinterpret_cast(&area->m_totalCost); if ( thisCostBits < thoseCostBits ) { break; } last = area; } if ( area ) { // insert before this area this->m_prevOpen = area->m_prevOpen; if ( this->m_prevOpen ) { this->m_prevOpen->m_nextOpen = this; } else { m_openList = this; } this->m_nextOpen = area; area->m_prevOpen = this; } else { // append to end of list last->m_nextOpen = this; this->m_prevOpen = last; this->m_nextOpen = NULL; m_openListTail = this; } Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); } //-------------------------------------------------------------------------------------------------------------- /** * Add to tail of the open list */ void CNavArea::AddToOpenListTail( void ) { Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); if ( IsOpen() ) { // already on list return; } // mark as being on open list for quick check m_openMarker = m_masterMarker; // if list is empty, add and return if ( m_openList == NULL ) { m_openList = this; m_openListTail = this; this->m_prevOpen = NULL; this->m_nextOpen = NULL; Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); return; } // append to end of list m_openListTail->m_nextOpen = this; this->m_prevOpen = m_openListTail; this->m_nextOpen = NULL; m_openListTail = this; Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); } //-------------------------------------------------------------------------------------------------------------- /** * A smaller value has been found, update this area on the open list * @todo "bubbling" does unnecessary work, since the order of all other nodes will be unchanged - only this node is altered */ void CNavArea::UpdateOnOpenList( void ) { // since value can only decrease, bubble this area up from current spot while( m_prevOpen && this->GetTotalCost() < m_prevOpen->GetTotalCost() ) { // swap position with predecessor CNavArea *other = m_prevOpen; CNavArea *before = other->m_prevOpen; CNavArea *after = this->m_nextOpen; this->m_nextOpen = other; this->m_prevOpen = before; other->m_prevOpen = this; other->m_nextOpen = after; if ( before ) { before->m_nextOpen = this; } else { m_openList = this; } if ( after ) { after->m_prevOpen = other; } else { m_openListTail = this; } } } //-------------------------------------------------------------------------------------------------------------- void CNavArea::RemoveFromOpenList( void ) { if ( m_openMarker == 0 ) { // not on the list return; } if ( m_prevOpen ) { m_prevOpen->m_nextOpen = m_nextOpen; } else { m_openList = m_nextOpen; } if ( m_nextOpen ) { m_nextOpen->m_prevOpen = m_prevOpen; } else { m_openListTail = m_prevOpen; } // zero is an invalid marker m_openMarker = 0; } //-------------------------------------------------------------------------------------------------------------- /** * Clears the open and closed lists for a new search */ void CNavArea::ClearSearchLists( void ) { // effectively clears all open list pointers and closed flags CNavArea::MakeNewMarker(); m_openList = NULL; m_openListTail = NULL; } //-------------------------------------------------------------------------------------------------------------- void CNavArea::SetCorner( NavCornerType corner, const Vector& newPosition ) { switch( corner ) { case NORTH_WEST: m_nwCorner = newPosition; break; case NORTH_EAST: m_seCorner.x = newPosition.x; m_nwCorner.y = newPosition.y; m_neZ = newPosition.z; break; case SOUTH_WEST: m_nwCorner.x = newPosition.x; m_seCorner.y = newPosition.y; m_swZ = newPosition.z; break; case SOUTH_EAST: m_seCorner = newPosition; break; default: { Vector oldPosition = GetCenter(); Vector delta = newPosition - oldPosition; m_nwCorner += delta; m_seCorner += delta; m_neZ += delta.z; m_swZ += delta.z; } } m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) { m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); } else { m_invDxCorners = m_invDyCorners = 0; } CalcDebugID(); } //-------------------------------------------------------------------------------------------------------------- /** * Returns true if an existing hiding spot is too close to given position */ bool CNavArea::IsHidingSpotCollision( const Vector &pos ) const { const float collisionRange = 30.0f; FOR_EACH_VEC( m_hidingSpots, it ) { const HidingSpot *spot = m_hidingSpots[ it ]; if ((spot->GetPosition() - pos).IsLengthLessThan( collisionRange )) return true; } return false; } //-------------------------------------------------------------------------------------------------------------- bool IsHidingSpotInCover( const Vector &spot ) { int coverCount = 0; trace_t result; Vector from = spot; from.z += HalfHumanHeight; Vector to; // if we are crouched underneath something, that counts as good cover to = from + Vector( 0, 0, 20.0f ); UTIL_TraceLine( from, to, MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); if (result.fraction != 1.0f) return true; const float coverRange = 100.0f; const float inc = M_PI / 8.0f; for( float angle = 0.0f; angle < 2.0f * M_PI; angle += inc ) { to = from + Vector( coverRange * (float)cos(angle), coverRange * (float)sin(angle), HalfHumanHeight ); UTIL_TraceLine( from, to, MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); // if traceline hit something, it hit "cover" if (result.fraction != 1.0f) ++coverCount; } // if more than half of the circle has no cover, the spot is not "in cover" const int halfCover = 8; if (coverCount < halfCover) return false; return true; } //-------------------------------------------------------------------------------------------------------------- /** * Finds the hiding spot position in a corner's area. If the typical inset is off the nav area (small * hand-constructed areas), it tries to fit the position inside the area. */ static Vector FindPositionInArea( CNavArea *area, NavCornerType corner ) { int multX = 1, multY = 1; switch ( corner ) { case NORTH_WEST: break; case NORTH_EAST: multX = -1; break; case SOUTH_WEST: multY = -1; break; case SOUTH_EAST: multX = -1; multY = -1; break; } const float offset = 12.5f; Vector cornerPos = area->GetCorner( corner ); // Try the basic inset Vector pos = cornerPos + Vector( offset*multX, offset*multY, 0.0f ); if ( !area->IsOverlapping( pos ) ) { // Try pulling the Y offset to the area's center pos = cornerPos + Vector( offset*multX, area->GetSizeY()*0.5f*multY, 0.0f ); if ( !area->IsOverlapping( pos ) ) { // Try pulling the X offset to the area's center pos = cornerPos + Vector( area->GetSizeX()*0.5f*multX, offset*multY, 0.0f ); if ( !area->IsOverlapping( pos ) ) { // Try pulling the X and Y offsets to the area's center pos = cornerPos + Vector( area->GetSizeX()*0.5f*multX, area->GetSizeY()*0.5f*multY, 0.0f ); if ( !area->IsOverlapping( pos ) ) { AssertMsg( false, UTIL_VarArgs( "A Hiding Spot can't be placed on its area at (%.0f %.0f %.0f)", cornerPos.x, cornerPos.y, cornerPos.z) ); // Just pull the position to a small offset pos = cornerPos + Vector( 1.0f*multX, 1.0f*multY, 0.0f ); if ( !area->IsOverlapping( pos ) ) { // Nothing is working (degenerate area?), so just put it directly on the corner pos = cornerPos; } } } } } return pos; } //-------------------------------------------------------------------------------------------------------------- /** * Analyze local area neighborhood to find "hiding spots" for this area */ void CNavArea::ComputeHidingSpots( void ) { struct { float lo, hi; } extent; m_hidingSpots.PurgeAndDeleteElements(); // "jump areas" cannot have hiding spots if ( GetAttributes() & NAV_MESH_JUMP ) return; // "don't hide areas" cannot have hiding spots if ( GetAttributes() & NAV_MESH_DONT_HIDE ) return; int cornerCount[NUM_CORNERS]; for( int i=0; iIsConnected( this, OppositeDirection( static_cast( d ) ) ) == false) continue; // ignore jump areas if (connect.area->GetAttributes() & NAV_MESH_JUMP) continue; if (isHoriz) { if (connect.area->m_nwCorner.x < extent.lo) extent.lo = connect.area->m_nwCorner.x; if (connect.area->m_seCorner.x > extent.hi) extent.hi = connect.area->m_seCorner.x; } else { if (connect.area->m_nwCorner.y < extent.lo) extent.lo = connect.area->m_nwCorner.y; if (connect.area->m_seCorner.y > extent.hi) extent.hi = connect.area->m_seCorner.y; } } switch( d ) { case NORTH: if (extent.lo - m_nwCorner.x >= cornerSize) ++cornerCount[ NORTH_WEST ]; if (m_seCorner.x - extent.hi >= cornerSize) ++cornerCount[ NORTH_EAST ]; break; case SOUTH: if (extent.lo - m_nwCorner.x >= cornerSize) ++cornerCount[ SOUTH_WEST ]; if (m_seCorner.x - extent.hi >= cornerSize) ++cornerCount[ SOUTH_EAST ]; break; case EAST: if (extent.lo - m_nwCorner.y >= cornerSize) ++cornerCount[ NORTH_EAST ]; if (m_seCorner.y - extent.hi >= cornerSize) ++cornerCount[ SOUTH_EAST ]; break; case WEST: if (extent.lo - m_nwCorner.y >= cornerSize) ++cornerCount[ NORTH_WEST ]; if (m_seCorner.y - extent.hi >= cornerSize) ++cornerCount[ SOUTH_WEST ]; break; } } for ( int c=0; cCreateHidingSpot(); spot->SetPosition( pos ); spot->SetFlags( IsHidingSpotInCover( pos ) ? HidingSpot::IN_COVER : HidingSpot::EXPOSED ); m_hidingSpots.AddToTail( spot ); } } } } //-------------------------------------------------------------------------------------------------------------- /** * Determine how much walkable area we can see from the spot, and how far away we can see. */ void ClassifySniperSpot( HidingSpot *spot ) { Vector eye = spot->GetPosition(); CNavArea *hidingArea = TheNavMesh->GetNavArea( spot->GetPosition() ); if (hidingArea && (hidingArea->GetAttributes() & NAV_MESH_STAND)) { // we will be standing at this hiding spot eye.z += HumanEyeHeight; } else { // we are crouching when at this hiding spot eye.z += HumanCrouchEyeHeight; } Vector walkable; trace_t result; Extent sniperExtent; float farthestRangeSq = 0.0f; const float minSniperRangeSq = 1000.0f * 1000.0f; bool found = false; // to make compiler stop warning me sniperExtent.lo = Vector( 0.0f, 0.0f, 0.0f ); sniperExtent.hi = Vector( 0.0f, 0.0f, 0.0f ); Extent areaExtent; FOR_EACH_VEC( TheNavAreas, it ) { CNavArea *area = TheNavAreas[ it ]; area->GetExtent( &areaExtent ); // scan this area for( walkable.y = areaExtent.lo.y + GenerationStepSize/2.0f; walkable.y < areaExtent.hi.y; walkable.y += GenerationStepSize ) { for( walkable.x = areaExtent.lo.x + GenerationStepSize/2.0f; walkable.x < areaExtent.hi.x; walkable.x += GenerationStepSize ) { walkable.z = area->GetZ( walkable ) + HalfHumanHeight; // check line of sight UTIL_TraceLine( eye, walkable, CONTENTS_SOLID|CONTENTS_MOVEABLE|CONTENTS_PLAYERCLIP, NULL, COLLISION_GROUP_NONE, &result ); if (result.fraction == 1.0f && !result.startsolid) { // can see this spot // keep track of how far we can see float rangeSq = (eye - walkable).LengthSqr(); if (rangeSq > farthestRangeSq) { farthestRangeSq = rangeSq; if (rangeSq >= minSniperRangeSq) { // this is a sniper spot // determine how good of a sniper spot it is by keeping track of the snipable area if (found) { if (walkable.x < sniperExtent.lo.x) sniperExtent.lo.x = walkable.x; if (walkable.x > sniperExtent.hi.x) sniperExtent.hi.x = walkable.x; if (walkable.y < sniperExtent.lo.y) sniperExtent.lo.y = walkable.y; if (walkable.y > sniperExtent.hi.y) sniperExtent.hi.y = walkable.y; } else { sniperExtent.lo = walkable; sniperExtent.hi = walkable; found = true; } } } } } } } if (found) { // if we can see a large snipable area, it is an "ideal" spot float snipableArea = sniperExtent.Area(); const float minIdealSniperArea = 200.0f * 200.0f; const float longSniperRangeSq = 1500.0f * 1500.0f; if (snipableArea >= minIdealSniperArea || farthestRangeSq >= longSniperRangeSq) spot->m_flags |= HidingSpot::IDEAL_SNIPER_SPOT; else spot->m_flags |= HidingSpot::GOOD_SNIPER_SPOT; } } //-------------------------------------------------------------------------------------------------------------- /** * Analyze local area neighborhood to find "sniper spots" for this area */ void CNavArea::ComputeSniperSpots( void ) { if (nav_quicksave.GetBool()) return; FOR_EACH_VEC( m_hidingSpots, it ) { HidingSpot *spot = m_hidingSpots[ it ]; ClassifySniperSpot( spot ); } } //-------------------------------------------------------------------------------------------------------------- /** * Given the areas we are moving between, return the spots we will encounter */ SpotEncounter *CNavArea::GetSpotEncounter( const CNavArea *from, const CNavArea *to ) { if (from && to) { SpotEncounter *e; FOR_EACH_VEC( m_spotEncounters, it ) { e = m_spotEncounters[ it ]; if (e->from.area == from && e->to.area == to) return e; } } return NULL; } //-------------------------------------------------------------------------------------------------------------- /** * Add spot encounter data when moving from area to area */ void CNavArea::AddSpotEncounters( const CNavArea *from, NavDirType fromDir, const CNavArea *to, NavDirType toDir ) { SpotEncounter *e = new SpotEncounter; e->from.area = const_cast( from ); e->fromDir = fromDir; e->to.area = const_cast( to ); e->toDir = toDir; float halfWidth; ComputePortal( to, toDir, &e->path.to, &halfWidth ); ComputePortal( from, fromDir, &e->path.from, &halfWidth ); const float eyeHeight = HumanEyeHeight; e->path.from.z = from->GetZ( e->path.from ) + eyeHeight; e->path.to.z = to->GetZ( e->path.to ) + eyeHeight; // step along ray and track which spots can be seen Vector dir = e->path.to - e->path.from; float length = dir.NormalizeInPlace(); // create unique marker to flag used spots HidingSpot::ChangeMasterMarker(); const float stepSize = 25.0f; // 50 const float seeSpotRange = 2000.0f; // 3000 trace_t result; Vector eye, delta; HidingSpot *spot; SpotOrder spotOrder; // step along path thru this area bool done = false; for( float along = 0.0f; !done; along += stepSize ) { // make sure we check the endpoint of the path segment if (along >= length) { along = length; done = true; } // move the eyepoint along the path segment eye = e->path.from + along * dir; // check each hiding spot for visibility FOR_EACH_VEC( TheHidingSpots, it ) { spot = TheHidingSpots[ it ]; // only look at spots with cover (others are out in the open and easily seen) if (!spot->HasGoodCover()) continue; if (spot->IsMarked()) continue; const Vector &spotPos = spot->GetPosition(); delta.x = spotPos.x - eye.x; delta.y = spotPos.y - eye.y; delta.z = (spotPos.z + eyeHeight) - eye.z; // check if in range if (delta.IsLengthGreaterThan( seeSpotRange )) continue; // check if we have LOS // BOTPORT: ignore glass here UTIL_TraceLine( eye, Vector( spotPos.x, spotPos.y, spotPos.z + HalfHumanHeight ), MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); if (result.fraction != 1.0f) continue; // if spot is in front of us along our path, ignore it delta.NormalizeInPlace(); float dot = DotProduct( dir, delta ); if (dot < 0.7071f && dot > -0.7071f) { // we only want to keep spots that BECOME visible as we walk past them // therefore, skip ALL visible spots at the start of the path segment if (along > 0.0f) { // add spot to encounter spotOrder.spot = spot; spotOrder.t = along/length; e->spots.AddToTail( spotOrder ); } } // mark spot as encountered spot->Mark(); } } // add encounter to list m_spotEncounters.AddToTail( e ); } //-------------------------------------------------------------------------------------------------------------- /** * Compute "spot encounter" data. This is an ordered list of spots to look at * for each possible path thru a nav area. */ void CNavArea::ComputeSpotEncounters( void ) { m_spotEncounters.RemoveAll(); if (nav_quicksave.GetBool()) return; // for each adjacent area for( int fromDir=0; fromDirarea, (NavDirType)fromDir, toCon->area, (NavDirType)toDir ); } } } } } //-------------------------------------------------------------------------------------------------------------- /** * Decay the danger values */ void CNavArea::DecayDanger( void ) { for( int i=0; icurtime - m_dangerTimestamp[i]; float decayAmount = GetDangerDecayRate() * deltaT; m_danger[i] -= decayAmount; if (m_danger[i] < 0.0f) m_danger[i] = 0.0f; // update timestamp m_dangerTimestamp[i] = gpGlobals->curtime; } } //-------------------------------------------------------------------------------------------------------------- /** * Increase the danger of this area for the given team */ void CNavArea::IncreaseDanger( int teamID, float amount ) { // before we add the new value, decay what's there DecayDanger(); int teamIdx = teamID % MAX_NAV_TEAMS; m_danger[ teamIdx ] += amount; m_dangerTimestamp[ teamIdx ] = gpGlobals->curtime; } //-------------------------------------------------------------------------------------------------------------- /** * Return the danger of this area (decays over time) */ float CNavArea::GetDanger( int teamID ) { DecayDanger(); int teamIdx = teamID % MAX_NAV_TEAMS; return m_danger[ teamIdx ]; } //-------------------------------------------------------------------------------------------------------------- /** * Returns a 0..1 light intensity for the given point */ float CNavArea::GetLightIntensity( const Vector &pos ) const { Vector testPos; testPos.x = clamp( pos.x, m_nwCorner.x, m_seCorner.x ); testPos.y = clamp( pos.y, m_nwCorner.y, m_seCorner.y ); testPos.z = pos.z; float dX = (testPos.x - m_nwCorner.x) / (m_seCorner.x - m_nwCorner.x); float dY = (testPos.y - m_nwCorner.y) / (m_seCorner.y - m_nwCorner.y); float northLight = m_lightIntensity[ NORTH_WEST ] * ( 1 - dX ) + m_lightIntensity[ NORTH_EAST ] * dX; float southLight = m_lightIntensity[ SOUTH_WEST ] * ( 1 - dX ) + m_lightIntensity[ SOUTH_EAST ] * dX; float light = northLight * ( 1 - dY ) + southLight * dY; return light; } //-------------------------------------------------------------------------------------------------------------- /** * Returns a 0..1 light intensity for the given point */ float CNavArea::GetLightIntensity( float x, float y ) const { return GetLightIntensity( Vector( x, y, 0 ) ); } //-------------------------------------------------------------------------------------------------------------- /** * Returns a 0..1 light intensity averaged over the whole area */ float CNavArea::GetLightIntensity( void ) const { float light = m_lightIntensity[ NORTH_WEST ]; light += m_lightIntensity[ NORTH_EAST ]; light += m_lightIntensity[ SOUTH_WEST]; light += m_lightIntensity[ SOUTH_EAST ]; return light / 4.0f; } //-------------------------------------------------------------------------------------------------------------- /** * Compute light intensity at corners and center (requires client via listenserver) */ bool CNavArea::ComputeLighting( void ) { if ( engine->IsDedicatedServer() ) { for ( int i=0; iGetGroundHeight( pos, &height ) ) { pos.z = height + HalfHumanHeight - StepHeight; // players light from their centers, and we light from slightly below that, to allow for low ceilings } Vector light( 0, 0, 0 ); // FIXMEL4DTOMAINMERGE //if ( !engine->GetLightForPointListenServerOnly( pos, false, &light ) ) //{ //NDebugOverlay::Line( pos, pos + Vector( 0, 0, -100 ), 255, 0, 0, false, 100.0f ); // return false; //} Vector ambientColor; // FIXMEL4DTOMAINMERGE //if ( !GetTerrainAmbientLightAtPoint( pos, &ambientColor ) ) { //NDebugOverlay::Line( pos, pos + Vector( 0, 0, -100 ), 255, 127, 0, false, 100.0f ); return false; } //NDebugOverlay::Line( pos, pos + Vector( 0, 0, -100 ), 0, 255, 127, false, 100.0f ); float ambientIntensity = ambientColor.x + ambientColor.y + ambientColor.z; float lightIntensity = light.x + light.y + light.z; lightIntensity = clamp( lightIntensity, 0.f, 1.f ); // sum can go well over 1.0, but it's the lower region we care about. if it's bright, we don't need to know *how* bright. lightIntensity = MAX( lightIntensity, ambientIntensity ); m_lightIntensity[i] = lightIntensity; } return true; } //-------------------------------------------------------------------------------------------------------------- CON_COMMAND_F( nav_update_lighting, "Recomputes lighting values", FCVAR_CHEAT ) { int numComputed = 0; if ( args.ArgC() == 2 ) { int areaID = atoi( args[1] ); CNavArea *area = TheNavMesh->GetNavAreaByID( areaID ); if ( area ) { if ( area->ComputeLighting() ) { ++numComputed; } } } else { FOR_EACH_VEC( TheNavAreas, index ) { CNavArea *area = TheNavAreas[ index ]; if ( area->ComputeLighting() ) { ++numComputed; } } } DevMsg( "Computed lighting for %d/%d areas\n", numComputed, TheNavAreas.Count() ); } //-------------------------------------------------------------------------------------------------------------- /** * Raise/lower a corner */ void CNavArea::RaiseCorner( NavCornerType corner, int amount, bool raiseAdjacentCorners ) { if ( corner == NUM_CORNERS ) { RaiseCorner( NORTH_WEST, amount, raiseAdjacentCorners ); RaiseCorner( NORTH_EAST, amount, raiseAdjacentCorners ); RaiseCorner( SOUTH_WEST, amount, raiseAdjacentCorners ); RaiseCorner( SOUTH_EAST, amount, raiseAdjacentCorners ); return; } // Move the corner switch (corner) { case NORTH_WEST: m_nwCorner.z += amount; break; case NORTH_EAST: m_neZ += amount; break; case SOUTH_WEST: m_swZ += amount; break; case SOUTH_EAST: m_seCorner.z += amount; break; } // Recompute the center m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) { m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); } else { m_invDxCorners = m_invDyCorners = 0; } if ( !raiseAdjacentCorners || nav_corner_adjust_adjacent.GetFloat() <= 0.0f ) { return; } // Find nearby areas that share the corner CNavArea::MakeNewMarker(); Mark(); const float tolerance = nav_corner_adjust_adjacent.GetFloat(); Vector cornerPos = GetCorner( corner ); cornerPos.z -= amount; // use the pre-adjustment corner for adjacency checks int gridX = TheNavMesh->WorldToGridX( cornerPos.x ); int gridY = TheNavMesh->WorldToGridY( cornerPos.y ); const int shift = 1; // try a 3x3 set of grids in case we're on the edge for( int x = gridX - shift; x <= gridX + shift; ++x ) { if (x < 0 || x >= TheNavMesh->m_gridSizeX) continue; for( int y = gridY - shift; y <= gridY + shift; ++y ) { if (y < 0 || y >= TheNavMesh->m_gridSizeY) continue; NavAreaVector *areas = &TheNavMesh->m_grid[ x + y*TheNavMesh->m_gridSizeX ]; // find closest area in this cell FOR_EACH_VEC( (*areas), it ) { CNavArea *area = (*areas)[ it ]; // skip if we've already visited this area if (area->IsMarked()) continue; area->Mark(); Vector areaPos; for ( int i=0; iGetCorner( NavCornerType(i) ); if ( areaPos.DistTo( cornerPos ) < tolerance ) { float heightDiff = (cornerPos.z + amount ) - areaPos.z; area->RaiseCorner( NavCornerType(i), heightDiff, false ); } } } } } } //-------------------------------------------------------------------------------------------------------------- /** * FindGroundZFromPoint walks from the start position to the end position in GenerationStepSize increments, * checking the ground height along the way. */ float FindGroundZFromPoint( const Vector& end, const Vector& start ) { Vector step( 0, 0, StepHeight ); if ( fabs( end.x - start.x ) > fabs( end.y - start.y ) ) { step.x = GenerationStepSize; if ( end.x < start.x ) { step.x = -step.x; } } else { step.y = GenerationStepSize; if ( end.y < start.y ) { step.y = -step.y; } } // step towards our end point Vector point = start; float z; while ( point.AsVector2D().DistTo( end.AsVector2D() ) > GenerationStepSize ) { point = point + step; z = point.z; if ( TheNavMesh->GetGroundHeight( point, &z ) ) { point.z = z; } else { point.z -= step.z; } } // now do the exact one once we're within GenerationStepSize of it z = point.z + step.z; point = end; point.z = z; if ( TheNavMesh->GetGroundHeight( point, &z ) ) { point.z = z; } else { point.z -= step.z; } return point.z; } //-------------------------------------------------------------------------------------------------------------- /** * Finds the Z value for a corner given two other corner points. This walks along the edges of the nav area * in GenerationStepSize increments, to increase accuracy. */ float FindGroundZ( const Vector& original, const Vector& corner1, const Vector& corner2 ) { float first = FindGroundZFromPoint( original, corner1 ); float second = FindGroundZFromPoint( original, corner2 ); if ( fabs( first - second ) > StepHeight ) { // approaching the point from the two directions didn't agree. Take the one closest to the original z. if ( fabs( original.z - first ) > fabs( original.z - second ) ) { return second; } else { return first; } } return first; } //-------------------------------------------------------------------------------------------------------------- /** * Places a corner (or all corners if corner == NUM_CORNERS) on the ground */ void CNavArea::PlaceOnGround( NavCornerType corner, float inset ) { trace_t result; Vector from, to; Vector nw = m_nwCorner + Vector ( inset, inset, 0 ); Vector se = m_seCorner + Vector ( -inset, -inset, 0 ); Vector ne, sw; ne.x = se.x; ne.y = nw.y; ne.z = m_neZ; sw.x = nw.x; sw.y = se.y; sw.z = m_swZ; if ( corner == NORTH_WEST || corner == NUM_CORNERS ) { float newZ = FindGroundZ( nw, ne, sw ); RaiseCorner( NORTH_WEST, newZ - nw.z ); } if ( corner == NORTH_EAST || corner == NUM_CORNERS ) { float newZ = FindGroundZ( ne, nw, se ); RaiseCorner( NORTH_EAST, newZ - ne.z ); } if ( corner == SOUTH_WEST || corner == NUM_CORNERS ) { float newZ = FindGroundZ( sw, nw, se ); RaiseCorner( SOUTH_WEST, newZ - sw.z ); } if ( corner == SOUTH_EAST || corner == NUM_CORNERS ) { float newZ = FindGroundZ( se, ne, sw ); RaiseCorner( SOUTH_EAST, newZ - se.z ); } } //-------------------------------------------------------------------------------------------------------------- /** * Shift the nav area */ void CNavArea::Shift( const Vector &shift ) { m_nwCorner += shift; m_seCorner += shift; m_center += shift; } //-------------------------------------------------------------------------------------------------------------- static void CommandNavUpdateBlocked( void ) { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; if ( TheNavMesh->GetMarkedArea() ) { CNavArea *area = TheNavMesh->GetMarkedArea(); area->UpdateBlocked( true ); if ( area->IsBlocked( TEAM_ANY ) ) { DevMsg( "Area #%d %s is blocked\n", area->GetID(), VecToString( area->GetCenter() + Vector( 0, 0, HalfHumanHeight ) ) ); } } else { float start = Plat_FloatTime(); CNavArea *blockedArea = NULL; FOR_EACH_VEC( TheNavAreas, nit ) { CNavArea *area = TheNavAreas[ nit ]; area->UpdateBlocked( true ); if ( area->IsBlocked( TEAM_ANY ) ) { DevMsg( "Area #%d %s is blocked\n", area->GetID(), VecToString( area->GetCenter() + Vector( 0, 0, HalfHumanHeight ) ) ); if ( !blockedArea ) { blockedArea = area; } } } float end = Plat_FloatTime(); float time = (end - start) * 1000.0f; DevMsg( "nav_update_blocked took %2.2f ms\n", time ); if ( blockedArea ) { CBasePlayer *player = UTIL_GetListenServerHost(); if ( player ) { if ( ( player->IsDead() || player->IsObserver() ) && player->GetObserverMode() == OBS_MODE_ROAMING ) { Vector origin = blockedArea->GetCenter() + Vector( 0, 0, 0.75f * HumanHeight ); UTIL_SetOrigin( player, origin ); } } } } } static ConCommand nav_update_blocked( "nav_update_blocked", CommandNavUpdateBlocked, "Updates the blocked/unblocked status for every nav area.", FCVAR_GAMEDLL ); //-------------------------------------------------------------------------------------------------------- bool CNavArea::IsBlocked( int teamID, bool ignoreNavBlockers ) const { if ( ignoreNavBlockers && ( m_attributeFlags & NAV_MESH_NAV_BLOCKER ) ) { return false; } #ifdef TERROR if ( ( teamID == TEAM_SURVIVOR ) && ( m_attributeFlags & CNavArea::NAV_PLAYERCLIP ) ) return true; #endif if ( teamID == TEAM_ANY ) { bool isBlocked = false; for ( int i=0; iClassMatches( "func_nav_blocker" ) ) { m_attributeFlags |= NAV_MESH_NAV_BLOCKER; } bool wasBlocked = false; if ( teamID == TEAM_ANY ) { for ( int i=0; iCreateEvent( "nav_blocked" ); if ( event ) { event->SetInt( "area", m_id ); event->SetInt( "blocked", 1 ); gameeventmanager->FireEvent( event ); } } if ( nav_debug_blocked.GetBool() ) { if ( blocker ) { ConColorMsg( Color( 0, 255, 128, 255 ), "%s %d blocked area %d\n", blocker->GetDebugName(), blocker->entindex(), GetID() ); } else { ConColorMsg( Color( 0, 255, 128, 255 ), "non-entity blocked area %d\n", GetID() ); } } TheNavMesh->OnAreaBlocked( this ); } else { if ( nav_debug_blocked.GetBool() ) { if ( blocker ) { ConColorMsg( Color( 0, 255, 128, 255 ), "DUPE: %s %d blocked area %d\n", blocker->GetDebugName(), blocker->entindex(), GetID() ); } else { ConColorMsg( Color( 0, 255, 128, 255 ), "DUPE: non-entity blocked area %d\n", GetID() ); } } } } //-------------------------------------------------------------------------------------------------------- // checks if any func_nav_blockers are still blocking the area void CNavArea::UpdateBlockedFromNavBlockers( void ) { VPROF( "CNavArea::UpdateBlockedFromNavBlockers" ); Extent bounds; GetExtent( &bounds ); // Save off old values, reset to not blocked state m_attributeFlags &= ~NAV_MESH_NAV_BLOCKER; bool oldBlocked[MAX_NAV_TEAMS]; bool wasBlocked = false; for ( int i=0; iCreateEvent( "nav_blocked" ); if ( event ) { event->SetInt( "area", m_id ); event->SetInt( "blocked", isBlocked ); gameeventmanager->FireEvent( event ); } if ( isBlocked ) { if ( nav_debug_blocked.GetBool() ) { ConColorMsg( Color( 0, 255, 128, 255 ), "area %d is blocked by a nav blocker\n", GetID() ); } TheNavMesh->OnAreaBlocked( this ); } else { if ( nav_debug_blocked.GetBool() ) { ConColorMsg( Color( 0, 128, 255, 255 ), "area %d is unblocked by a nav blocker\n", GetID() ); } TheNavMesh->OnAreaUnblocked( this ); } } } //-------------------------------------------------------------------------------------------------------------- void CNavArea::UnblockArea( int teamID ) { bool wasBlocked = IsBlocked( teamID ); if ( teamID == TEAM_ANY ) { for ( int i=0; iCreateEvent( "nav_blocked" ); if ( event ) { event->SetInt( "area", m_id ); event->SetInt( "blocked", false ); gameeventmanager->FireEvent( event ); } if ( nav_debug_blocked.GetBool() ) { ConColorMsg( Color( 255, 0, 128, 255 ), "area %d is unblocked by UnblockArea\n", GetID() ); } TheNavMesh->OnAreaUnblocked( this ); } } //-------------------------------------------------------------------------------------------------------------- /** * Updates the (un)blocked status of the nav area * The semantics of this method have gotten very muddled - needs refactoring (MSB 5/7/09) */ void CNavArea::UpdateBlocked( bool force, int teamID ) { VPROF( "CNavArea::UpdateBlocked" ); if ( !force && !m_blockedTimer.IsElapsed() ) { return; } const float MaxBlockedCheckInterval = 5; float interval = m_blockedTimer.GetCountdownDuration() + 1; if ( interval > MaxBlockedCheckInterval ) { interval = MaxBlockedCheckInterval; } m_blockedTimer.Start( interval ); if ( ( m_attributeFlags & NAV_MESH_NAV_BLOCKER ) ) { if ( force ) { UpdateBlockedFromNavBlockers(); } return; } Vector origin = GetCenter(); origin.z += HalfHumanHeight; const float sizeX = MAX( 1, MIN( GetSizeX()/2 - 5, HalfHumanWidth ) ); const float sizeY = MAX( 1, MIN( GetSizeY()/2 - 5, HalfHumanWidth ) ); Extent bounds; bounds.lo.Init( -sizeX, -sizeY, 0 ); bounds.hi.Init( sizeX, sizeY, VEC_DUCK_HULL_MAX.z - HalfHumanHeight ); bool wasBlocked = IsBlocked( TEAM_ANY ); // See if spot is valid #ifdef TERROR // don't unblock func_doors CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT, WALK_THRU_PROP_DOORS | WALK_THRU_BREAKABLES ); #else CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT, WALK_THRU_DOORS | WALK_THRU_BREAKABLES ); #endif trace_t tr; { VPROF( "CNavArea::UpdateBlocked-Trace" ); UTIL_TraceHull( origin, origin, bounds.lo, bounds.hi, MASK_NPCSOLID_BRUSHONLY, &filter, &tr ); } if ( !tr.startsolid ) { // unblock ourself #ifdef TERROR extern ConVar DebugZombieBreakables; if ( DebugZombieBreakables.GetBool() ) #else if ( false ) #endif { NDebugOverlay::Box( origin, bounds.lo, bounds.hi, 0, 255, 0, 10, 5.0f ); } else { for ( int i=0; iCreateEvent( "nav_blocked" ); if ( event ) { event->SetInt( "area", m_id ); event->SetInt( "blocked", isBlocked ); gameeventmanager->FireEvent( event ); } if ( isBlocked ) { TheNavMesh->OnAreaBlocked( this ); } else { TheNavMesh->OnAreaUnblocked( this ); } } if ( TheNavMesh->GetMarkedArea() == this ) { if ( IsBlocked( teamID ) ) { NDebugOverlay::Box( origin, bounds.lo, bounds.hi, 255, 0, 0, 64, 3.0f ); } else { NDebugOverlay::Box( origin, bounds.lo, bounds.hi, 0, 255, 0, 64, 3.0f ); } } } //-------------------------------------------------------------------------------------------------------------- /** * Checks if there is a floor under the nav area, in case a breakable floor is gone */ void CNavArea::CheckFloor( CBaseEntity *ignore ) { if ( IsBlocked( TEAM_ANY ) ) return; Vector origin = GetCenter(); origin.z -= JumpCrouchHeight; const float size = GenerationStepSize * 0.5f; Vector mins = Vector( -size, -size, 0 ); Vector maxs = Vector( size, size, JumpCrouchHeight + 10.0f ); // See if spot is valid trace_t tr; UTIL_TraceHull( origin, origin, mins, maxs, MASK_NPCSOLID_BRUSHONLY, ignore, COLLISION_GROUP_PLAYER_MOVEMENT, &tr ); // If the center is open space, we're effectively blocked if ( !tr.startsolid ) { MarkAsBlocked( TEAM_ANY, NULL ); } /* if ( IsBlocked( TEAM_ANY ) ) { NDebugOverlay::Box( origin, mins, maxs, 255, 0, 0, 64, 3.0f ); } else { NDebugOverlay::Box( origin, mins, maxs, 0, 255, 0, 64, 3.0f ); } */ } //-------------------------------------------------------------------------------------------------------- void CNavArea::MarkObstacleToAvoid( float obstructionHeight ) { if ( m_avoidanceObstacleHeight < obstructionHeight ) { if ( m_avoidanceObstacleHeight == 0 ) { TheNavMesh->OnAvoidanceObstacleEnteredArea( this ); } m_avoidanceObstacleHeight = obstructionHeight; } } //-------------------------------------------------------------------------------------------------------------- /** * Updates the (un)obstructed status of the nav area */ void CNavArea::UpdateAvoidanceObstacles( void ) { if ( !m_avoidanceObstacleTimer.IsElapsed() ) { return; } const float MaxBlockedCheckInterval = 5; float interval = m_blockedTimer.GetCountdownDuration() + 1; if ( interval > MaxBlockedCheckInterval ) { interval = MaxBlockedCheckInterval; } m_avoidanceObstacleTimer.Start( interval ); Vector mins = m_nwCorner; Vector maxs = m_seCorner; mins.z = MIN( m_nwCorner.z, m_seCorner.z ); maxs.z = MAX( m_nwCorner.z, m_seCorner.z ) + HumanCrouchHeight; float obstructionHeight = 0.0f; for ( int i=0; iGetObstructions().Count(); ++i ) { INavAvoidanceObstacle *obstruction = TheNavMesh->GetObstructions()[i]; CBaseEntity *obstructingEntity = obstruction->GetObstructingEntity(); if ( !obstructingEntity ) continue; // check if the aabb intersects the search aabb. Vector vecSurroundMins, vecSurroundMaxs; obstructingEntity->CollisionProp()->WorldSpaceSurroundingBounds( &vecSurroundMins, &vecSurroundMaxs ); if ( !IsBoxIntersectingBox( mins, maxs, vecSurroundMins, vecSurroundMaxs ) ) continue; if ( !obstruction->CanObstructNavAreas() ) continue; float propHeight = obstruction->GetNavObstructionHeight(); obstructionHeight = MAX( obstructionHeight, propHeight ); } m_avoidanceObstacleHeight = obstructionHeight; if ( m_avoidanceObstacleHeight == 0.0f ) { TheNavMesh->OnAvoidanceObstacleLeftArea( this ); } } //-------------------------------------------------------------------------------------------------------------- // Clear set of func_nav_cost entities that affect this area void CNavArea::ClearAllNavCostEntities( void ) { RemoveAttributes( NAV_MESH_FUNC_COST ); m_funcNavCostVector.RemoveAll(); } //-------------------------------------------------------------------------------------------------------------- // Add the given func_nav_cost entity to the cost of this area void CNavArea::AddFuncNavCostEntity( CFuncNavCost *cost ) { SetAttributes( NAV_MESH_FUNC_COST ); m_funcNavCostVector.AddToTail( cost ); } //-------------------------------------------------------------------------------------------------------------- // Return the cost multiplier of this area's func_nav_cost entities for the given actor float CNavArea::ComputeFuncNavCost( CBaseCombatCharacter *who ) const { float funcCost = 1.0f; for( int i=0; iGetCostMultiplier( who ); } } return funcCost; } //-------------------------------------------------------------------------------------------------------------- bool CNavArea::HasFuncNavAvoid( void ) const { for( int i=0; i( m_funcNavCostVector[i].Get() ); if ( avoid ) { return true; } } return false; } //-------------------------------------------------------------------------------------------------------------- bool CNavArea::HasFuncNavPrefer( void ) const { for( int i=0; i( m_funcNavCostVector[i].Get() ); if ( prefer ) { return true; } } return false; } //-------------------------------------------------------------------------------------------------------------- void CNavArea::CheckWaterLevel( void ) { Vector pos( GetCenter() ); if ( !TheNavMesh->GetGroundHeight( pos, &pos.z ) ) { m_isUnderwater = false; return; } pos.z += 1; m_isUnderwater = (enginetrace->GetPointContents( pos ) & MASK_WATER ) != 0; } //-------------------------------------------------------------------------------------------------------------- static void CommandNavCheckFloor( void ) { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; if ( TheNavMesh->GetMarkedArea() ) { CNavArea *area = TheNavMesh->GetMarkedArea(); area->CheckFloor( NULL ); if ( area->IsBlocked( TEAM_ANY ) ) { DevMsg( "Area #%d %s is blocked\n", area->GetID(), VecToString( area->GetCenter() + Vector( 0, 0, HalfHumanHeight ) ) ); } } else { float start = Plat_FloatTime(); FOR_EACH_VEC( TheNavAreas, nit ) { CNavArea *area = TheNavAreas[ nit ]; area->CheckFloor( NULL ); if ( area->IsBlocked( TEAM_ANY ) ) { DevMsg( "Area #%d %s is blocked\n", area->GetID(), VecToString( area->GetCenter() + Vector( 0, 0, HalfHumanHeight ) ) ); } } float end = Plat_FloatTime(); float time = (end - start) * 1000.0f; DevMsg( "nav_check_floor took %2.2f ms\n", time ); } } static ConCommand nav_check_floor( "nav_check_floor", CommandNavCheckFloor, "Updates the blocked/unblocked status for every nav area.", FCVAR_GAMEDLL ); //-------------------------------------------------------------------------------------------------------------- bool SelectOverlappingAreas::operator()( CNavArea *area ) { CNavArea *overlappingArea = NULL; CNavLadder *overlappingLadder = NULL; Vector nw = area->GetCorner( NORTH_WEST ); Vector se = area->GetCorner( SOUTH_EAST ); Vector start = nw; start.x += GenerationStepSize/2; start.y += GenerationStepSize/2; while ( start.x < se.x ) { start.y = nw.y + GenerationStepSize/2; while ( start.y < se.y ) { start.z = area->GetZ( start.x, start.y ); Vector end = start; start.z -= StepHeight; end.z += HalfHumanHeight; if ( TheNavMesh->FindNavAreaOrLadderAlongRay( start, end, &overlappingArea, &overlappingLadder, area ) ) { if ( overlappingArea ) { TheNavMesh->AddToSelectedSet( overlappingArea ); TheNavMesh->AddToSelectedSet( area ); } } start.y += GenerationStepSize; } start.x += GenerationStepSize; } return true; } //-------------------------------------------------------------------------------------------------------------- static void CommandNavSelectOverlapping( void ) { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; TheNavMesh->ClearSelectedSet(); SelectOverlappingAreas overlapCheck; TheNavMesh->ForAllAreas( overlapCheck ); Msg( "%d overlapping areas selected\n", TheNavMesh->GetSelecteSetSize() ); } static ConCommand nav_select_overlapping( "nav_select_overlapping", CommandNavSelectOverlapping, "Selects nav areas that are overlapping others.", FCVAR_GAMEDLL ); //-------------------------------------------------------------------------------------------------------- static byte m_PVS[PAD_NUMBER( MAX_MAP_CLUSTERS,8 ) / 8]; static int m_nPVSSize; // PVS size in bytes CUtlHash< NavVisPair_t, CVisPairHashFuncs, CVisPairHashFuncs > *g_pNavVisPairHash; #define MASK_NAV_VISION (MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE) //-------------------------------------------------------------------------------------------------------- /** * Set PVS to only include the Potentially Visible Set as seen from anywhere * within this nav area */ void CNavArea::SetupPVS( void ) const { m_nPVSSize = sizeof( m_PVS ); engine->ResetPVS( m_PVS, m_nPVSSize ); const float margin = GenerationStepSize/2.0f; Vector eye( 0, 0, 0.75f * HumanHeight ); // step across area checking visibility to given area Vector shift( eye ); for( shift.y = margin; shift.y <= GetSizeY() - margin; shift.y += GenerationStepSize ) { for( shift.x = margin; shift.x <= GetSizeX() - margin; shift.x += GenerationStepSize ) { // Optimization: // If we are already POTENTIALLY_VISIBLE, and no longer COMPLETELY_VISIBLE, there's // no way for vis to change again. Vector testPos( GetCorner( NORTH_WEST ) + shift ); testPos.z = GetZ( testPos ) + eye.z; engine->AddOriginToPVS( testPos ); } } } //-------------------------------------------------------------------------------------------------------- /** * Return true if this area is within the current PVS */ bool CNavArea::IsInPVS( void ) const { Vector eye( 0, 0, 0.75f * HumanHeight ); Extent areaExtent; areaExtent.lo = GetCenter() + eye; areaExtent.hi = areaExtent.lo; areaExtent.Encompass( GetCorner( NORTH_WEST ) + eye ); areaExtent.Encompass( GetCorner( NORTH_EAST ) + eye ); areaExtent.Encompass( GetCorner( SOUTH_WEST ) + eye ); areaExtent.Encompass( GetCorner( SOUTH_EAST ) + eye ); return engine->CheckBoxInPVS( areaExtent.lo, areaExtent.hi, m_PVS, m_nPVSSize ); } //-------------------------------------------------------------------------------------------------------- /** * Do actual line-of-sight traces to determine if any part of given area is visible from this area */ CNavArea::VisibilityType CNavArea::ComputeVisibility( const CNavArea *area, bool isPVSValid, bool bCheckPVS, bool *pOutsidePVS ) const { float distanceSq = area->GetCenter().DistToSqr( GetCenter() ); if ( nav_max_view_distance.GetFloat() > 0.00001f ) { // limit range of visibility check if ( distanceSq > Sqr( nav_max_view_distance.GetFloat() ) ) { // too far to be visible return NOT_VISIBLE; } } if ( !isPVSValid ) { SetupPVS(); } Vector eye( 0, 0, 0.75f * HumanHeight ); if ( bCheckPVS ) { Extent areaExtent; areaExtent.lo = areaExtent.hi = area->GetCenter() + eye; areaExtent.Encompass( area->GetCorner( NORTH_WEST ) + eye ); areaExtent.Encompass( area->GetCorner( NORTH_EAST ) + eye ); areaExtent.Encompass( area->GetCorner( SOUTH_WEST ) + eye ); areaExtent.Encompass( area->GetCorner( SOUTH_EAST ) + eye ); if ( !engine->CheckBoxInPVS( areaExtent.lo, areaExtent.hi, m_PVS, m_nPVSSize ) ) { if ( pOutsidePVS ) *pOutsidePVS = true; return NOT_VISIBLE; } if ( pOutsidePVS ) *pOutsidePVS = false; } //------------------------------------ Vector vThisNW = GetCorner( NORTH_WEST ) + eye; Vector vThisNE = GetCorner( NORTH_EAST ) + eye; Vector vThisSW = GetCorner( SOUTH_WEST ) + eye; Vector vThisSE = GetCorner( SOUTH_EAST ) + eye; Vector vThisCenter = GetCenter() + eye; Vector vTraceMins( vThisNW ); Vector vTraceMaxs( vThisSE ); vTraceMins.z = MIN( MIN( MIN( vThisNW.z, vThisNE.z ), vThisSE.z ), vThisSW.z ); vTraceMaxs.z = MAX( MAX( MAX( vThisNW.z, vThisNE.z ), vThisSE.z ), vThisSW.z ) + 0.1; vTraceMins -= vThisCenter; vTraceMaxs -= vThisCenter; Vector vOtherMins( area->GetCorner( NORTH_WEST) ); Vector vOtherMaxs( area->GetCorner( SOUTH_EAST) ); Vector vTarget; CalcClosestPointOnAABB( vOtherMins, vOtherMaxs, vThisCenter, vTarget ); vTarget.z = area->GetZ( vTarget ) + eye.z; trace_t tr; CTraceFilterNoNPCsOrPlayer traceFilter( NULL, COLLISION_GROUP_NONE ); UTIL_TraceHull( vThisCenter, vTarget, vTraceMins, vTraceMaxs, MASK_NAV_VISION, &traceFilter, &tr ); if ( tr.fraction == 1.0 || ( tr.endpos.x > vOtherMins.x && tr.endpos.x < vOtherMaxs.x && tr.endpos.y > vOtherMins.y && tr.endpos.y < vOtherMaxs.y ) ) { return COMPLETELY_VISIBLE; // Counter-intuitive: the way this function was written, "COMPLETELY_VISIBLE" actually means "I am completely visible to the other" } //------------------------------------ // check line of sight between areas unsigned char vis = COMPLETELY_VISIBLE; const float margin = GenerationStepSize/2.0f; Vector shift( 0, 0, 0.75f * HumanHeight ); // always check center to catch very small areas if ( area->IsPartiallyVisible( GetCenter() + eye ) ) { vis |= POTENTIALLY_VISIBLE; } else { vis &= ~COMPLETELY_VISIBLE; } Vector eyeToCenter( GetCenter() - area->GetCenter() ); eyeToCenter.NormalizeInPlace(); float angleTolerance = nav_potentially_visible_dot_tolerance.GetFloat(); // if corner-to-eye angles are this close to center-to-eye angles, assume the same result and skip the trace // step across area checking visibility to given area for( shift.y = margin; shift.y <= GetSizeY() - margin; shift.y += GenerationStepSize ) { for( shift.x = margin; shift.x <= GetSizeX() - margin; shift.x += GenerationStepSize ) { // Optimization: // If we are already POTENTIALLY_VISIBLE, and no longer COMPLETELY_VISIBLE, there's // no way for vis to change again. if ( vis == POTENTIALLY_VISIBLE ) return POTENTIALLY_VISIBLE; Vector testPos( GetCorner( NORTH_WEST ) + shift ); testPos.z = GetZ( testPos ) + eye.z; // Optimization - treat long-distance traces that are effectively collinear as the same if ( distanceSq > Sqr( 1000 ) ) { Vector eyeToCorner( testPos - (GetCenter() + eye) ); eyeToCorner.NormalizeInPlace(); if ( eyeToCorner.Dot( eyeToCenter ) >= angleTolerance ) { continue; } } if ( area->IsPartiallyVisible( testPos ) ) { vis |= POTENTIALLY_VISIBLE; } else { vis &= ~COMPLETELY_VISIBLE; } } } return (VisibilityType)vis; } //-------------------------------------------------------------------------------------------------------- /** * Return a list of the delta between our visibility list and the given adjacent area */ const CNavArea::CAreaBindInfoArray &CNavArea::ComputeVisibilityDelta( const CNavArea *other ) const { static CAreaBindInfoArray delta; delta.RemoveAll(); // do not delta from a delta - if 'other' is already inheriting, use its inherited source directly if ( other->m_inheritVisibilityFrom.area != NULL ) { Assert( false && "Visibility inheriting from inherited area" ); delta = m_potentiallyVisibleAreas; return delta; } // add any visible areas in my list that are not in 'others' list into the delta int i, j; for( i=0; im_potentiallyVisibleAreas.Count(); ++j ) { if ( m_potentiallyVisibleAreas[i].area == other->m_potentiallyVisibleAreas[j].area && m_potentiallyVisibleAreas[i].attributes == other->m_potentiallyVisibleAreas[j].attributes ) { // mutually identically visible break; } } if ( j == other->m_potentiallyVisibleAreas.Count() ) { // my vis area not in adjacent area's vis list or has different visibility attributes - add to delta delta.AddToTail( m_potentiallyVisibleAreas[i] ); } } } // add explicit NOT_VISIBLE references to areas in 'others' list that are NOT in mine for( j=0; jm_potentiallyVisibleAreas.Count(); ++j ) { if ( other->m_potentiallyVisibleAreas[j].area ) { for( i=0; im_potentiallyVisibleAreas[j].area ) { // area in both lists - already handled in delta above break; } } if ( i == m_potentiallyVisibleAreas.Count() ) { // 'other' has area in their list that we don't - mark it explicitly NOT_VISIBLE AreaBindInfo info; info.area = other->m_potentiallyVisibleAreas[j].area; info.attributes = NOT_VISIBLE; delta.AddToTail( info ); } } } return delta; } //-------------------------------------------------------------------------------------------------------- void CNavArea::ResetPotentiallyVisibleAreas() { m_potentiallyVisibleAreas.RemoveAll(); } //-------------------------------------------------------------------------------------------------------- /** * Determine visibility between areas. * Compute full list of all areas visible for each area. This list will be compressed into deltas * in the PostCustomAnalysis() step. */ CNavArea *g_pCurVisArea; CTSListWithFreeList< CNavArea::AreaBindInfo > g_ComputedVis; void CNavArea::ComputeVisToArea( CNavArea *&pOtherArea ) { CNavArea *area = assert_cast< CNavArea * >( pOtherArea ); VisibilityType visThisToOther = ( area == g_pCurVisArea ) ? COMPLETELY_VISIBLE : NOT_VISIBLE; VisibilityType visOtherToThis = NOT_VISIBLE; if ( area != g_pCurVisArea ) { bool bOutsidePVS; visOtherToThis = g_pCurVisArea->ComputeVisibility( area, true, true, &bOutsidePVS ); // TODO: Hacky right now. Compute visibility for the "complete" case actually returns how completely visible the area is to the other. Should fix it to be more clear [1/30/2009 tom] if ( !bOutsidePVS && ( visOtherToThis || ( g_pCurVisArea->GetCenter() - area->GetCenter() ).LengthSqr() < Sqr( nav_max_view_distance.GetFloat() ) ) ) { visThisToOther = area->ComputeVisibility( g_pCurVisArea, true, false ); } if ( !visOtherToThis && visThisToOther ) { visOtherToThis = POTENTIALLY_VISIBLE; } if ( !visThisToOther && visOtherToThis ) { visThisToOther = POTENTIALLY_VISIBLE; } } CNavArea::AreaBindInfo info; if ( visThisToOther != NOT_VISIBLE ) { info.area = area; info.attributes = visThisToOther; g_ComputedVis.PushItem( info ); } if ( visOtherToThis != NOT_VISIBLE ) { info.area = g_pCurVisArea; info.attributes = visOtherToThis; area->m_potentiallyVisibleAreas.AddToTail( info ); } } //-------------------------------------------------------------------------------------------------------- /** * Determine visibility from this area to all potentially/completely visible areas in the mesh */ void CNavArea::ComputeVisibilityToMesh( void ) { m_inheritVisibilityFrom.area = NULL; m_isInheritedFrom = false; // collect all possible nav areas that could be visible from this area NavAreaCollector collector; float radius = nav_max_view_distance.GetFloat(); if ( radius == 0.0f ) { radius = DEF_NAV_VIEW_DISTANCE; } collector.m_area.EnsureCapacity( 1000 ); TheNavMesh->ForAllAreasInRadius( collector, GetCenter(), radius ); NavVisPair_t visPair; UtlHashHandle_t hHash; // First eliminate the ones already calculated for ( int i = collector.m_area.Count() - 1; i >= 0; --i ) { visPair.SetPair( this, collector.m_area[i] ); hHash = g_pNavVisPairHash->Find( visPair ); if ( hHash != g_pNavVisPairHash->InvalidHandle() ) { collector.m_area.FastRemove( i ); } } SetupPVS(); g_pCurVisArea = this; ParallelProcess( "CNavArea::ComputeVisibilityToMesh", collector.m_area.Base(), collector.m_area.Count(), &ComputeVisToArea ); m_potentiallyVisibleAreas.EnsureCapacity( g_ComputedVis.Count() ); while ( g_ComputedVis.Count() ) { g_ComputedVis.PopItem( &m_potentiallyVisibleAreas[ m_potentiallyVisibleAreas.AddToTail() ] ); } FOR_EACH_VEC( collector.m_area, it ) { visPair.SetPair( this, (CNavArea *)collector.m_area[it] ); Assert( g_pNavVisPairHash->Find( visPair ) == g_pNavVisPairHash->InvalidHandle() ); g_pNavVisPairHash->Insert( visPair ); } } //-------------------------------------------------------------------------------------------------------- /** * The center and all four corners must ALL be visible */ bool CNavArea::IsEntirelyVisible( const Vector &eye, CBaseEntity *ignore ) const { Vector corner; trace_t result; CTraceFilterNoNPCsOrPlayer traceFilter( ignore, COLLISION_GROUP_NONE ); const float offset = 0.75f * HumanHeight; // check center UTIL_TraceLine( eye, GetCenter() + Vector( 0, 0, offset ), MASK_NAV_VISION, &traceFilter, &result ); if (result.fraction < 1.0f) { return false; } for( int c=0; c= 1.0f) { return true; } Vector eyeToCenter( GetCenter() + Vector( 0, 0, offset ) - eye ); eyeToCenter.NormalizeInPlace(); float angleTolerance = nav_potentially_visible_dot_tolerance.GetFloat(); // if corner-to-eye angles are this close to center-to-eye angles, assume the same result and skip the trace for( int c=0; c= angleTolerance ) { continue; } UTIL_TraceLine( eye, corner + Vector( 0, 0, offset ), MASK_NAV_VISION, &traceFilter, &result ); if (result.fraction >= 1.0f) { return true; } } // nothing is visible return false; } //-------------------------------------------------------------------------------------------------------- bool CNavArea::IsPotentiallyVisible( const CNavArea *viewedArea ) const { VPROF_BUDGET( "CNavArea::IsPotentiallyVisible", "NextBot" ); if ( viewedArea == NULL ) { return false; } // can always see ourselves if ( viewedArea == this ) { return true; } // normal visibility check for ( int i=0; im_potentiallyVisibleAreas; for ( int i=0; im_potentiallyVisibleAreas; for ( int i=0; iGetNumPlayers(); ++i ) { if ( team->GetPlayer(i)->IsAlive() ) { CNavArea *from = (CNavArea *)team->GetPlayer(i)->GetLastKnownArea(); if ( from && from->IsPotentiallyVisible( this ) ) { return true; } } } return false; } //-------------------------------------------------------------------------------------------------------- /** * Return true if given area is completely visible from somewhere in this area by someone on the team (very fast) */ bool CNavArea::IsCompletelyVisibleToTeam( int teamIndex ) const { VPROF_BUDGET( "CNavArea::IsCompletelyVisibleToTeam", "NextBot" ); CTeam *team = GetGlobalTeam( teamIndex ); for( int i = 0; i < team->GetNumPlayers(); ++i ) { if ( team->GetPlayer(i)->IsAlive() ) { CNavArea *from = (CNavArea *)team->GetPlayer(i)->GetLastKnownArea(); if ( from && from->IsCompletelyVisible( this ) ) { return true; } } } return false; } //-------------------------------------------------------------------------------------------------------- Vector CNavArea::GetRandomPoint( void ) const { Extent extent; GetExtent( &extent ); Vector spot; spot.x = RandomFloat( extent.lo.x, extent.hi.x ); spot.y = RandomFloat( extent.lo.y, extent.hi.y ); spot.z = GetZ( spot.x, spot.y ); return spot; }