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.
520 lines
14 KiB
520 lines
14 KiB
// NextBotLocomotionInterface.cpp |
|
// Common functionality for all NextBot locomotors |
|
// Author: Michael Booth, April 2005 |
|
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
|
|
#include "cbase.h" |
|
|
|
#include "BasePropDoor.h" |
|
|
|
#include "nav_area.h" |
|
#include "NextBot.h" |
|
#include "NextBotUtil.h" |
|
#include "NextBotLocomotionInterface.h" |
|
#include "NextBotBodyInterface.h" |
|
|
|
#include "tier0/vprof.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
// how far a bot must move to not be considered "stuck" |
|
#define STUCK_RADIUS 100.0f |
|
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Reset to initial state |
|
*/ |
|
ILocomotion::ILocomotion( INextBot *bot ) : INextBotComponent( bot ) |
|
{ |
|
Reset(); |
|
} |
|
|
|
ILocomotion::~ILocomotion() |
|
{ |
|
} |
|
|
|
void ILocomotion::Reset( void ) |
|
{ |
|
INextBotComponent::Reset(); |
|
|
|
m_motionVector = Vector( 1.0f, 0.0f, 0.0f ); |
|
m_speed = 0.0f; |
|
m_groundMotionVector = m_motionVector; |
|
m_groundSpeed = m_speed; |
|
|
|
m_moveRequestTimer.Invalidate(); |
|
|
|
m_isStuck = false; |
|
m_stuckTimer.Invalidate(); |
|
m_stuckPos = vec3_origin; |
|
} |
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Update internal state |
|
*/ |
|
void ILocomotion::Update( void ) |
|
{ |
|
StuckMonitor(); |
|
|
|
// maintain motion vector and speed values |
|
const Vector &vel = GetVelocity(); |
|
m_speed = vel.Length(); |
|
m_groundSpeed = vel.AsVector2D().Length(); |
|
|
|
const float velocityThreshold = 10.0f; |
|
if ( m_speed > velocityThreshold ) |
|
{ |
|
m_motionVector = vel / m_speed; |
|
} |
|
|
|
if ( m_groundSpeed > velocityThreshold ) |
|
{ |
|
m_groundMotionVector.x = vel.x / m_groundSpeed; |
|
m_groundMotionVector.y = vel.y / m_groundSpeed; |
|
m_groundMotionVector.z = 0.0f; |
|
} |
|
|
|
if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) |
|
{ |
|
// show motion vector |
|
NDebugOverlay::HorzArrow( GetFeet(), GetFeet() + 25.0f * m_groundMotionVector, 3.0f, 100, 255, 0, 255, true, 0.1f ); |
|
NDebugOverlay::HorzArrow( GetFeet(), GetFeet() + 25.0f * m_motionVector, 5.0f, 255, 255, 0, 255, true, 0.1f ); |
|
} |
|
} |
|
|
|
|
|
//---------------------------------------------------------------------------- |
|
void ILocomotion::AdjustPosture( const Vector &moveGoal ) |
|
{ |
|
// This function has no effect if we're not standing or crouching |
|
IBody *body = GetBot()->GetBodyInterface(); |
|
if ( !body->IsActualPosture( IBody::STAND ) && !body->IsActualPosture( IBody::CROUCH ) ) |
|
return; |
|
|
|
// |
|
// Stand or crouch as needed |
|
// |
|
|
|
// get bounding limits, ignoring step-upable height |
|
const Vector &mins = body->GetHullMins() + Vector( 0, 0, GetStepHeight() ); |
|
|
|
const float halfSize = body->GetHullWidth()/2.0f; |
|
Vector standMaxs( halfSize, halfSize, body->GetStandHullHeight() ); |
|
|
|
trace_t trace; |
|
NextBotTraversableTraceFilter filter( GetBot(), ILocomotion::IMMEDIATELY ); |
|
|
|
// snap forward movement vector along floor |
|
const Vector &groundNormal = GetGroundNormal(); |
|
const Vector &feet = GetFeet(); |
|
Vector moveDir = moveGoal - feet; |
|
float moveLength = moveDir.NormalizeInPlace(); |
|
Vector left( -moveDir.y, moveDir.x, 0.0f ); |
|
Vector goal = feet + moveLength * CrossProduct( left, groundNormal ).Normalized(); |
|
|
|
TraceHull( feet, goal, mins, standMaxs, body->GetSolidMask(), &filter, &trace ); |
|
|
|
if ( trace.fraction >= 1.0f && !trace.startsolid ) |
|
{ |
|
// no collision while standing |
|
if ( body->IsActualPosture( IBody::CROUCH ) ) |
|
{ |
|
body->SetDesiredPosture( IBody::STAND ); |
|
} |
|
return; |
|
} |
|
|
|
if ( body->IsActualPosture( IBody::CROUCH ) ) |
|
return; |
|
|
|
// crouch hull check |
|
Vector crouchMaxs( halfSize, halfSize, body->GetCrouchHullHeight() ); |
|
|
|
TraceHull( feet, goal, mins, crouchMaxs, body->GetSolidMask(), &filter, &trace ); |
|
|
|
if ( trace.fraction >= 1.0f && !trace.startsolid ) |
|
{ |
|
// no collision while crouching |
|
body->SetDesiredPosture( IBody::CROUCH ); |
|
} |
|
} |
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Move directly towards the given position |
|
*/ |
|
void ILocomotion::Approach( const Vector &goalPos, float goalWeight ) |
|
{ |
|
// there is a desire to move |
|
m_moveRequestTimer.Start(); |
|
} |
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Move the bot to the precise given position immediately |
|
*/ |
|
void ILocomotion::DriveTo( const Vector &pos ) |
|
{ |
|
// there is a desire to move |
|
m_moveRequestTimer.Start(); |
|
} |
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if this locomotor could potentially move along the line given. |
|
* If false is returned, fraction of walkable ray is returned in 'fraction' |
|
*/ |
|
bool ILocomotion::IsPotentiallyTraversable( const Vector &from, const Vector &to, TraverseWhenType when, float *fraction ) const |
|
{ |
|
VPROF_BUDGET( "Locomotion::IsPotentiallyTraversable", "NextBotExpensive" ); |
|
|
|
// if 'to' is high above us, it's not directly traversable |
|
// Adding a bit of fudge room to allow for floating point roundoff errors |
|
if ( ( to.z - from.z ) > GetMaxJumpHeight() + 0.1f ) |
|
{ |
|
Vector along = to - from; |
|
along.NormalizeInPlace(); |
|
if ( along.z > GetTraversableSlopeLimit() ) |
|
{ |
|
if ( fraction ) |
|
{ |
|
*fraction = 0.0f; |
|
} |
|
return false; |
|
} |
|
} |
|
|
|
trace_t result; |
|
NextBotTraversableTraceFilter filter( GetBot(), when ); |
|
|
|
// use a small hull since we cannot simulate collision resolution and avoidance along the way |
|
const float probeSize = 0.25f * GetBot()->GetBodyInterface()->GetHullWidth(); // Cant be TOO small, or open stairwells/grates/etc will cause problems |
|
const float probeZ = GetStepHeight(); |
|
|
|
Vector hullMin( -probeSize, -probeSize, probeZ ); |
|
Vector hullMax( probeSize, probeSize, GetBot()->GetBodyInterface()->GetCrouchHullHeight() ); |
|
TraceHull( from, to, hullMin, hullMax, GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result ); |
|
|
|
/* |
|
if ( result.DidHit() ) |
|
{ |
|
NDebugOverlay::SweptBox( from, result.endpos, hullMin, hullMax, vec3_angle, 255, 0, 0, 255, 9999.9f ); |
|
NDebugOverlay::SweptBox( result.endpos, to, hullMin, hullMax, vec3_angle, 255, 255, 0, 255, 9999.9f ); |
|
} |
|
else |
|
{ |
|
NDebugOverlay::SweptBox( from, to, hullMin, hullMax, vec3_angle, 255, 255, 0, 255, 0.1f ); |
|
} |
|
*/ |
|
|
|
if ( fraction ) |
|
{ |
|
*fraction = result.fraction; |
|
} |
|
|
|
return ( result.fraction >= 1.0f ) && ( !result.startsolid ); |
|
} |
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if there is a possible "gap" that will need to be jumped over |
|
* If true is returned, fraction of ray before gap is returned in 'fraction' |
|
*/ |
|
bool ILocomotion::HasPotentialGap( const Vector &from, const Vector &desiredTo, float *fraction ) const |
|
{ |
|
VPROF_BUDGET( "Locomotion::HasPotentialGap", "NextBot" ); |
|
|
|
// find section of this ray that is actually traversable |
|
float traversableFraction; |
|
IsPotentiallyTraversable( from, desiredTo, IMMEDIATELY, &traversableFraction ); |
|
|
|
// compute end of traversable ray |
|
Vector to = from + ( desiredTo - from ) * traversableFraction; |
|
|
|
Vector forward = to - from; |
|
float length = forward.NormalizeInPlace(); |
|
|
|
IBody *body = GetBot()->GetBodyInterface(); |
|
|
|
float step = body->GetHullWidth()/2.0f; |
|
|
|
// scan along the line checking for gaps |
|
Vector pos = from; |
|
Vector delta = step * forward; |
|
for( float t = 0.0f; t < (length + step); t += step ) |
|
{ |
|
if ( IsGap( pos, forward ) ) |
|
{ |
|
if ( fraction ) |
|
{ |
|
*fraction = ( t - step ) / ( length + step ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
pos += delta; |
|
} |
|
|
|
if ( fraction ) |
|
{ |
|
*fraction = 1.0f; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if there is a "gap" here when moving in the given direction. |
|
* A "gap" is a vertical dropoff that is too high to jump back up to. |
|
*/ |
|
bool ILocomotion::IsGap( const Vector &pos, const Vector &forward ) const |
|
{ |
|
VPROF_BUDGET( "Locomotion::IsGap", "NextBotSpiky" ); |
|
|
|
IBody *body = GetBot()->GetBodyInterface(); |
|
|
|
//float halfWidth = ( body ) ? body->GetHullWidth()/2.0f : 1.0f; |
|
|
|
// can't really jump effectively when crouched anyhow |
|
//float hullHeight = ( body ) ? body->GetStandHullHeight() : 1.0f; |
|
|
|
// use a small hull since we cannot simulate collision resolution and avoidance along the way |
|
const float halfWidth = 1.0f; |
|
const float hullHeight = 1.0f; |
|
|
|
unsigned int mask = ( body ) ? body->GetSolidMask() : MASK_PLAYERSOLID; |
|
|
|
trace_t ground; |
|
|
|
NextBotTraceFilterIgnoreActors filter( GetBot()->GetEntity(), COLLISION_GROUP_NONE ); |
|
|
|
TraceHull( pos + Vector( 0, 0, GetStepHeight() ), // start up a bit to handle rough terrain |
|
pos + Vector( 0, 0, -GetMaxJumpHeight() ), |
|
Vector( -halfWidth, -halfWidth, 0 ), Vector( halfWidth, halfWidth, hullHeight ), |
|
mask, &filter, &ground ); |
|
|
|
// int r,g,b; |
|
// |
|
// if ( ground.fraction >= 1.0f && !ground.startsolid ) |
|
// { |
|
// r = 255, g = 0, b = 0; |
|
// } |
|
// else |
|
// { |
|
// r = 0, g = 255, b = 0; |
|
// } |
|
// |
|
// NDebugOverlay::SweptBox( pos, |
|
// pos + Vector( 0, 0, -GetStepHeight() ), |
|
// Vector( -halfWidth, -halfWidth, 0 ), Vector( halfWidth, halfWidth, hullHeight ), |
|
// vec3_angle, |
|
// r, g, b, 255, 3.0f ); |
|
|
|
// if trace hit nothing, there's a gap ahead of us |
|
return ( ground.fraction >= 1.0f && !ground.startsolid ); |
|
} |
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------- |
|
bool ILocomotion::IsEntityTraversable( CBaseEntity *obstacle, TraverseWhenType when ) const |
|
{ |
|
if ( obstacle->IsWorld() ) |
|
return false; |
|
|
|
// assume bot will open a door in its path |
|
if ( FClassnameIs( obstacle, "prop_door*" ) || FClassnameIs( obstacle, "func_door*" ) ) |
|
{ |
|
CBasePropDoor *door = dynamic_cast< CBasePropDoor * >( obstacle ); |
|
|
|
if ( door && door->IsDoorOpen() ) |
|
{ |
|
// open doors are obstacles |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
// if we hit a clip brush, ignore it if it is not BRUSHSOLID_ALWAYS |
|
if ( FClassnameIs( obstacle, "func_brush" ) ) |
|
{ |
|
CFuncBrush *brush = (CFuncBrush *)obstacle; |
|
|
|
switch ( brush->m_iSolidity ) |
|
{ |
|
case CFuncBrush::BRUSHSOLID_ALWAYS: |
|
return false; |
|
case CFuncBrush::BRUSHSOLID_NEVER: |
|
return true; |
|
case CFuncBrush::BRUSHSOLID_TOGGLE: |
|
return true; |
|
} |
|
} |
|
|
|
if ( when == IMMEDIATELY ) |
|
{ |
|
// special rules in specific games can immediately break some breakables, etc. |
|
return false; |
|
} |
|
|
|
// assume bot will EVENTUALLY break breakables in its path |
|
return GetBot()->IsAbleToBreak( obstacle ); |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
bool ILocomotion::IsAreaTraversable( const CNavArea *baseArea ) const |
|
{ |
|
return !baseArea->IsBlocked( GetBot()->GetEntity()->GetTeamNumber() ); |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Reset stuck status to un-stuck |
|
*/ |
|
void ILocomotion::ClearStuckStatus( const char *reason ) |
|
{ |
|
if ( IsStuck() ) |
|
{ |
|
m_isStuck = false; |
|
|
|
// tell other components we're no longer stuck |
|
GetBot()->OnUnStuck(); |
|
} |
|
|
|
// always reset stuck monitoring data in case we cleared preemptively are were not yet stuck |
|
m_stuckPos = GetFeet(); |
|
m_stuckTimer.Start(); |
|
|
|
if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) |
|
{ |
|
DevMsg( "%3.2f: ClearStuckStatus: %s %s\n", gpGlobals->curtime, GetBot()->GetDebugIdentifier(), reason ); |
|
} |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Stuck check |
|
*/ |
|
void ILocomotion::StuckMonitor( void ) |
|
{ |
|
// a timer is needed to smooth over a few frames of inactivity due to state changes, etc. |
|
// we only want to detect idle situations when the bot really doesn't "want" to move. |
|
const float idleTime = 0.25f; |
|
if ( m_moveRequestTimer.IsGreaterThen( idleTime ) ) |
|
{ |
|
// we have no desire to move, and therefore cannot emit stuck events |
|
|
|
// prepare our internal state for when the bot starts to move next |
|
m_stuckPos = GetFeet(); |
|
m_stuckTimer.Start(); |
|
|
|
return; |
|
} |
|
|
|
// if ( !IsOnGround() ) |
|
// { |
|
// // can't be stuck when in-air |
|
// ClearStuckStatus( "Off the ground" ); |
|
// return; |
|
// } |
|
|
|
// if ( IsUsingLadder() ) |
|
// { |
|
// // can't be stuck when on a ladder (for now) |
|
// ClearStuckStatus( "On a ladder" ); |
|
// return; |
|
// } |
|
|
|
if ( IsStuck() ) |
|
{ |
|
// we are/were stuck - have we moved enough to consider ourselves "dislodged" |
|
if ( GetBot()->IsRangeGreaterThan( m_stuckPos, STUCK_RADIUS ) ) |
|
{ |
|
// we've just become un-stuck |
|
ClearStuckStatus( "UN-STUCK" ); |
|
} |
|
else |
|
{ |
|
// still stuck - periodically resend the event |
|
if ( m_stillStuckTimer.IsElapsed() ) |
|
{ |
|
m_stillStuckTimer.Start( 1.0f ); |
|
|
|
if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) |
|
{ |
|
DevMsg( "%3.2f: %s STILL STUCK\n", gpGlobals->curtime, GetBot()->GetDebugIdentifier() ); |
|
NDebugOverlay::Circle( m_stuckPos + Vector( 0, 0, 5.0f ), QAngle( -90.0f, 0, 0 ), 5.0f, 255, 0, 0, 255, true, 1.0f ); |
|
} |
|
|
|
GetBot()->OnStuck(); |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
// we're not stuck - yet |
|
|
|
if ( /*IsClimbingOrJumping() || */GetBot()->IsRangeGreaterThan( m_stuckPos, STUCK_RADIUS ) ) |
|
{ |
|
// we have moved - reset anchor |
|
m_stuckPos = GetFeet(); |
|
|
|
if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) |
|
{ |
|
NDebugOverlay::Cross3D( m_stuckPos, 3.0f, 255, 0, 255, true, 3.0f ); |
|
} |
|
|
|
m_stuckTimer.Start(); |
|
} |
|
else |
|
{ |
|
// within stuck range of anchor. if we've been here too long, we're stuck |
|
if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) |
|
{ |
|
NDebugOverlay::Line( GetBot()->GetEntity()->WorldSpaceCenter(), m_stuckPos, 255, 0, 255, true, 0.1f ); |
|
} |
|
|
|
float minMoveSpeed = 0.1f * GetDesiredSpeed() + 0.1f; |
|
float escapeTime = STUCK_RADIUS / minMoveSpeed; |
|
if ( m_stuckTimer.IsGreaterThen( escapeTime ) ) |
|
{ |
|
// we have taken too long - we're stuck |
|
m_isStuck = true; |
|
|
|
if ( GetBot()->IsDebugging( NEXTBOT_ERRORS ) ) |
|
{ |
|
DevMsg( "%3.2f: %s STUCK at position( %3.2f, %3.2f, %3.2f )\n", gpGlobals->curtime, GetBot()->GetDebugIdentifier(), m_stuckPos.x, m_stuckPos.y, m_stuckPos.z ); |
|
|
|
NDebugOverlay::Circle( m_stuckPos + Vector( 0, 0, 15.0f ), QAngle( -90.0f, 0, 0 ), 3.0f, 255, 255, 0, 255, true, 1.0f ); |
|
NDebugOverlay::Circle( m_stuckPos + Vector( 0, 0, 5.0f ), QAngle( -90.0f, 0, 0 ), 5.0f, 255, 0, 0, 255, true, 9999999.9f ); |
|
} |
|
|
|
// tell other components we've become stuck |
|
GetBot()->OnStuck(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
const Vector &ILocomotion::GetFeet( void ) const |
|
{ |
|
return GetBot()->GetEntity()->GetAbsOrigin(); |
|
} |
|
|
|
|