1
0
mirror of https://github.com/alliedmodders/hl2sdk.git synced 2025-01-04 00:23:25 +08:00
hl2sdk/dlls/nav_pathfind.h

622 lines
16 KiB
C
Raw Normal View History

//========= Copyright <20> 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $NoKeywords: $
//
//=============================================================================//
// nav_pathfind.h
// Path-finding mechanisms using the Navigation Mesh
// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003
#ifndef _NAV_PATHFIND_H_
#define _NAV_PATHFIND_H_
//-------------------------------------------------------------------------------------------------------------------
/**
* Used when building a path to determine the kind of path to build
*/
enum RouteType
{
FASTEST_ROUTE,
SAFEST_ROUTE,
};
//--------------------------------------------------------------------------------------------------------------
/**
* Functor used with NavAreaBuildPath()
*/
class ShortestPathCost
{
public:
float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder )
{
if (fromArea == NULL)
{
// first area in path, no cost
return 0.0f;
}
else
{
// compute distance travelled along path so far
float dist;
if (ladder)
dist = ladder->m_length;
else
dist = (area->GetCenter() - fromArea->GetCenter()).Length();
float cost = dist + fromArea->GetCostSoFar();
// if this is a "crouch" area, add penalty
if (area->GetAttributes() & NAV_MESH_CROUCH)
{
const float crouchPenalty = 20.0f; // 10
cost += crouchPenalty * dist;
}
// if this is a "jump" area, add penalty
if (area->GetAttributes() & NAV_MESH_JUMP)
{
const float jumpPenalty = 5.0f;
cost += jumpPenalty * dist;
}
return cost;
}
}
};
//--------------------------------------------------------------------------------------------------------------
/**
* Find path from startArea to goalArea via an A* search, using supplied cost heuristic.
* If cost functor returns -1 for an area, that area is considered a dead end.
* This doesn't actually build a path, but the path is defined by following parent
* pointers back from goalArea to startArea.
* If 'closestArea' is non-NULL, the closest area to the goal is returned (useful if the path fails).
* If 'goalArea' is NULL, will compute a path as close as possible to 'goalPos'.
* If 'goalPos' is NULL, will use the center of 'goalArea' as the goal position.
* Returns true if a path exists.
*/
template< typename CostFunctor >
bool NavAreaBuildPath( CNavArea *startArea, CNavArea *goalArea, const Vector *goalPos, CostFunctor &costFunc, CNavArea **closestArea = NULL )
{
if (startArea == NULL)
return false;
if (goalArea != NULL && goalArea->IsBlocked())
goalArea = NULL;
if (goalArea == NULL && goalPos == NULL)
return false;
startArea->SetParent( NULL );
// if we are already in the goal area, build trivial path
if (startArea == goalArea)
{
goalArea->SetParent( NULL );
return true;
}
// determine actual goal position
Vector actualGoalPos = (goalPos) ? *goalPos : goalArea->GetCenter();
// start search
CNavArea::ClearSearchLists();
// compute estimate of path length
/// @todo Cost might work as "manhattan distance"
startArea->SetTotalCost( (startArea->GetCenter() - actualGoalPos).Length() );
float initCost = costFunc( startArea, NULL, NULL );
if (initCost < 0.0f)
return false;
startArea->SetCostSoFar( initCost );
startArea->AddToOpenList();
// keep track of the area we visit that is closest to the goal
if (closestArea)
*closestArea = startArea;
float closestAreaDist = startArea->GetTotalCost();
// do A* search
while( !CNavArea::IsOpenListEmpty() )
{
// get next area to check
CNavArea *area = CNavArea::PopOpenList();
// don't consider blocked areas
if ( area->IsBlocked() )
continue;
// check if we have found the goal area or position
if (area == goalArea || (goalArea == NULL && goalPos && area->Contains( *goalPos )))
{
if (closestArea)
{
*closestArea = area;
}
return true;
}
// search adjacent areas
bool searchFloor = true;
int dir = NORTH;
const NavConnectList *floorList = area->GetAdjacentList( NORTH );
int floorIter = floorList->Head();
bool ladderUp = true;
const NavLadderConnectList *ladderList = NULL;
int ladderIter = NavLadderConnectList::InvalidIndex();
enum { AHEAD = 0, LEFT, RIGHT, BEHIND, NUM_TOP_DIRECTIONS };
int ladderTopDir = AHEAD;
while(true)
{
CNavArea *newArea;
NavTraverseType how;
const CNavLadder *ladder = NULL;
//
// Get next adjacent area - either on floor or via ladder
//
if (searchFloor)
{
// if exhausted adjacent connections in current direction, begin checking next direction
if (floorIter == floorList->InvalidIndex())
{
++dir;
if (dir == NUM_DIRECTIONS)
{
// checked all directions on floor - check ladders next
searchFloor = false;
ladderList = area->GetLadderList( CNavLadder::LADDER_UP );
ladderIter = ladderList->Head();
ladderTopDir = AHEAD;
}
else
{
// start next direction
floorList = area->GetAdjacentList( (NavDirType)dir );
floorIter = floorList->Head();
}
continue;
}
newArea = floorList->Element(floorIter).area;
how = (NavTraverseType)dir;
floorIter = floorList->Next( floorIter );
}
else // search ladders
{
if (ladderIter == ladderList->InvalidIndex())
{
if (!ladderUp)
{
// checked both ladder directions - done
break;
}
else
{
// check down ladders
ladderUp = false;
ladderList = area->GetLadderList( CNavLadder::LADDER_DOWN );
ladderIter = ladderList->Head();
}
continue;
}
if (ladderUp)
{
ladder = ladderList->Element( ladderIter ).ladder;
// do not use BEHIND connection, as its very hard to get to when going up a ladder
if (ladderTopDir == AHEAD)
newArea = ladder->m_topForwardArea;
else if (ladderTopDir == LEFT)
newArea = ladder->m_topLeftArea;
else if (ladderTopDir == RIGHT)
newArea = ladder->m_topRightArea;
else
{
ladderIter = ladderList->Next( ladderIter );
ladderTopDir = AHEAD;
continue;
}
how = GO_LADDER_UP;
++ladderTopDir;
}
else
{
newArea = ladderList->Element(ladderIter).ladder->m_bottomArea;
how = GO_LADDER_DOWN;
ladder = ladderList->Element(ladderIter).ladder;
ladderIter = ladderList->Next( ladderIter );
}
if (newArea == NULL)
continue;
}
// don't backtrack
if (newArea == area)
continue;
// don't consider blocked areas
if ( newArea->IsBlocked() )
continue;
float newCostSoFar = costFunc( newArea, area, ladder );
// check if cost functor says this area is a dead-end
if (newCostSoFar < 0.0f)
continue;
if ((newArea->IsOpen() || newArea->IsClosed()) && newArea->GetCostSoFar() <= newCostSoFar)
{
// this is a worse path - skip it
continue;
}
else
{
// compute estimate of distance left to go
float newCostRemaining = (newArea->GetCenter() - actualGoalPos).Length();
// track closest area to goal in case path fails
if (closestArea && newCostRemaining < closestAreaDist)
{
*closestArea = newArea;
closestAreaDist = newCostRemaining;
}
newArea->SetParent( area, how );
newArea->SetCostSoFar( newCostSoFar );
newArea->SetTotalCost( newCostSoFar + newCostRemaining );
if (newArea->IsClosed())
newArea->RemoveFromClosedList();
if (newArea->IsOpen())
{
// area already on open list, update the list order to keep costs sorted
newArea->UpdateOnOpenList();
}
else
{
newArea->AddToOpenList();
}
}
}
// we have searched this area
area->AddToClosedList();
}
return false;
}
//--------------------------------------------------------------------------------------------------------------
/**
* Compute distance between two areas. Return -1 if can't reach 'endArea' from 'startArea'.
*/
template< typename CostFunctor >
float NavAreaTravelDistance( CNavArea *startArea, CNavArea *endArea, CostFunctor &costFunc )
{
if (startArea == NULL)
return -1.0f;
if (endArea == NULL)
return -1.0f;
if (startArea == endArea)
return 0.0f;
// compute path between areas using given cost heuristic
if (NavAreaBuildPath( startArea, endArea, NULL, costFunc ) == false)
return -1.0f;
// compute distance along path
float distance = 0.0f;
for( CNavArea *area = endArea; area->GetParent(); area = area->GetParent() )
{
distance += (area->GetCenter() - area->GetParent()->GetCenter()).Length();
}
return distance;
}
//--------------------------------------------------------------------------------------------------------------
/**
* Compute travel distance along shortest path from startPos to goalPos.
* Return -1 if can't reach endPos from goalPos.
*/
template< typename CostFunctor >
float NavAreaTravelDistance( const Vector &startPos, const Vector &goalPos, CostFunctor &costFunc )
{
CNavArea *startArea = TheNavMesh->GetNearestNavArea( startPos );
if (startArea == NULL)
{
return -1.0f;
}
// compute path between areas using given cost heuristic
CNavArea *goalArea = NULL;
if (NavAreaBuildPath( startArea, NULL, &goalPos, costFunc, &goalArea ) == false)
{
return -1.0f;
}
// compute distance along path
if (goalArea->GetParent() == NULL)
{
// both points are in the same area - return euclidean distance
return (goalPos - startPos).Length();
}
else
{
CNavArea *area;
float distance;
// goalPos is assumed to be inside goalArea (or very close to it) - skip to next area
area = goalArea->GetParent();
distance = (goalPos - area->GetCenter()).Length();
for( ; area->GetParent(); area = area->GetParent() )
{
distance += (area->GetCenter() - area->GetParent()->GetCenter()).Length();
}
// add in distance to startPos
distance += (startPos - area->GetCenter()).Length();
return distance;
}
}
//--------------------------------------------------------------------------------------------------------------
/**
* Do a breadth-first search, invoking functor on each area.
* If functor returns 'true', continue searching from this area.
* If functor returns 'false', the area's adjacent areas are not explored (dead end).
* If 'maxRange' is 0 or less, no range check is done (all areas will be examined).
*
* NOTE: Returns all areas that overlap range, even partially
*
* @todo Use ladder connections
*/
// helper function
inline void AddAreaToOpenList( CNavArea *area, CNavArea *parent, const Vector &startPos, float maxRange )
{
if (area == NULL)
return;
if (!area->IsMarked())
{
area->Mark();
area->SetTotalCost( 0.0f );
area->SetParent( parent );
if (maxRange > 0.0f)
{
// make sure this area overlaps range
Vector closePos;
area->GetClosestPointOnArea( startPos, &closePos );
if ((closePos - startPos).AsVector2D().IsLengthLessThan( maxRange ))
{
// compute approximate distance along path to limit travel range, too
float distAlong = parent->GetCostSoFar();
distAlong += (area->GetCenter() - parent->GetCenter()).Length();
area->SetCostSoFar( distAlong );
// allow for some fudge due to large size areas
if (distAlong <= 1.5f * maxRange)
area->AddToOpenList();
}
}
else
{
// infinite range
area->AddToOpenList();
}
}
}
template < typename Functor >
void SearchSurroundingAreas( CNavArea *startArea, const Vector &startPos, Functor &func, float maxRange = -1.0f )
{
if (startArea == NULL)
return;
CNavArea::MakeNewMarker();
CNavArea::ClearSearchLists();
startArea->AddToOpenList();
startArea->SetTotalCost( 0.0f );
startArea->SetCostSoFar( 0.0f );
startArea->SetParent( NULL );
startArea->Mark();
while( !CNavArea::IsOpenListEmpty() )
{
// get next area to check
CNavArea *area = CNavArea::PopOpenList();
// don't use blocked areas
if ( area->IsBlocked() )
continue;
// invoke functor on area
if (func( area ))
{
// explore adjacent floor areas
for( int dir=0; dir<NUM_DIRECTIONS; ++dir )
{
int count = area->GetAdjacentCount( (NavDirType)dir );
for( int i=0; i<count; ++i )
{
CNavArea *adjArea = area->GetAdjacentArea( (NavDirType)dir, i );
AddAreaToOpenList( adjArea, area, startPos, maxRange );
}
}
// explore adjacent areas connected by ladders
// check up ladders
const NavLadderConnectList *ladderList = area->GetLadderList( CNavLadder::LADDER_UP );
if (ladderList)
{
FOR_EACH_LL( (*ladderList), it )
{
const CNavLadder *ladder = (*ladderList)[ it ].ladder;
// do not use BEHIND connection, as its very hard to get to when going up a ladder
AddAreaToOpenList( ladder->m_topForwardArea, area, startPos, maxRange );
AddAreaToOpenList( ladder->m_topLeftArea, area, startPos, maxRange );
AddAreaToOpenList( ladder->m_topRightArea, area, startPos, maxRange );
}
}
// check down ladders
ladderList = area->GetLadderList( CNavLadder::LADDER_DOWN );
if (ladderList)
{
FOR_EACH_LL( (*ladderList), it )
{
const CNavLadder *ladder = (*ladderList)[ it ].ladder;
AddAreaToOpenList( ladder->m_bottomArea, area, startPos, maxRange );
}
}
}
}
}
//--------------------------------------------------------------------------------------------------------------
/**
* Fuctor that returns lowest cost for farthest away areas
* For use with FindMinimumCostArea()
*/
class FarAwayFunctor
{
public:
float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder )
{
if (area == fromArea)
return 9999999.9f;
return 1.0f/(fromArea->GetCenter() - area->GetCenter()).Length();
}
};
/**
* Fuctor that returns lowest cost for areas farthest from given position
* For use with FindMinimumCostArea()
*/
class FarAwayFromPositionFunctor
{
public:
FarAwayFromPositionFunctor( const Vector &pos ) : m_pos( pos )
{
}
float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder )
{
return 1.0f/(m_pos - area->GetCenter()).Length();
}
private:
const Vector &m_pos;
};
/**
* Pick a low-cost area of "decent" size
*/
template< typename CostFunctor >
CNavArea *FindMinimumCostArea( CNavArea *startArea, CostFunctor &costFunc )
{
const float minSize = 150.0f;
// collect N low-cost areas of a decent size
enum { NUM_CHEAP_AREAS = 32 };
struct
{
CNavArea *area;
float cost;
}
cheapAreaSet[ NUM_CHEAP_AREAS ];
int cheapAreaSetCount = 0;
FOR_EACH_LL( TheNavAreaList, iter )
{
CNavArea *area = TheNavAreaList[iter];
// skip the small areas
const Extent &extent = area->GetExtent();
if (extent.hi.x - extent.lo.x < minSize || extent.hi.y - extent.lo.y < minSize)
continue;
// compute cost of this area
float cost = costFunc( area, startArea, NULL );
if (cheapAreaSetCount < NUM_CHEAP_AREAS)
{
cheapAreaSet[ cheapAreaSetCount ].area = area;
cheapAreaSet[ cheapAreaSetCount++ ].cost = cost;
}
else
{
// replace most expensive cost if this is cheaper
int expensive = 0;
for( int i=1; i<NUM_CHEAP_AREAS; ++i )
if (cheapAreaSet[i].cost > cheapAreaSet[expensive].cost)
expensive = i;
if (cheapAreaSet[expensive].cost > cost)
{
cheapAreaSet[expensive].area = area;
cheapAreaSet[expensive].cost = cost;
}
}
}
if (cheapAreaSetCount)
{
// pick one of the areas at random
return cheapAreaSet[ RandomInt( 0, cheapAreaSetCount-1 ) ].area;
}
else
{
// degenerate case - no decent sized areas - pick a random area
int numAreas = TheNavAreaList.Count();
int which = RandomInt( 0, numAreas-1 );
FOR_EACH_LL( TheNavAreaList, iter )
{
if (which-- == 0)
return TheNavAreaList[iter];
}
}
return cheapAreaSet[ RandomInt( 0, cheapAreaSetCount-1 ) ].area;
}
#endif // _NAV_PATHFIND_H_