// NextBotChasePath.h // Maintain and follow a "chase path" to a selected Actor // Author: Michael Booth, September 2006 // Copyright (c) 2006 Turtle Rock Studios, Inc. - All Rights Reserved #ifndef _NEXT_BOT_CHASE_PATH_ #define _NEXT_BOT_CHASE_PATH_ #include "nav.h" #include "NextBotInterface.h" #include "NextBotLocomotionInterface.h" #include "NextBotChasePath.h" #include "NextBotUtil.h" #include "NextBotPathFollow.h" #include "tier0/vprof.h" //---------------------------------------------------------------------------------------------- /** * A ChasePath extends a PathFollower to periodically recompute a path to a chase * subject, and to move along the path towards that subject. */ class ChasePath : public PathFollower { public: enum SubjectChaseType { LEAD_SUBJECT, DONT_LEAD_SUBJECT }; ChasePath( SubjectChaseType chaseHow = DONT_LEAD_SUBJECT ); virtual ~ChasePath() { } virtual void Update( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos = NULL ); // update path to chase target and move bot along path virtual float GetLeadRadius( void ) const; // range where movement leading begins - beyond this just head right for the subject virtual float GetMaxPathLength( void ) const; // return maximum path length virtual Vector PredictSubjectPosition( INextBot *bot, CBaseEntity *subject ) const; // try to cutoff our chase subject, knowing our relative positions and velocities virtual bool IsRepathNeeded( INextBot *bot, CBaseEntity *subject ) const; // return true if situation has changed enough to warrant recomputing the current path virtual float GetLifetime( void ) const; // Return duration this path is valid. Path will become invalid at its earliest opportunity once this duration elapses. Zero = infinite lifetime virtual void Invalidate( void ); // (EXTEND) cause the path to become invalid private: void RefreshPath( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos ); CountdownTimer m_failTimer; // throttle re-pathing if last path attempt failed CountdownTimer m_throttleTimer; // require a minimum time between re-paths CountdownTimer m_lifetimeTimer; EHANDLE m_lastPathSubject; // the subject used to compute the current/last path SubjectChaseType m_chaseHow; }; inline ChasePath::ChasePath( SubjectChaseType chaseHow ) { m_failTimer.Invalidate(); m_throttleTimer.Invalidate(); m_lifetimeTimer.Invalidate(); m_lastPathSubject = NULL; m_chaseHow = chaseHow; } inline float ChasePath::GetLeadRadius( void ) const { return 500.0f; // 1000.0f; } inline float ChasePath::GetMaxPathLength( void ) const { // no limit return 0.0f; } inline float ChasePath::GetLifetime( void ) const { // infinite duration return 0.0f; } inline void ChasePath::Invalidate( void ) { // path is gone, repath at earliest opportunity m_throttleTimer.Invalidate(); m_lifetimeTimer.Invalidate(); // extend PathFollower::Invalidate(); } //---------------------------------------------------------------------------------------------- /** * Maintain a path to our chase subject and move along that path */ inline void ChasePath::Update( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos ) { VPROF_BUDGET( "ChasePath::Update", "NextBot" ); // maintain the path to the subject RefreshPath( bot, subject, cost, pPredictedSubjectPos ); // move along the path towards the subject PathFollower::Update( bot ); } //---------------------------------------------------------------------------------------------- /** * Return true if situation has changed enough to warrant recomputing the current path */ inline bool ChasePath::IsRepathNeeded( INextBot *bot, CBaseEntity *subject ) const { // the closer we get, the more accurate our path needs to be Vector to = subject->GetAbsOrigin() - bot->GetPosition(); const float minTolerance = 0.0f; // 25.0f; const float toleranceRate = 0.33f; // 1.0f; // 0.15f; float tolerance = minTolerance + toleranceRate * to.Length(); return ( subject->GetAbsOrigin() - GetEndPosition() ).IsLengthGreaterThan( tolerance ); } //---------------------------------------------------------------------------------------------- /** * Periodically rebuild the path to our victim */ inline void ChasePath::RefreshPath( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos ) { VPROF_BUDGET( "ChasePath::RefreshPath", "NextBot" ); ILocomotion *mover = bot->GetLocomotionInterface(); // don't change our path if we're on a ladder if ( IsValid() && mover->IsUsingLadder() ) { if ( bot->IsDebugging( NEXTBOT_PATH ) ) { DevMsg( "%3.2f: bot(#%d) ChasePath::RefreshPath failed. Bot is on a ladder.\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); } // don't allow repath until a moment AFTER we have left the ladder m_throttleTimer.Start( 1.0f ); return; } if ( subject == NULL ) { if ( bot->IsDebugging( NEXTBOT_PATH ) ) { DevMsg( "%3.2f: bot(#%d) CasePath::RefreshPath failed. No subject.\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); } return; } if ( !m_failTimer.IsElapsed() ) { // if ( bot->IsDebugging( NEXTBOT_PATH ) ) // { // DevMsg( "%3.2f: bot(#%d) ChasePath::RefreshPath failed. Fail timer not elapsed.\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); // } return; } // if our path subject changed, repath immediately if ( subject != m_lastPathSubject ) { if ( bot->IsDebugging( NEXTBOT_PATH ) ) { DevMsg( "%3.2f: bot(#%d) Chase path subject changed (from %p to %p).\n", gpGlobals->curtime, bot->GetEntity()->entindex(), m_lastPathSubject.Get(), subject ); } Invalidate(); // new subject, fresh attempt m_failTimer.Invalidate(); } if ( IsValid() && !m_throttleTimer.IsElapsed() ) { // require a minimum time between repaths, as long as we have a path to follow // if ( bot->IsDebugging( NEXTBOT_PATH ) ) // { // DevMsg( "%3.2f: bot(#%d) ChasePath::RefreshPath failed. Rate throttled.\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); // } return; } if ( IsValid() && m_lifetimeTimer.HasStarted() && m_lifetimeTimer.IsElapsed() ) { // this path's lifetime has elapsed Invalidate(); } if ( !IsValid() || IsRepathNeeded( bot, subject ) ) { // the situation has changed - try a new path bool isPath; Vector pathTarget = subject->GetAbsOrigin(); if ( m_chaseHow == LEAD_SUBJECT ) { pathTarget = pPredictedSubjectPos ? *pPredictedSubjectPos : PredictSubjectPosition( bot, subject ); isPath = Compute( bot, pathTarget, cost, GetMaxPathLength() ); } else if ( subject->MyCombatCharacterPointer() && subject->MyCombatCharacterPointer()->GetLastKnownArea() ) { isPath = Compute( bot, subject->MyCombatCharacterPointer(), cost, GetMaxPathLength() ); } else { isPath = Compute( bot, pathTarget, cost, GetMaxPathLength() ); } if ( isPath ) { if ( bot->IsDebugging( NEXTBOT_PATH ) ) { const float size = 20.0f; NDebugOverlay::VertArrow( bot->GetPosition() + Vector( 0, 0, size ), bot->GetPosition(), size, 255, RandomInt( 0, 200 ), 255, 255, true, 30.0f ); DevMsg( "%3.2f: bot(#%d) REPATH\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); } m_lastPathSubject = subject; const float minRepathInterval = 0.5f; m_throttleTimer.Start( minRepathInterval ); // track the lifetime of this new path float lifetime = GetLifetime(); if ( lifetime > 0.0f ) { m_lifetimeTimer.Start( lifetime ); } else { m_lifetimeTimer.Invalidate(); } } else { // can't reach subject - throttle retry based on range to subject m_failTimer.Start( 0.005f * ( bot->GetRangeTo( subject ) ) ); // allow bot to react to path failure bot->OnMoveToFailure( this, FAIL_NO_PATH_EXISTS ); if ( bot->IsDebugging( NEXTBOT_PATH ) ) { const float size = 20.0f; const float dT = 90.0f; int c = RandomInt( 0, 100 ); NDebugOverlay::VertArrow( bot->GetPosition() + Vector( 0, 0, size ), bot->GetPosition(), size, 255, c, c, 255, true, dT ); NDebugOverlay::HorzArrow( bot->GetPosition(), pathTarget, 5.0f, 255, c, c, 255, true, dT ); DevMsg( "%3.2f: bot(#%d) REPATH FAILED\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); } Invalidate(); } } } //---------------------------------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------------------------------- /** * Directly beeline toward victim if we have a clear shot, otherwise pathfind. */ class DirectChasePath : public ChasePath { public: DirectChasePath( ChasePath::SubjectChaseType chaseHow = ChasePath::DONT_LEAD_SUBJECT ) : ChasePath( chaseHow ) { } //------------------------------------------------------------------------------------------------------- virtual void Update( INextBot *me, CBaseEntity *victim, const IPathCost &pathCost, Vector *pPredictedSubjectPos = NULL ) // update path to chase target and move bot along path { Assert( !pPredictedSubjectPos ); bool bComputedPredictedPosition; Vector vecPredictedPosition; if ( !DirectChase( &bComputedPredictedPosition, &vecPredictedPosition, me, victim ) ) { // path around obstacles to reach our victim ChasePath::Update( me, victim, pathCost, bComputedPredictedPosition ? &vecPredictedPosition : NULL ); } NotifyVictim( me, victim ); } //------------------------------------------------------------------------------------------------------- bool DirectChase( bool *pPredictedPositionComputed, Vector *pPredictedPos, INextBot *me, CBaseEntity *victim ) // if there is nothing between us and our victim, run directly at them { *pPredictedPositionComputed = false; ILocomotion *mover = me->GetLocomotionInterface(); if ( me->IsImmobile() || mover->IsScrambling() ) { return false; } if ( IsDiscontinuityAhead( me, CLIMB_UP ) ) { return false; } if ( IsDiscontinuityAhead( me, JUMP_OVER_GAP ) ) { return false; } Vector leadVictimPos = PredictSubjectPosition( me, victim ); // Don't want to have to compute the predicted position twice. *pPredictedPositionComputed = true; *pPredictedPos = leadVictimPos; if ( !mover->IsPotentiallyTraversable( mover->GetFeet(), leadVictimPos ) ) { return false; } // the way is clear - move directly towards our victim mover->FaceTowards( leadVictimPos ); mover->Approach( leadVictimPos ); me->GetBodyInterface()->AimHeadTowards( victim ); // old path is no longer useful since we've moved off of it Invalidate(); return true; } //------------------------------------------------------------------------------------------------------- virtual bool IsRepathNeeded( INextBot *bot, CBaseEntity *subject ) const // return true if situation has changed enough to warrant recomputing the current path { if ( ChasePath::IsRepathNeeded( bot, subject ) ) { return true; } return bot->GetLocomotionInterface()->IsStuck() && bot->GetLocomotionInterface()->GetStuckDuration() > 2.0f; } //------------------------------------------------------------------------------------------------------- /** * Determine exactly where the path goes between the given two areas * on the path. Return this point in 'crossPos'. */ virtual void ComputeAreaCrossing( INextBot *bot, const CNavArea *from, const Vector &fromPos, const CNavArea *to, NavDirType dir, Vector *crossPos ) const { Vector center; float halfWidth; from->ComputePortal( to, dir, ¢er, &halfWidth ); *crossPos = center; } void NotifyVictim( INextBot *me, CBaseEntity *victim ); }; #endif // _NEXT_BOT_CHASE_PATH_