// nav_area.h // Navigation areas // Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 #ifndef _NAV_AREA_H_ #define _NAV_AREA_H_ #include #include "nav.h" #include "steam_util.h" class CNavArea; void DestroyHidingSpots( void ); void StripNavigationAreas( void ); bool SaveNavigationMap( const char *filename ); NavErrorType LoadNavigationMap( void ); void DestroyNavigationMap( void ); //------------------------------------------------------------------------------------------------------------------- /** * Used when building a path to determine the kind of path to build */ enum RouteType { FASTEST_ROUTE, SAFEST_ROUTE, }; //------------------------------------------------------------------------------------------------------------------- /** * The NavConnect union is used to refer to connections to areas */ union NavConnect { unsigned int id; CNavArea *area; bool operator==( const NavConnect &other ) const { return (area == other.area) ? true : false; } }; typedef std::list NavConnectList; //-------------------------------------------------------------------------------------------------------------- enum LadderDirectionType { LADDER_UP = 0, LADDER_DOWN, NUM_LADDER_DIRECTIONS }; /** * The NavLadder class encapsulates traversable ladders, and their connections to NavAreas * @todo Deal with ladders that allow jumping off to areas in the middle */ class CNavLadder { public: CNavLadder( void ) { m_topForwardArea = NULL; m_topRightArea = NULL; m_topLeftArea = NULL; m_topBehindArea = NULL; m_bottomArea = NULL; m_entity = NULL; } Vector m_top; ///< world coords of the top of the ladder Vector m_bottom; ///< world coords of the top of the ladder float m_length; ///< the length of the ladder NavDirType m_dir; ///< which way the ladder faces (ie: surface normal of climbable side) Vector2D m_dirVector; ///< unit vector representation of m_dir CBaseEntity *m_entity; ///< the ladder itself CNavArea *m_topForwardArea; ///< the area at the top of the ladder CNavArea *m_topLeftArea; CNavArea *m_topRightArea; CNavArea *m_topBehindArea; ///< area at top of ladder "behind" it - only useful for descending CNavArea *m_bottomArea; ///< the area at the bottom of the ladder bool m_isDangling; ///< if true, the bottom of the ladder is hanging too high to climb up void OnDestroyNotify( CNavArea *dead ) ///< invoked when given area is going away { if (dead == m_topForwardArea) m_topForwardArea = NULL; if (dead == m_topLeftArea) m_topLeftArea = NULL; if (dead == m_topRightArea) m_topRightArea = NULL; if (dead == m_topBehindArea) m_topBehindArea = NULL; if (dead == m_bottomArea) m_bottomArea = NULL; } }; typedef std::list NavLadderList; extern NavLadderList TheNavLadderList; //-------------------------------------------------------------------------------------------------------------- /** * A HidingSpot is a good place for a bot to crouch and wait for enemies */ class HidingSpot { public: HidingSpot( void ); ///< for use when loading from a file HidingSpot( const Vector *pos, unsigned char flags ); ///< for use when generating - assigns unique ID enum { IN_COVER = 0x01, ///< in a corner with good hard cover nearby GOOD_SNIPER_SPOT = 0x02, ///< had at least one decent sniping corridor IDEAL_SNIPER_SPOT = 0x04 ///< can see either very far, or a large area, or both }; bool HasGoodCover( void ) const { return (m_flags & IN_COVER) ? true : false; } ///< return true if hiding spot in in cover bool IsGoodSniperSpot( void ) const { return (m_flags & GOOD_SNIPER_SPOT) ? true : false; } bool IsIdealSniperSpot( void ) const { return (m_flags & IDEAL_SNIPER_SPOT) ? true : false; } void SetFlags( unsigned char flags ) { m_flags |= flags; } ///< FOR INTERNAL USE ONLY unsigned char GetFlags( void ) const { return m_flags; } void Save( int fd, unsigned int version ) const; void Load( SteamFile *file, unsigned int version ); const Vector *GetPosition( void ) const { return &m_pos; } ///< get the position of the hiding spot unsigned int GetID( void ) const { return m_id; } void Mark( void ) { m_marker = m_masterMarker; } bool IsMarked( void ) const { return (m_marker == m_masterMarker) ? true : false; } static void ChangeMasterMarker( void ) { ++m_masterMarker; } private: friend void DestroyHidingSpots( void ); Vector m_pos; ///< world coordinates of the spot unsigned int m_id; ///< this spot's unique ID unsigned int m_marker; ///< this spot's unique marker unsigned char m_flags; ///< bit flags static unsigned int m_nextID; ///< used when allocating spot ID's static unsigned int m_masterMarker; ///< used to mark spots }; typedef std::list HidingSpotList; extern HidingSpotList TheHidingSpotList; extern HidingSpot *GetHidingSpotByID( unsigned int id ); //-------------------------------------------------------------------------------------------------------------- /** * Stores a pointer to an interesting "spot", and a parametric distance along a path */ struct SpotOrder { float t; ///< parametric distance along ray where this spot first has LOS to our path union { HidingSpot *spot; ///< the spot to look at unsigned int id; ///< spot ID for save/load }; }; typedef std::list SpotOrderList; /** * This struct stores possible path segments thru a CNavArea, and the dangerous spots * to look at as we traverse that path segment. */ struct SpotEncounter { NavConnect from; NavDirType fromDir; NavConnect to; NavDirType toDir; Ray path; ///< the path segment SpotOrderList spotList; ///< list of spots to look at, in order of occurrence }; typedef std::list SpotEncounterList; //------------------------------------------------------------------------------------------------------------------- /** * A CNavArea is a rectangular region defining a walkable area in the map */ class CNavArea { public: CNavArea( CNavNode *nwNode, CNavNode *neNode, CNavNode *seNode, CNavNode *swNode ); CNavArea( void ); CNavArea( const Vector *corner, const Vector *otherCorner ); CNavArea( const Vector *nwCorner, const Vector *neCorner, const Vector *seCorner, const Vector *swCorner ); ~CNavArea(); void ConnectTo( CNavArea *area, NavDirType dir ); ///< connect this area to given area in given direction void Disconnect( CNavArea *area ); ///< disconnect this area from given area void Save( FILE *fp ) const; void Save( int fd, unsigned int version ); void Load( SteamFile *file, unsigned int version ); NavErrorType PostLoad( void ); unsigned int GetID( void ) const { return m_id; } void SetAttributes( unsigned char bits ) { m_attributeFlags = bits; } unsigned char GetAttributes( void ) const { return m_attributeFlags; } void SetPlace( Place place ) { m_place = place; } ///< set place descriptor Place GetPlace( void ) const { return m_place; } ///< get place descriptor bool IsOverlapping( const Vector *pos ) const; ///< return true if 'pos' is within 2D extents of area. bool IsOverlapping( const CNavArea *area ) const; ///< return true if 'area' overlaps our 2D extents bool IsOverlappingX( const CNavArea *area ) const; ///< return true if 'area' overlaps our X extent bool IsOverlappingY( const CNavArea *area ) const; ///< return true if 'area' overlaps our Y extent int GetPlayerCount( int teamID = 0, CBasePlayer *ignore = NULL ) const; ///< return number of players with given teamID in this area (teamID == 0 means any/all) float GetZ( const Vector *pos ) const; ///< return Z of area at (x,y) of 'pos' float GetZ( float x, float y ) const; ///< return Z of area at (x,y) of 'pos' bool Contains( const Vector *pos ) const; ///< return true if given point is on or above this area, but no others bool IsCoplanar( const CNavArea *area ) const; ///< return true if this area and given area are approximately co-planar void GetClosestPointOnArea( const Vector *pos, Vector *close ) const; ///< return closest point to 'pos' on this area - returned point in 'close' float GetDistanceSquaredToPoint( const Vector *pos ) const; ///< return shortest distance between point and this area bool IsDegenerate( void ) const; ///< return true if this area is badly formed bool IsEdge( NavDirType dir ) const; ///< return true if there are no bi-directional links on the given side int GetAdjacentCount( NavDirType dir ) const { return m_connect[ dir ].size(); } ///< return number of connected areas in given direction CNavArea *GetAdjacentArea( NavDirType dir, int i ) const; /// return the i'th adjacent area in the given direction CNavArea *GetRandomAdjacentArea( NavDirType dir ) const; const NavConnectList *GetAdjacentList( NavDirType dir ) const { return &m_connect[dir]; } bool IsConnected( const CNavArea *area, NavDirType dir ) const; ///< return true if given area is connected in given direction float ComputeHeightChange( const CNavArea *area ); ///< compute change in height from this area to given area const NavLadderList *GetLadderList( LadderDirectionType dir ) const { return &m_ladder[dir]; } void ComputePortal( const CNavArea *to, NavDirType dir, Vector *center, float *halfWidth ) const; ///< compute portal to adjacent area void ComputeClosestPointInPortal( const CNavArea *to, NavDirType dir, const Vector *fromPos, Vector *closePos ) const; ///< compute closest point within the "portal" between to adjacent areas NavDirType ComputeDirection( Vector *point ) const; ///< return direction from this area to the given point //- for hunting algorithm --------------------------------------------------------------------------- void SetClearedTimestamp( int teamID ) { m_clearedTimestamp[ teamID ] = gpGlobals->time; } ///< set this area's "clear" timestamp to now float GetClearedTimestamp( int teamID ) { return m_clearedTimestamp[ teamID ]; } ///< get time this area was marked "clear" //- hiding spots ------------------------------------------------------------------------------------ const HidingSpotList *GetHidingSpotList( void ) const { return &m_hidingSpotList; } void ComputeHidingSpots( void ); ///< analyze local area neighborhood to find "hiding spots" in this area - for map learning void ComputeSniperSpots( void ); ///< analyze local area neighborhood to find "sniper spots" in this area - for map learning SpotEncounter *GetSpotEncounter( const CNavArea *from, const CNavArea *to ); ///< given the areas we are moving between, return the spots we will encounter void ComputeSpotEncounters( void ); ///< compute spot encounter data - for map learning //- "danger" ---------------------------------------------------------------------------------------- void IncreaseDanger( int teamID, float amount ); ///< increase the danger of this area for the given team float GetDanger( int teamID ); ///< return the danger of this area (decays over time) float GetSizeX( void ) const { return m_extent.hi.x - m_extent.lo.x; } float GetSizeY( void ) const { return m_extent.hi.y - m_extent.lo.y; } const Extent *GetExtent( void ) const { return &m_extent; } const Vector *GetCenter( void ) const { return &m_center; } const Vector *GetCorner( NavCornerType corner ) const; //- approach areas ---------------------------------------------------------------------------------- struct ApproachInfo { NavConnect here; ///< the approach area NavConnect prev; ///< the area just before the approach area on the path NavTraverseType prevToHereHow; NavConnect next; ///< the area just after the approach area on the path NavTraverseType hereToNextHow; }; const ApproachInfo *GetApproachInfo( int i ) const { return &m_approach[i]; } int GetApproachInfoCount( void ) const { return m_approachCount; } void ComputeApproachAreas( void ); ///< determine the set of "approach areas" - for map learning //- A* pathfinding algorithm ------------------------------------------------------------------------ static void MakeNewMarker( void ) { ++m_masterMarker; if (m_masterMarker == 0) m_masterMarker = 1; } void Mark( void ) { m_marker = m_masterMarker; } BOOL IsMarked( void ) const { return (m_marker == m_masterMarker) ? true : false; } void SetParent( CNavArea *parent, NavTraverseType how = NUM_TRAVERSE_TYPES ) { m_parent = parent; m_parentHow = how; } CNavArea *GetParent( void ) const { return m_parent; } NavTraverseType GetParentHow( void ) const { return m_parentHow; } bool IsOpen( void ) const; ///< true if on "open list" void AddToOpenList( void ); ///< add to open list in decreasing value order void UpdateOnOpenList( void ); ///< a smaller value has been found, update this area on the open list void RemoveFromOpenList( void ); static bool IsOpenListEmpty( void ); static CNavArea *PopOpenList( void ); ///< remove and return the first element of the open list bool IsClosed( void ) const; ///< true if on "closed list" void AddToClosedList( void ); ///< add to the closed list void RemoveFromClosedList( void ); static void ClearSearchLists( void ); ///< clears the open and closed lists for a new search void SetTotalCost( float value ) { m_totalCost = value; } float GetTotalCost( void ) const { return m_totalCost; } void SetCostSoFar( float value ) { m_costSoFar = value; } float GetCostSoFar( void ) const { return m_costSoFar; } //- editing ----------------------------------------------------------------------------------------- void Draw( byte red, byte green, byte blue, int duration = 50 ); ///< draw area for debugging & editing void DrawConnectedAreas( void ); void DrawMarkedCorner( NavCornerType corner, byte red, byte green, byte blue, int duration = 50 ); bool SplitEdit( bool splitAlongX, float splitEdge, CNavArea **outAlpha = NULL, CNavArea **outBeta = NULL ); ///< split this area into two areas at the given edge bool MergeEdit( CNavArea *adj ); ///< merge this area and given adjacent area bool SpliceEdit( CNavArea *other ); ///< create a new area between this area and given area void RaiseCorner( NavCornerType corner, int amount ); ///< raise/lower a corner (or all corners if corner == NUM_CORNERS) //- ladders ----------------------------------------------------------------------------------------- void AddLadderUp( CNavLadder *ladder ) { m_ladder[ LADDER_UP ].push_back( ladder ); } void AddLadderDown( CNavLadder *ladder ) { m_ladder[ LADDER_DOWN ].push_back( ladder ); } private: friend void ConnectGeneratedAreas( void ); friend void MergeGeneratedAreas( void ); friend void MarkJumpAreas( void ); friend bool SaveNavigationMap( const char *filename ); friend NavErrorType LoadNavigationMap( void ); friend void DestroyNavigationMap( void ); friend void DestroyHidingSpots( void ); friend void StripNavigationAreas( void ); friend class CNavAreaGrid; friend class CCSBotManager; void Initialize( void ); ///< to keep constructors consistent static bool m_isReset; ///< if true, don't bother cleaning up in destructor since everything is going away static unsigned int m_nextID; ///< used to allocate unique IDs unsigned int m_id; ///< unique area ID Extent m_extent; ///< extents of area in world coords (NOTE: lo.z is not necessarily the minimum Z, but corresponds to Z at point (lo.x, lo.y), etc Vector m_center; ///< centroid of area unsigned char m_attributeFlags; ///< set of attribute bit flags (see NavAttributeType) Place m_place; ///< place descriptor /// height of the implicit corners float m_neZ; float m_swZ; enum { MAX_AREA_TEAMS = 2 }; //- for hunting ------------------------------------------------------------------------------------- float m_clearedTimestamp[ MAX_AREA_TEAMS ]; ///< time this area was last "cleared" of enemies //- "danger" ---------------------------------------------------------------------------------------- float m_danger[ MAX_AREA_TEAMS ]; ///< danger of this area, allowing bots to avoid areas where they died in the past - zero is no danger float m_dangerTimestamp[ MAX_AREA_TEAMS ]; ///< time when danger value was set - used for decaying void DecayDanger( void ); //- hiding spots ------------------------------------------------------------------------------------ HidingSpotList m_hidingSpotList; bool IsHidingSpotCollision( const Vector *pos ) const; ///< returns true if an existing hiding spot is too close to given position //- encounter spots --------------------------------------------------------------------------------- SpotEncounterList m_spotEncounterList; ///< list of possible ways to move thru this area, and the spots to look at as we do void AddSpotEncounters( const CNavArea *from, NavDirType fromDir, const CNavArea *to, NavDirType toDir ); ///< add spot encounter data when moving from area to area //- approach areas ---------------------------------------------------------------------------------- enum { MAX_APPROACH_AREAS = 16 }; ApproachInfo m_approach[ MAX_APPROACH_AREAS ]; unsigned char m_approachCount; void Strip( void ); ///< remove "analyzed" data from nav area //- A* pathfinding algorithm ------------------------------------------------------------------------ static unsigned int m_masterMarker; unsigned int m_marker; ///< used to flag the area as visited CNavArea *m_parent; ///< the area just prior to this on in the search path NavTraverseType m_parentHow; ///< how we get from parent to us float m_totalCost; ///< the distance so far plus an estimate of the distance left float m_costSoFar; ///< distance travelled so far static CNavArea *m_openList; CNavArea *m_nextOpen, *m_prevOpen; ///< only valid if m_openMarker == m_masterMarker unsigned int m_openMarker; ///< if this equals the current marker value, we are on the open list //- connections to adjacent areas ------------------------------------------------------------------- NavConnectList m_connect[ NUM_DIRECTIONS ]; ///< a list of adjacent areas for each direction NavLadderList m_ladder[ NUM_LADDER_DIRECTIONS ]; ///< list of ladders leading up and down from this area //--------------------------------------------------------------------------------------------------- CNavNode *m_node[ NUM_CORNERS ]; ///< nav nodes at each corner of the area void FinishMerge( CNavArea *adjArea ); ///< recompute internal data once nodes have been adjusted during merge void MergeAdjacentConnections( CNavArea *adjArea ); ///< for merging with "adjArea" - pick up all of "adjArea"s connections void AssignNodes( CNavArea *area ); ///< assign internal nodes to the given area void FinishSplitEdit( CNavArea *newArea, NavDirType ignoreEdge ); ///< given the portion of the original area, update its internal data std::list m_overlapList; ///< list of areas that overlap this area void OnDestroyNotify( CNavArea *dead ); ///< invoked when given area is going away CNavArea *m_prevHash, *m_nextHash; ///< for hash table in CNavAreaGrid }; typedef std::list NavAreaList; extern NavAreaList TheNavAreaList; // // Inlines // inline bool CNavArea::IsDegenerate( void ) const { return (m_extent.lo.x >= m_extent.hi.x || m_extent.lo.y >= m_extent.hi.y); } inline CNavArea *CNavArea::GetAdjacentArea( NavDirType dir, int i ) const { NavConnectList::const_iterator iter; for( iter = m_connect[dir].begin(); iter != m_connect[dir].end(); ++iter ) { if (i == 0) return (*iter).area; --i; } return NULL; } inline bool CNavArea::IsOpen( void ) const { return (m_openMarker == m_masterMarker) ? true : false; } inline bool CNavArea::IsOpenListEmpty( void ) { return (m_openList) ? false : true; } inline CNavArea *CNavArea::PopOpenList( void ) { if (m_openList) { CNavArea *area = m_openList; // disconnect from list area->RemoveFromOpenList(); return area; } return NULL; } inline bool CNavArea::IsClosed( void ) const { if (IsMarked() && !IsOpen()) return true; return false; } inline void CNavArea::AddToClosedList( void ) { Mark(); } inline void CNavArea::RemoveFromClosedList( void ) { // since "closed" is defined as visited (marked) and not on open list, do nothing } //-------------------------------------------------------------------------------------------------------------- /** * The CNavAreaGrid is used to efficiently access navigation areas by world position. * Each cell of the grid contains a list of areas that overlap it. * Given a world position, the corresponding grid cell is ( x/cellsize, y/cellsize ). */ class CNavAreaGrid { public: CNavAreaGrid( void ); ~CNavAreaGrid(); void Reset( void ); ///< clear the grid to empty void Initialize( float minX, float maxX, float minY, float maxY ); ///< clear and reset the grid to the given extents void AddNavArea( CNavArea *area ); ///< add an area to the grid void RemoveNavArea( CNavArea *area ); ///< remove an area from the grid unsigned int GetNavAreaCount( void ) const { return m_areaCount; } ///< return total number of nav areas CNavArea *GetNavArea( const Vector *pos, float beneathLimt = 120.0f ) const; ///< given a position, return the nav area that IsOverlapping and is *immediately* beneath it CNavArea *GetNavAreaByID( unsigned int id ) const; CNavArea *GetNearestNavArea( const Vector *pos, bool anyZ = false ) const; Place GetPlace( const Vector *pos ) const; ///< return radio chatter place for given coordinate private: const float m_cellSize; NavAreaList *m_grid; int m_gridSizeX; int m_gridSizeY; float m_minX; float m_minY; unsigned int m_areaCount; ///< total number of nav areas enum { HASH_TABLE_SIZE = 256 }; CNavArea *m_hashTable[ HASH_TABLE_SIZE ]; ///< hash table to optimize lookup by ID inline int ComputeHashKey( unsigned int id ) const ///< returns a hash key for the given nav area ID { return id & 0xFF; } inline int WorldToGridX( float wx ) const { int x = (wx - m_minX) / m_cellSize; if (x < 0) x = 0; else if (x >= m_gridSizeX) x = m_gridSizeX-1; return x; } inline int WorldToGridY( float wy ) const { int y = (wy - m_minY) / m_cellSize; if (y < 0) y = 0; else if (y >= m_gridSizeY) y = m_gridSizeY-1; return y; } }; extern CNavAreaGrid TheNavAreaGrid; //-------------------------------------------------------------------------------------------------------------- // // Function prototypes // extern NavErrorType LoadNavigationMap( void ); extern void GenerateNavigationAreaMesh( void ); extern void SanityCheckNavigationMap( const char *mapName ); ///< Performs a lightweight sanity-check of the specified map's nav mesh extern void ApproachAreaAnalysisPrep( void ); extern void CleanupApproachAreaAnalysisPrep( void ); extern void BuildLadders( void ); extern bool TestArea( CNavNode *node, int width, int height ); extern int BuildArea( CNavNode *node, int width, int height ); extern bool GetGroundHeight( const Vector *pos, float *height, Vector *normal = NULL ); extern bool GetSimpleGroundHeight( const Vector *pos, float *height, Vector *normal = NULL ); class CBasePlayer; class CBaseEntity; extern bool IsSpotOccupied( CBaseEntity *me, const Vector *pos ); // if a player is at the given spot, return true extern const Vector *FindNearbyHidingSpot( CBaseEntity *me, const Vector *pos, CNavArea *currentArea, float maxRange = 1000.0f, bool isSniper = false, bool useNearest = false ); extern const Vector *FindRandomHidingSpot( CBaseEntity *me, Place place, bool isSniper = false ); #define NO_CROUCH_SPOTS false extern const Vector *FindNearbyRetreatSpot( CBaseEntity *me, const Vector *start, CNavArea *startArea, float maxRange = 1000.0f, int avoidTeam = 0, bool useCrouchAreas = true ); /// return true if moving from "start" to "finish" will cross a player's line of fire. extern bool IsCrossingLineOfFire( const Vector &start, const Vector &finish, CBaseEntity *ignore = NULL, int ignoreTeam = 0 ); extern void IncreaseDangerNearby( int teamID, float amount, CNavArea *area, const Vector *pos, float maxRadius ); extern void DrawDanger( void ); enum NavEditCmdType { EDIT_NONE, EDIT_DELETE, ///< delete current area EDIT_SPLIT, ///< split current area EDIT_MERGE, ///< merge adjacent areas EDIT_JOIN, ///< define connection between areas EDIT_BREAK, ///< break connection between areas EDIT_MARK, ///< mark an area for further operations EDIT_ATTRIB_CROUCH, ///< toggle crouch attribute on current area EDIT_ATTRIB_JUMP, ///< toggle jump attribute on current area EDIT_ATTRIB_PRECISE, ///< toggle precise attribute on current area EDIT_ATTRIB_NO_JUMP, ///< toggle inhibiting discontinuity jumping in current area EDIT_BEGIN_AREA, ///< begin creating a new nav area EDIT_END_AREA, ///< end creation of the new nav area EDIT_CONNECT, ///< connect marked area to selected area EDIT_DISCONNECT, ///< disconnect marked area from selected area EDIT_SPLICE, ///< create new area in between marked and selected areas EDIT_TOGGLE_PLACE_MODE, ///< switch between normal and place editing EDIT_TOGGLE_PLACE_PAINTING, ///< switch between "painting" places onto areas EDIT_PLACE_FLOODFILL, ///< floodfill areas out from current area EDIT_PLACE_PICK, ///< "pick up" the place at the current area EDIT_MARK_UNNAMED, ///< mark an unnamed area for further operations EDIT_WARP_TO_MARK, ///< warp a spectating local player to the selected mark EDIT_SELECT_CORNER, ///< select a corner on the current area EDIT_RAISE_CORNER, ///< raise a corner on the current area EDIT_LOWER_CORNER, ///< lower a corner on the current area }; extern void EditNavAreasReset( void ); extern void EditNavAreas( NavEditCmdType cmd ); extern CNavArea *GetMarkedArea( void ); //-------------------------------------------------------------------------------------------------------------- // // Templates // //-------------------------------------------------------------------------------------------------------------- /** * 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_CROUCH) { const float crouchPenalty = 20.0f; // 10 cost += crouchPenalty * dist; } // if this is a "jump" area, add penalty if (area->GetAttributes() & NAV_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 (closestArea) *closestArea = NULL; if (startArea == NULL) return false; // // If goalArea is NULL, this function will return the closest area to the goal. // However, if there is also no goal, we can't do anything. // 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 ); if (closestArea) *closestArea = goalArea; 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(); // check if we have found the goal area if (area == goalArea) { if (closestArea) *closestArea = goalArea; return true; } // search adjacent areas bool searchFloor = true; int dir = NORTH; const NavConnectList *floorList = area->GetAdjacentList( NORTH ); NavConnectList::const_iterator floorIter = floorList->begin(); bool ladderUp = true; const NavLadderList *ladderList = NULL; NavLadderList::const_iterator ladderIter; enum { AHEAD = 0, LEFT, RIGHT, BEHIND, NUM_TOP_DIRECTIONS }; int ladderTopDir; 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->end()) { ++dir; if (dir == NUM_DIRECTIONS) { // checked all directions on floor - check ladders next searchFloor = false; ladderList = area->GetLadderList( LADDER_UP ); ladderIter = ladderList->begin(); ladderTopDir = AHEAD; } else { // start next direction floorList = area->GetAdjacentList( (NavDirType)dir ); floorIter = floorList->begin(); } continue; } newArea = (*floorIter).area; how = (NavTraverseType)dir; ++floorIter; } else // search ladders { if (ladderIter == ladderList->end()) { if (!ladderUp) { // checked both ladder directions - done break; } else { // check down ladders ladderUp = false; ladderList = area->GetLadderList( LADDER_DOWN ); ladderIter = ladderList->begin(); } continue; } if (ladderUp) { ladder = *ladderIter; // cannot use this ladder if the ladder bottom is hanging above our head if (ladder->m_isDangling) { ++ladderIter; continue; } // 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; continue; } how = GO_LADDER_UP; ++ladderTopDir; } else { newArea = (*ladderIter)->m_bottomArea; how = GO_LADDER_DOWN; ladder = (*ladderIter); ++ladderIter; } if (newArea == NULL) continue; } // don't backtrack if (newArea == area) 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 distance from area to position. Return -1 if can't reach position. */ template< typename CostFunctor > float NavAreaTravelDistance( const Vector *startPos, CNavArea *startArea, const Vector *goalPos, CostFunctor &costFunc ) { if (startArea == NULL || startPos == NULL || goalPos == NULL) return -1.0f; // compute path between areas using given cost heuristic CNavArea *goalArea = NULL; if (NavAreaBuildPath( startArea, TheNavAreaGrid.GetNearestNavArea( goalPos ), goalPos, costFunc, &goalArea ) == false) return -1.0f; if (goalArea == NULL) return -1.0f; // compute distance along path if (goalArea->GetParent() == NULL) { return (*goalPos - *startPos).Length(); } else { CNavArea *area = goalArea->GetParent(); float distance = (*goalPos - *area->GetCenter()).Length(); for( ; area->GetParent(); area = area->GetParent() ) { distance += (*area->GetCenter() - *area->GetParent()->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).Make2D().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 || startPos == 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(); // invoke functor on area if (func( area )) { // explore adjacent floor areas for( int dir=0; dirGetAdjacentCount( (NavDirType)dir ); for( int i=0; iGetAdjacentArea( (NavDirType)dir, i ); AddAreaToOpenList( adjArea, area, startPos, maxRange ); } } // explore adjacent areas connected by ladders NavLadderList::const_iterator ladderIt; // check up ladders const NavLadderList *ladderList = area->GetLadderList( LADDER_UP ); if (ladderList) { for( ladderIt = ladderList->begin(); ladderIt != ladderList->end(); ++ladderIt ) { const CNavLadder *ladder = *ladderIt; // cannot use this ladder if the ladder bottom is hanging above our head if (ladder->m_isDangling) { continue; } // 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( LADDER_DOWN ); if (ladderList) { for( ladderIt = ladderList->begin(); ladderIt != ladderList->end(); ++ladderIt ) { const CNavLadder *ladder = *ladderIt; AddAreaToOpenList( ladder->m_bottomArea, area, startPos, maxRange ); } } } } } //-------------------------------------------------------------------------------------------------------------- /** * Apply the functor to all navigation areas */ template < typename Functor > void ForAllAreas( Functor &func ) { NavAreaList::iterator iter; for( iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; func( area ); } } //-------------------------------------------------------------------------------------------------------------- /** * 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; NavAreaList::iterator iter; for( iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *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 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[ RANDOM_LONG( 0, cheapAreaSetCount-1 ) ].area; } else { // degenerate case - no decent sized areas - pick a random area int numAreas = TheNavAreaList.size(); int which = RANDOM_LONG( 0, numAreas-1 ); NavAreaList::iterator iter; for( iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) if (which-- == 0) break; return *iter; } } #endif // _NAV_AREA_H_