Modified source engine (2017) developed by valve and leaked in 2020. Not for commercial purporses
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

574 lines
15 KiB

5 years ago
// NextBotRetreatPath.h
// Maintain and follow a path that leads safely away from the given Actor
// Author: Michael Booth, February 2007
//========= Copyright Valve Corporation, All rights reserved. ============//
#ifndef _NEXT_BOT_RETREAT_PATH_
#define _NEXT_BOT_RETREAT_PATH_
#include "nav.h"
#include "NextBotInterface.h"
#include "NextBotLocomotionInterface.h"
#include "NextBotRetreatPath.h"
#include "NextBotUtil.h"
#include "NextBotPathFollow.h"
#include "tier0/vprof.h"
//----------------------------------------------------------------------------------------------
/**
* A RetreatPath extends a PathFollower to periodically recompute a path
* away from a threat, and to move along the path away from that threat.
*/
class RetreatPath : public PathFollower
{
public:
RetreatPath( void );
virtual ~RetreatPath() { }
void Update( INextBot *bot, CBaseEntity *threat ); // update path away from threat and move bot along path
virtual float GetMaxPathLength( void ) const; // return maximum path length
virtual void Invalidate( void ); // (EXTEND) cause the path to become invalid
private:
void RefreshPath( INextBot *bot, CBaseEntity *threat );
CountdownTimer m_throttleTimer; // require a minimum time between re-paths
EHANDLE m_pathThreat; // the threat of our existing path
Vector m_pathThreatPos; // where the threat was when the path was built
};
inline RetreatPath::RetreatPath( void )
{
m_throttleTimer.Invalidate();
m_pathThreat = NULL;
}
inline float RetreatPath::GetMaxPathLength( void ) const
{
return 1000.0f;
}
inline void RetreatPath::Invalidate( void )
{
// path is gone, repath at earliest opportunity
m_throttleTimer.Invalidate();
m_pathThreat = NULL;
// extend
PathFollower::Invalidate();
}
//----------------------------------------------------------------------------------------------
/**
* Maintain a path to our chase threat and move along that path
*/
inline void RetreatPath::Update( INextBot *bot, CBaseEntity *threat )
{
VPROF_BUDGET( "RetreatPath::Update", "NextBot" );
if ( threat == NULL )
{
return;
}
// if our path threat changed, repath immediately
if ( threat != m_pathThreat )
{
if ( bot->IsDebugging( INextBot::PATH ) )
{
DevMsg( "%3.2f: bot(#%d) Chase path threat changed (from %X to %X).\n", gpGlobals->curtime, bot->GetEntity()->entindex(), m_pathThreat.Get(), threat );
}
Invalidate();
}
// maintain the path away from the threat
RefreshPath( bot, threat );
// move along the path towards the threat
PathFollower::Update( bot );
}
//--------------------------------------------------------------------------------------------------------------
/**
* Build a path away from retreatFromArea up to retreatRange in length.
*/
class RetreatPathBuilder
{
public:
RetreatPathBuilder( INextBot *me, CBaseEntity *threat, float retreatRange = 500.0f )
{
m_me = me;
m_mover = me->GetLocomotionInterface();
m_threat = threat;
m_retreatRange = retreatRange;
}
CNavArea *ComputePath( void )
{
VPROF_BUDGET( "NavAreaBuildRetreatPath", "NextBot" );
if ( m_mover == NULL )
return NULL;
CNavArea *startArea = m_me->GetEntity()->GetLastKnownArea();
if ( startArea == NULL )
return NULL;
CNavArea *retreatFromArea = TheNavMesh->GetNearestNavArea( m_threat->GetAbsOrigin() );
if ( retreatFromArea == NULL )
return NULL;
startArea->SetParent( NULL );
// start search
CNavArea::ClearSearchLists();
float initCost = Cost( startArea, NULL, NULL );
if ( initCost < 0.0f )
return NULL;
int teamID = m_me->GetEntity()->GetTeamNumber();
startArea->SetTotalCost( initCost );
startArea->AddToOpenList();
// keep track of the area farthest away from the threat
CNavArea *farthestArea = NULL;
float farthestRange = 0.0f;
//
// Dijkstra's algorithm (since we don't know our goal).
// Build a path as far away from the retreat area as possible.
// Minimize total path length and danger.
// Maximize distance to threat of end of path.
//
while( !CNavArea::IsOpenListEmpty() )
{
// get next area to check
CNavArea *area = CNavArea::PopOpenList();
area->AddToClosedList();
// don't consider blocked areas
if ( area->IsBlocked( teamID ) )
continue;
// build adjacent area array
CollectAdjacentAreas( area );
// search adjacent areas
for( int i=0; i<m_adjAreaIndex; ++i )
{
CNavArea *newArea = m_adjAreaVector[ i ].area;
// only visit each area once
if ( newArea->IsClosed() )
continue;
// don't consider blocked areas
if ( newArea->IsBlocked( teamID ) )
continue;
// don't use this area if it is out of range
if ( ( newArea->GetCenter() - m_me->GetEntity()->GetAbsOrigin() ).IsLengthGreaterThan( m_retreatRange ) )
continue;
// determine cost of traversing this area
float newCost = Cost( newArea, area, m_adjAreaVector[ i ].ladder );
// don't use adjacent area if cost functor says it is a dead-end
if ( newCost < 0.0f )
continue;
if ( newArea->IsOpen() && newArea->GetTotalCost() <= newCost )
{
// we have already visited this area, and it has a better path
continue;
}
else
{
// whether this area has been visited or not, we now have a better path
newArea->SetParent( area, m_adjAreaVector[ i ].how );
newArea->SetTotalCost( newCost );
// use 'cost so far' to hold cumulative cost
newArea->SetCostSoFar( newCost );
// tricky bit here - relying on OpenList being sorted by cost
if ( newArea->IsOpen() )
{
// area already on open list, update the list order to keep costs sorted
newArea->UpdateOnOpenList();
}
else
{
newArea->AddToOpenList();
}
// keep track of area farthest from threat
float threatRange = ( newArea->GetCenter() - m_threat->GetAbsOrigin() ).Length();
if ( threatRange > farthestRange )
{
farthestArea = newArea;
farthestRange = threatRange;
}
}
}
}
return farthestArea;
}
/**
* Build a vector of adjacent areas reachable from the given area
*/
void CollectAdjacentAreas( CNavArea *area )
{
m_adjAreaIndex = 0;
const NavConnectVector &adjNorth = *area->GetAdjacentAreas( NORTH );
FOR_EACH_VEC( adjNorth, it )
{
if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
break;
m_adjAreaVector[ m_adjAreaIndex ].area = adjNorth[ it ].area;
m_adjAreaVector[ m_adjAreaIndex ].how = GO_NORTH;
m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
++m_adjAreaIndex;
}
const NavConnectVector &adjSouth = *area->GetAdjacentAreas( SOUTH );
FOR_EACH_VEC( adjSouth, it )
{
if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
break;
m_adjAreaVector[ m_adjAreaIndex ].area = adjSouth[ it ].area;
m_adjAreaVector[ m_adjAreaIndex ].how = GO_SOUTH;
m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
++m_adjAreaIndex;
}
const NavConnectVector &adjWest = *area->GetAdjacentAreas( WEST );
FOR_EACH_VEC( adjWest, it )
{
if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
break;
m_adjAreaVector[ m_adjAreaIndex ].area = adjWest[ it ].area;
m_adjAreaVector[ m_adjAreaIndex ].how = GO_WEST;
m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
++m_adjAreaIndex;
}
const NavConnectVector &adjEast = *area->GetAdjacentAreas( EAST );
FOR_EACH_VEC( adjEast, it )
{
if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
break;
m_adjAreaVector[ m_adjAreaIndex ].area = adjEast[ it ].area;
m_adjAreaVector[ m_adjAreaIndex ].how = GO_EAST;
m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL;
++m_adjAreaIndex;
}
const NavLadderConnectVector &adjUpLadder = *area->GetLadders( CNavLadder::LADDER_UP );
FOR_EACH_VEC( adjUpLadder, it )
{
CNavLadder *ladder = adjUpLadder[ it ].ladder;
if ( ladder->m_topForwardArea && m_adjAreaIndex < MAX_ADJ_AREAS )
{
m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topForwardArea;
m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP;
m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
++m_adjAreaIndex;
}
if ( ladder->m_topLeftArea && m_adjAreaIndex < MAX_ADJ_AREAS )
{
m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topLeftArea;
m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP;
m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
++m_adjAreaIndex;
}
if ( ladder->m_topRightArea && m_adjAreaIndex < MAX_ADJ_AREAS )
{
m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topRightArea;
m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP;
m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
++m_adjAreaIndex;
}
}
const NavLadderConnectVector &adjDownLadder = *area->GetLadders( CNavLadder::LADDER_DOWN );
FOR_EACH_VEC( adjDownLadder, it )
{
CNavLadder *ladder = adjDownLadder[ it ].ladder;
if ( m_adjAreaIndex >= MAX_ADJ_AREAS )
break;
if ( ladder->m_bottomArea )
{
m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_bottomArea;
m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_DOWN;
m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder;
++m_adjAreaIndex;
}
}
}
/**
* Cost minimizes path length traveled thus far and "danger" (proximity to threat(s))
*/
float Cost( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder )
{
// check if we can use this area
if ( !m_mover->IsAreaTraversable( area ) )
{
return -1.0f;
}
int teamID = m_me->GetEntity()->GetTeamNumber();
if ( area->IsBlocked( teamID ) )
{
return -1.0f;
}
const float debugDeltaT = 3.0f;
float cost;
const float maxThreatRange = 500.0f;
const float dangerDensity = 1000.0f;
if ( fromArea == NULL )
{
cost = 0.0f;
if ( area->Contains( m_threat->GetAbsOrigin() ) )
{
// maximum danger - threat is in the area with us
cost += 10.0f * dangerDensity;
if ( m_me->IsDebugging( INextBot::PATH ) )
{
area->DrawFilled( 255, 0, 0, 128 );
}
}
else
{
// danger proportional to range to us
float rangeToThreat = ( m_threat->GetAbsOrigin() - m_me->GetEntity()->GetAbsOrigin() ).Length();
if ( rangeToThreat < maxThreatRange )
{
cost += dangerDensity * ( 1.0f - ( rangeToThreat / maxThreatRange ) );
if ( m_me->IsDebugging( INextBot::PATH ) )
{
NDebugOverlay::Line( m_me->GetEntity()->GetAbsOrigin(), m_threat->GetAbsOrigin(), 255, 0, 0, true, debugDeltaT );
}
}
}
}
else
{
// compute distance traveled along path so far
float dist;
if ( ladder )
{
const float ladderCostFactor = 100.0f;
dist = ladderCostFactor * ladder->m_length;
}
else
{
Vector to = area->GetCenter() - fromArea->GetCenter();
dist = to.Length();
// check for vertical discontinuities
Vector closeFrom, closeTo;
area->GetClosestPointOnArea( fromArea->GetCenter(), &closeTo );
fromArea->GetClosestPointOnArea( area->GetCenter(), &closeFrom );
float deltaZ = closeTo.z - closeFrom.z;
if ( deltaZ > m_mover->GetMaxJumpHeight() )
{
// too high to jump
return -1.0f;
}
else if ( -deltaZ > m_mover->GetDeathDropHeight() )
{
// too far down to drop
return -1.0f;
}
// prefer to maintain our level
const float climbCost = 10.0f;
dist += climbCost * fabs( deltaZ );
}
cost = dist + fromArea->GetTotalCost();
// Add in danger cost due to threat
// Assume straight line between areas and find closest point
// to the threat along that line segment. The distance between
// the threat and closest point on the line is the danger cost.
// path danger is CUMULATIVE
float dangerCost = fromArea->GetCostSoFar();
Vector close;
float t;
CalcClosestPointOnLineSegment( m_threat->GetAbsOrigin(), area->GetCenter(), fromArea->GetCenter(), close, &t );
if ( t < 0.0f )
{
close = area->GetCenter();
}
else if ( t > 1.0f )
{
close = fromArea->GetCenter();
}
float rangeToThreat = ( m_threat->GetAbsOrigin() - close ).Length();
if ( rangeToThreat < maxThreatRange )
{
float dangerFactor = 1.0f - ( rangeToThreat / maxThreatRange );
dangerCost = dangerDensity * dangerFactor;
if ( m_me->IsDebugging( INextBot::PATH ) )
{
NDebugOverlay::HorzArrow( fromArea->GetCenter(), area->GetCenter(), 5, 255 * dangerFactor, 0, 0, 255, true, debugDeltaT );
Vector to = close - m_threat->GetAbsOrigin();
to.NormalizeInPlace();
NDebugOverlay::Line( close, close - 50.0f * to, 255, 0, 0, true, debugDeltaT );
}
}
cost += dangerCost;
}
return cost;
}
private:
INextBot *m_me;
ILocomotion *m_mover;
CBaseEntity *m_threat;
float m_retreatRange;
enum { MAX_ADJ_AREAS = 64 };
struct AdjInfo
{
CNavArea *area;
CNavLadder *ladder;
NavTraverseType how;
};
AdjInfo m_adjAreaVector[ MAX_ADJ_AREAS ];
int m_adjAreaIndex;
};
//----------------------------------------------------------------------------------------------
/**
* Periodically rebuild the path away from our threat
*/
inline void RetreatPath::RefreshPath( INextBot *bot, CBaseEntity *threat )
{
VPROF_BUDGET( "RetreatPath::RefreshPath", "NextBot" );
if ( threat == NULL )
{
if ( bot->IsDebugging( INextBot::PATH ) )
{
DevMsg( "%3.2f: bot(#%d) CasePath::RefreshPath failed. No threat.\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
}
return;
}
// don't change our path if we're on a ladder
ILocomotion *mover = bot->GetLocomotionInterface();
if ( IsValid() && mover && mover->IsUsingLadder() )
{
if ( bot->IsDebugging( INextBot::PATH ) )
{
DevMsg( "%3.2f: bot(#%d) RetreatPath::RefreshPath failed. Bot is on a ladder.\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
}
return;
}
// the closer we get, the more accurate our path needs to be
Vector to = threat->GetAbsOrigin() - bot->GetPosition();
const float minTolerance = 0.0f;
const float toleranceRate = 0.33f;
float tolerance = minTolerance + toleranceRate * to.Length();
if ( !IsValid() || ( threat->GetAbsOrigin() - m_pathThreatPos ).IsLengthGreaterThan( tolerance ) )
{
if ( !m_throttleTimer.IsElapsed() )
{
// require a minimum time between repaths, as long as we have a path to follow
if ( bot->IsDebugging( INextBot::PATH ) )
{
DevMsg( "%3.2f: bot(#%d) RetreatPath::RefreshPath failed. Rate throttled.\n", gpGlobals->curtime, bot->GetEntity()->entindex() );
}
return;
}
// remember our path threat
m_pathThreat = threat;
m_pathThreatPos = threat->GetAbsOrigin();
RetreatPathBuilder retreat( bot, threat, GetMaxPathLength() );
CNavArea *goalArea = retreat.ComputePath();
if ( goalArea )
{
AssemblePrecomputedPath( bot, goalArea->GetCenter(), goalArea );
}
else
{
// all adjacent areas are too far away - just move directly away from threat
Vector to = threat->GetAbsOrigin() - bot->GetPosition();
BuildTrivialPath( bot, bot->GetPosition() - to );
}
const float minRepathInterval = 0.5f;
m_throttleTimer.Start( minRepathInterval );
}
}
#endif // _NEXT_BOT_RETREAT_PATH_