diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 5c18f81818..fec1fd9998 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1515,6 +1515,9 @@ set(UNITY_SOURCE_NEO_BOT neo/bot/neo_bot.cpp neo/bot/neo_bot_path_compute.cpp neo/bot/neo_bot_path_cost.cpp + neo/bot/neo_bot_obstacle_props.cpp + neo/bot/neo_bot_path_obstacles.cpp + neo/bot/neo_bot_prop_detour.cpp neo/bot/neo_bot_path_reservation.cpp neo/bot/neo_bot_body.cpp neo/bot/neo_bot_locomotion.cpp @@ -1577,6 +1580,9 @@ target_sources_grouped( neo/bot/neo_bot.h neo/bot/neo_bot_path_compute.h neo/bot/neo_bot_path_cost.h + neo/bot/neo_bot_obstacle_props.h + neo/bot/neo_bot_path_obstacles.h + neo/bot/neo_bot_prop_detour.h neo/bot/neo_bot_path_reservation.h neo/bot/neo_bot_body.h neo/bot/neo_bot_contextual_query_interface.h diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.cpp b/src/game/server/NextBot/Path/NextBotPathFollow.cpp index 216f9a10e0..d9c427bb1c 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.cpp +++ b/src/game/server/NextBot/Path/NextBotPathFollow.cpp @@ -102,6 +102,10 @@ void PathFollower::Invalidate( void ) m_avoidTimer.Invalidate(); m_waitTimer.Invalidate(); m_hindrance = NULL; + +#ifdef NEO + m_pathObstacles.Reset(); +#endif } @@ -114,6 +118,10 @@ void PathFollower::OnPathChanged( INextBot *bot, Path::ResultType result ) // start from the beginning m_goal = FirstSegment(); m_result = result; + +#ifdef NEO + m_pathObstacles.Reset(); +#endif } @@ -684,6 +692,19 @@ void PathFollower::Update( INextBot *bot ) return; } +#ifdef NEO + // look ahead for breakables and walk around props in the way, which the nav mesh does not know about, + // heading for the path segment past them rather than turning back for a goal the detour went around + m_pathObstacles.Update( bot, *this ); + const Path::Segment *detourPathGoal = m_pathObstacles.GetPathGoal(); + if ( detourPathGoal ) + { + m_goal = detourPathGoal; + } + + const bool isDetouring = m_pathObstacles.IsDetouring(); +#endif + // use the direction towards the goal as 'forward' direction Vector forward = m_goal->pos - mover->GetFeet(); @@ -739,8 +760,15 @@ void PathFollower::Update( INextBot *bot ) NDebugOverlay::Line( mover->GetFeet(), mover->GetFeet() + axisSize * left, 0, 0, 255, true, 0.1f ); } +#ifdef NEO + // a detour crosses only walkable floor, and the prop it goes around is no ledge to climb + const bool shouldClimb = !isDetouring; +#else + const bool shouldClimb = true; +#endif + // climb up ledges - if ( !Climbing( bot, m_goal, forward, left, goalRange ) ) + if ( shouldClimb && !Climbing( bot, m_goal, forward, left, goalRange ) ) { // a failed climb could mean an invalid path if ( !IsValid() ) @@ -829,6 +857,13 @@ void PathFollower::Update( INextBot *bot ) Vector goalPos = m_goal->pos; +#ifdef NEO + if ( isDetouring ) + { + goalPos = m_pathObstacles.GetMoveGoal(); + } +#endif + // avoid small obstacles forward = goalPos - mover->GetFeet(); forward.z = 0.0f; diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.h b/src/game/server/NextBot/Path/NextBotPathFollow.h index 3b37e95072..c421f82852 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.h +++ b/src/game/server/NextBot/Path/NextBotPathFollow.h @@ -9,6 +9,9 @@ #include "nav_mesh.h" #include "nav_pathfind.h" #include "NextBotPath.h" +#ifdef NEO +#include "neo/bot/neo_bot_path_obstacles.h" +#endif class INextBot; class ILocomotion; @@ -42,6 +45,10 @@ class PathFollower : public Path Path::ResultType GetResult() const { return m_result; } +#ifdef NEO + CBaseEntity *GetBreakableInWay() const { return m_pathObstacles.GetBreakableInWay(); } // a breakable the last look ahead met on the path +#endif + private: const Path::Segment *m_goal; // our current goal along the path float m_minLookAheadRange; @@ -83,6 +90,7 @@ class PathFollower : public Path #ifdef NEO bool HasFallenBelowPath( INextBot *bot ) const; // return true if we stand off the path, more than a step below it bool m_wasOnGround; + CNEOBotPathObstacles m_pathObstacles; // looks ahead for props to steer around and breakables in the way #endif float m_goalTolerance; diff --git a/src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.cpp b/src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.cpp index 03276cf9f8..59c45f1271 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.cpp @@ -40,30 +40,11 @@ CBaseEntity *CNEOBotPathClearBreakable::GetBreakableInPath( CNEOBot *me ) return nullptr; } - const Path::Segment *goal = path->GetCurrentGoal(); - if ( !goal ) + // the path follower's look ahead for props also finds breakables in the way + CBaseEntity *breakable = path->GetBreakableInWay(); + if ( breakable && breakable->IsAlive() && breakable->GetHealth() > 0 ) { - return nullptr; - } - - // Trace forward along the path to look for breakables - // We use a hull trace to match the bot's collision size - trace_t tr; - UTIL_TraceHull( me->GetAbsOrigin() + Vector( 0, 0, 10 ), - goal->pos + Vector( 0, 0, 10 ), - me->GetBodyInterface()->GetHullMins(), - me->GetBodyInterface()->GetHullMaxs(), - MASK_NPCSOLID, - me->GetEntity(), - COLLISION_GROUP_NONE, - &tr ); - - if ( tr.DidHit() && tr.m_pEnt ) - { - if ( me->IsAbleToBreak( tr.m_pEnt ) ) - { - return tr.m_pEnt; - } + return breakable; } return nullptr; diff --git a/src/game/server/neo/bot/neo_bot_obstacle_props.cpp b/src/game/server/neo/bot/neo_bot_obstacle_props.cpp new file mode 100644 index 0000000000..afa7897adc --- /dev/null +++ b/src/game/server/neo/bot/neo_bot_obstacle_props.cpp @@ -0,0 +1,270 @@ +#include "cbase.h" +#include "NextBot.h" +#include "NextBotBodyInterface.h" +#include "NextBotLocomotionInterface.h" +#include "neo_bot_obstacle_props.h" +#include "vphysics_interface.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +namespace +{ +// A moving prop is kept clear of where it will be within this time, at its current velocity +constexpr float OBSTACLE_PROP_MOTION_PREDICT_TIME = 1.0f; +constexpr float OBSTACLE_PROP_MOVING_MIN_SPEED = 10.0f; + +// A prop this light that physics moves is shoved out of the way, as a player does +constexpr float OBSTACLE_PROP_PUSHABLE_MAX_MASS = 200.0f; + +// Props are looked for up to this high above the path, so one coming down is seen in time +constexpr float OBSTACLE_PROP_QUERY_HEADROOM = 512.0f; + +// A prop smaller than this in every direction (a can, a bottle) is pushed aside, not walked around +const Vector OBSTACLE_PROP_SMALL_SIZE( 16.0f, 16.0f, 40.0f ); + +// The body box is this much narrower than the hull, so a prop the bot only brushes is not in its way +constexpr float OBSTACLE_PROP_BODY_CLEARANCE = 1.0f; +} + +//---------------------------------------------------------------------------------------------------------------- +// Props the nav mesh cannot account for: physics props and physboxes move when pushed or shot, +// and bone followers move with an animated prop's bones (fixed props belong to the mesh) +static bool IsMovableProp( CBaseEntity *entity ) +{ + return FClassnameIs( entity, "prop_physics*" ) || FClassnameIs( entity, "func_physbox*" ) || FClassnameIs( entity, "phys_bone_follower" ); +} + + +//---------------------------------------------------------------------------------------------------------------- +static bool IsSolidToPlayers( CBaseEntity *entity ) +{ + if ( !entity->IsSolid() || entity->IsSolidFlagSet( FSOLID_TRIGGER ) ) + { + return false; + } + + return g_pGameRules->ShouldCollide( COLLISION_GROUP_PLAYER_MOVEMENT, entity->GetCollisionGroup() ); +} + + +//---------------------------------------------------------------------------------------------------------------- +namespace +{ + // The movable props and the breakables solid to players that a spatial partition query meets, into the lists given: + // a box query into a fixed-size list fills up with everything it finds, cans, weapons and triggers alike + class CObstacleEntityEnum : public IPartitionEnumerator + { + public: + CObstacleEntityEnum( CUtlVector< CBaseEntity * > *props, INextBot *breaker, CUtlVector< CBaseEntity * > *breakables ) + : m_props( props ), m_breaker( breaker ), m_breakables( breakables ) + { + Assert( !m_breakables || m_breaker ); + } + + virtual IterationRetval_t EnumElement( IHandleEntity *handleEntity ) + { + CBaseEntity *entity = gEntList.GetBaseEntity( handleEntity->GetRefEHandle() ); + if ( !entity || !IsSolidToPlayers( entity ) ) + { + return ITERATION_CONTINUE; + } + + if ( m_props && IsMovableProp( entity ) ) + { + m_props->AddToTail( entity ); + } + + if ( m_breakables && entity->GetHealth() > 0 && m_breaker->IsAbleToBreak( entity ) ) + { + m_breakables->AddToTail( entity ); + } + + return ITERATION_CONTINUE; + } + + private: + CUtlVector< CBaseEntity * > *m_props; + INextBot *m_breaker; + CUtlVector< CBaseEntity * > *m_breakables; + }; +} + + +//---------------------------------------------------------------------------------------------------------------- +static Vector GetPropVelocity( CBaseEntity *entity ) +{ + IPhysicsObject *physics = entity->VPhysicsGetObject(); + if ( !physics || physics->IsAsleep() ) + { + return vec3_origin; + } + + Vector velocity; + physics->GetVelocity( &velocity, NULL ); + return velocity.IsLengthGreaterThan( OBSTACLE_PROP_MOVING_MIN_SPEED ) ? velocity : vec3_origin; +} + + +//---------------------------------------------------------------------------------------------------------------- +// A light prop physics moves freely: walking into it shoves it aside. +// One hung on a rope or a hinge swings back, and an animated prop's bone followers move with their prop +static bool IsPushable( CBaseEntity *entity ) +{ + IPhysicsObject *physics = entity->VPhysicsGetObject(); + return physics && physics->IsMoveable() && physics->GetMass() <= OBSTACLE_PROP_PUSHABLE_MAX_MASS + && !physics->IsAttachedToConstraint( false ) && !FClassnameIs( entity, "phys_bone_follower" ); +} + + +//---------------------------------------------------------------------------------------------------------------- +// The bot can see the prop: no world geometry between its eyes and the prop's middle +static bool IsInSight( INextBot *bot, CBaseEntity *entity ) +{ + trace_t result; + CTraceFilterWorldOnly filter; + UTIL_TraceLine( bot->GetBodyInterface()->GetEyePosition(), entity->WorldSpaceCenter(), MASK_BLOCKLOS, &filter, &result ); + return !result.DidHit(); +} + + +//---------------------------------------------------------------------------------------------------------------- +BodyBox_t GetBodyBox( INextBot *bot ) +{ + const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth() - OBSTACLE_PROP_BODY_CLEARANCE; + + BodyBox_t box; + box.mins.Init( -halfWidth, -halfWidth, bot->GetLocomotionInterface()->GetStepHeight() ); + box.maxs.Init( halfWidth, halfWidth, bot->GetBodyInterface()->GetStandHullHeight() ); + return box; +} + + +//---------------------------------------------------------------------------------------------------------------- +bool BodyMeetsEntity( const BodyBox_t &body, const Vector &from, const Vector &to, CBaseEntity *entity, float *fraction ) +{ + Ray_t ray; + ray.Init( from, to, body.mins, body.maxs ); + + trace_t result; + enginetrace->ClipRayToEntity( ray, MASK_PLAYERSOLID, entity, &result ); + *fraction = result.startsolid ? 0.0f : result.fraction; + return result.startsolid || result.fraction < 1.0f; +} + + +//---------------------------------------------------------------------------------------------------------------- +bool BodyMeetsProp( const BodyBox_t &body, const Vector &from, const Vector &to, const PropObstacle_t &obstacle, float *fraction ) +{ + if ( !BodyMeetsEntity( body, from, to, obstacle.entity, fraction ) ) + { + *fraction = 1.0f; + } + + if ( *fraction == 0.0f || obstacle.sweep.IsZero() ) + { + return *fraction < 1.0f; + } + + // moving the body back along the prop's motion is the same as moving the prop forward + float movedFraction; + if ( BodyMeetsEntity( body, from - obstacle.sweep, to - obstacle.sweep, obstacle.entity, &movedFraction ) ) + { + *fraction = MIN( *fraction, movedFraction ); + } + + return *fraction < 1.0f; +} + +//---------------------------------------------------------------------------------------------------------------- +// The movable props and the breakables around the floor in the box, in one partition query, into the lists given +static void QueryObstacleEntities( const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *props, + INextBot *breaker, CUtlVector< CBaseEntity * > *breakables ) +{ + const Vector queryLo = floorLo - Vector( OBSTACLE_PROP_QUERY_MARGIN, OBSTACLE_PROP_QUERY_MARGIN, OBSTACLE_PROP_FLOOR_TOLERANCE ); + const Vector queryHi = floorHi + Vector( OBSTACLE_PROP_QUERY_MARGIN, OBSTACLE_PROP_QUERY_MARGIN, OBSTACLE_PROP_QUERY_HEADROOM ); + + CObstacleEntityEnum entityEnum( props, breaker, breakables ); + partition->EnumerateElementsInBox( PARTITION_ENGINE_NON_STATIC_EDICTS, queryLo, queryHi, false, &entityEnum ); +} + + +//---------------------------------------------------------------------------------------------------------------- +void FindMovableProps( const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *props ) +{ + QueryObstacleEntities( floorLo, floorHi, props, NULL, NULL ); +} + + +//---------------------------------------------------------------------------------------------------------------- +void FindBreakables( INextBot *bot, const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *breakables ) +{ + QueryObstacleEntities( floorLo, floorHi, NULL, bot, breakables ); +} + + +//---------------------------------------------------------------------------------------------------------------- +void CollectProps( INextBot *bot, const Vector &floorLo, const Vector &floorHi, CUtlVector< PropObstacle_t > *obstacles, + CUtlVector< CBaseEntity * > *breakables ) +{ + const BodyBox_t body = GetBodyBox( bot ); + const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth(); + + CUtlVector< CBaseEntity * > props; + QueryObstacleEntities( floorLo, floorHi, &props, breakables ? bot : NULL, breakables ); + FOR_EACH_VEC( props, i ) + { + CBaseEntity *entity = props[ i ]; + Vector propLo, propHi; + entity->CollisionProp()->WorldSpaceAABB( &propLo, &propHi ); + + const Vector size = propHi - propLo; + if ( size.x < OBSTACLE_PROP_SMALL_SIZE.x && size.y < OBSTACLE_PROP_SMALL_SIZE.y && size.z < OBSTACLE_PROP_SMALL_SIZE.z ) + { + continue; + } + + const Vector sweep = GetPropVelocity( entity ) * OBSTACLE_PROP_MOTION_PREDICT_TIME; + Vector sweptLo = propLo; + Vector sweptHi = propHi; + VectorMin( sweptLo, propLo + sweep, sweptLo ); + VectorMax( sweptHi, propHi + sweep, sweptHi ); + + // overhead or underfoot all over the box + if ( sweptLo.z > floorHi.z + body.maxs.z || sweptHi.z < floorLo.z + body.mins.z ) + { + continue; + } + + if ( !IsInSight( bot, entity ) ) + { + continue; + } + + PropObstacle_t obstacle; + obstacle.entity = entity; + obstacle.sweep = sweep; + obstacle.isPushable = IsPushable( entity ); + obstacle.lo.Init( sweptLo.x - halfWidth, sweptLo.y - halfWidth ); + obstacle.hi.Init( sweptHi.x + halfWidth, sweptHi.y + halfWidth ); + obstacles->AddToTail( obstacle ); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +int CountRestingProps( const Vector &floorLo, const Vector &floorHi ) +{ + CUtlVector< CBaseEntity * > props; + FindMovableProps( floorLo, floorHi, &props ); + FOR_EACH_VEC( props, i ) + { + IPhysicsObject *physics = props[ i ]->VPhysicsGetObject(); + if ( physics && !physics->IsAsleep() ) + { + return OBSTACLE_PROPS_MOVING; + } + } + + return props.Count(); +} diff --git a/src/game/server/neo/bot/neo_bot_obstacle_props.h b/src/game/server/neo/bot/neo_bot_obstacle_props.h new file mode 100644 index 0000000000..75c47b7ca2 --- /dev/null +++ b/src/game/server/neo/bot/neo_bot_obstacle_props.h @@ -0,0 +1,51 @@ +#pragma once + +class INextBot; +class CBaseEntity; + +// A query for props reaches this far past the floor box on each side, +constexpr float OBSTACLE_PROP_QUERY_MARGIN = 48.0f; +// and this far below its floor +constexpr float OBSTACLE_PROP_FLOOR_TOLERANCE = 40.0f; + +// What a count of the props resting in a region says when one of them is awake +constexpr int OBSTACLE_PROPS_MOVING = -1; + +//---------------------------------------------------------------------------------------------------------------- +// A movable prop in the bot's body space over the floor, or soon there +struct PropObstacle_t +{ + CBaseEntity *entity; + Vector sweep; // how far it moves within OBSTACLE_PROP_MOTION_PREDICT_TIME + Vector2D lo, hi; // footprint over that time, grown by a hull half-width + bool isPushable; // light enough to shove aside +}; + +// The bot's body as a box: from a step over the floor to its standing height +struct BodyBox_t +{ + Vector mins, maxs; +}; + +BodyBox_t GetBodyBox( INextBot *bot ); + +// Return true if a body moving from 'from' to 'to' touches the entity where it stands +bool BodyMeetsEntity( const BodyBox_t &body, const Vector &from, const Vector &to, CBaseEntity *entity, float *fraction ); + +// Return true if a body moving from 'from' to 'to' touches the prop, now or where it will be +bool BodyMeetsProp( const BodyBox_t &body, const Vector &from, const Vector &to, const PropObstacle_t &obstacle, float *fraction ); + +// Every movable prop solid to players around the floor in the box, from below it up to the headroom above it +void FindMovableProps( const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *props ); + +// Every solid entity the bot can break around the floor in the box, over the same reach +void FindBreakables( INextBot *bot, const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *breakables ); + +// Movable props the bot can see that stand, or will soon stand, in its body space over the floor in the box +// and, given a list for them, the breakables there +void CollectProps( INextBot *bot, const Vector &floorLo, const Vector &floorHi, CUtlVector< PropObstacle_t > *obstacles, + CUtlVector< CBaseEntity * > *breakables = NULL ); + +// How many movable props stand around the floor in the box, or OBSTACLE_PROPS_MOVING if any of them is awake: +// physics puts a prop to sleep once it comes to rest, and wakes it when it is touched or moved +int CountRestingProps( const Vector &floorLo, const Vector &floorHi ); diff --git a/src/game/server/neo/bot/neo_bot_path_obstacles.cpp b/src/game/server/neo/bot/neo_bot_path_obstacles.cpp new file mode 100644 index 0000000000..a5763938c4 --- /dev/null +++ b/src/game/server/neo/bot/neo_bot_path_obstacles.cpp @@ -0,0 +1,373 @@ +#include "cbase.h" +#include "collisionutils.h" +#include "NextBot.h" +#include "NextBot/Path/NextBotPathFollow.h" +#include "NextBotLocomotionInterface.h" +#include "NextBotBodyInterface.h" +#include "neo_bot_obstacle_props.h" +#include "neo_bot_path_obstacles.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +namespace +{ +// How often the path ahead is checked, so a prop that is pushed or animates is followed +// and a breakable in the way is seen before the bot walks into it +constexpr float PATH_OBSTACLE_LOOK_INTERVAL = 0.3f; + +// How far along the path props are looked for, and how much further the detour may rejoin it +constexpr float PATH_OBSTACLE_LOOK_AHEAD_RANGE = 256.0f; +constexpr float PATH_OBSTACLE_REJOIN_EXTRA_RANGE = 128.0f; + +// A breakable is in the way when the body meets it this far along the path: +// beyond a look's walk at a run, so it is found before the bot reaches it, and near enough to melee it on arrival +constexpr float PATH_OBSTACLE_BREAKABLE_RANGE = 128.0f; + +// The detour rejoins the path this far past the last prop in the way +constexpr float PATH_OBSTACLE_REJOIN_PAST_PROP = 32.0f; +} + +//---------------------------------------------------------------------------------------------------------------- +// Return true if the line from a to b crosses the box, with the entry and exit fractions +static bool SegmentCrossesBox( const Vector2D &a, const Vector2D &b, const Vector2D &lo, const Vector2D &hi, float *enter, float *exit ) +{ + float t0 = 0.0f; + float t1 = 1.0f; + const Vector2D delta = b - a; + for ( int axis = 0; axis < 2; ++axis ) + { + if ( fabsf( delta[ axis ] ) < 1e-4f ) + { + if ( a[ axis ] < lo[ axis ] || a[ axis ] > hi[ axis ] ) + { + return false; + } + + continue; + } + + float ta = ( lo[ axis ] - a[ axis ] ) / delta[ axis ]; + float tb = ( hi[ axis ] - a[ axis ] ) / delta[ axis ]; + if ( ta > tb ) + { + V_swap( ta, tb ); + } + + t0 = MAX( t0, ta ); + t1 = MIN( t1, tb ); + if ( t0 > t1 ) + { + return false; + } + } + + *enter = t0; + *exit = t1; + return true; +} + + +//---------------------------------------------------------------------------------------------------------------- +// The box around the line's points, over its first 'range' along it +static void GetLineBounds( const CUtlVector< Vector > &line, float range, Vector *lo, Vector *hi ) +{ + *lo = line[ 0 ]; + *hi = line[ 0 ]; + float length = 0.0f; + for ( int i = 1; i < line.Count() && length < range; ++i ) + { + length += ( line[ i ] - line[ i - 1 ] ).Length2D(); + VectorMin( *lo, line[ i ], *lo ); + VectorMax( *hi, line[ i ], *hi ); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +// The breakable the body meets first walking the line, within PATH_OBSTACLE_BREAKABLE_RANGE, +// when nothing else stands in front of it +static CBaseEntity *FindBreakableInWay( INextBot *bot, const BodyBox_t &body, const CUtlVector< Vector > &line, const CUtlVector< CBaseEntity * > &breakables ) +{ + // a breakable away from the body's way over the range cannot be met, so it costs no sweep + Vector reachLo, reachHi; + GetLineBounds( line, PATH_OBSTACLE_BREAKABLE_RANGE, &reachLo, &reachHi ); + reachLo += body.mins; + reachHi += body.maxs; + + CBaseEntity *nearest = NULL; + int nearestLeg = -1; + float nearestDistance = PATH_OBSTACLE_BREAKABLE_RANGE; + FOR_EACH_VEC( breakables, i ) + { + Vector entityLo, entityHi; + breakables[ i ]->CollisionProp()->WorldSpaceAABB( &entityLo, &entityHi ); + if ( !IsBoxIntersectingBox( reachLo, reachHi, entityLo, entityHi ) ) + { + continue; + } + + float legStart = 0.0f; + for ( int leg = 0; leg + 1 < line.Count() && legStart < nearestDistance; ++leg ) + { + const float legLength = ( line[ leg + 1 ] - line[ leg ] ).Length2D(); + float fraction; + if ( BodyMeetsEntity( body, line[ leg ], line[ leg + 1 ], breakables[ i ], &fraction ) && legStart + fraction * legLength < nearestDistance ) + { + nearest = breakables[ i ]; + nearestLeg = leg; + nearestDistance = legStart + fraction * legLength; + break; + } + + legStart += legLength; + } + } + + if ( !nearest ) + { + return NULL; + } + + // what the body meets first on the way may be a crate, a door or a wall in front of the breakable: + // keep the breakable only if the body reaches it first, passing through players, who move + CTraceFilterNoNPCsOrPlayer filter( bot->GetEntity(), COLLISION_GROUP_NONE ); + for ( int leg = 0; leg <= nearestLeg; ++leg ) + { + trace_t result; + UTIL_TraceHull( line[ leg ], line[ leg + 1 ], body.mins, body.maxs, MASK_PLAYERSOLID, &filter, &result ); + if ( result.DidHit() ) + { + return ( result.m_pEnt == nearest ) ? nearest : NULL; + } + } + + return NULL; +} + +//---------------------------------------------------------------------------------------------------------------- +// The path ahead as a line from the bot's feet, over walked segments only, up to the range, +// and, given a list for them, the segment each point of the line belongs to +static void GetPathAhead( const PathFollower &path, const Vector &feet, float range, CUtlVector< Vector > *line, CUtlVector< const Path::Segment * > *segments = NULL ) +{ + line->AddToTail( feet ); + if ( segments ) + { + segments->AddToTail( path.GetCurrentGoal() ); + } + + float length = 0.0f; + for ( const Path::Segment *seg = path.GetCurrentGoal(); seg && length < range; seg = path.NextSegment( seg ) ) + { + if ( seg->ladder || seg->type != Path::ON_GROUND ) + { + break; + } + + const Vector leg = seg->pos - line->Tail(); + const float legLength = leg.Length2D(); + if ( segments ) + { + segments->AddToTail( seg ); + } + + if ( length + legLength > range ) + { + line->AddToTail( line->Tail() + leg * ( ( range - length ) / legLength ) ); + break; + } + + length += legLength; + line->AddToTail( seg->pos ); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +// Return the point the given distance along the line, and the index of the leg's end +static Vector GetPointAlong( const CUtlVector< Vector > &line, float distance, int *legEnd ) +{ + for ( int i = 0; i + 1 < line.Count(); ++i ) + { + const float legLength = ( line[ i + 1 ] - line[ i ] ).Length2D(); + if ( distance <= legLength && legLength > 0.0f ) + { + *legEnd = i + 1; + return line[ i ] + ( line[ i + 1 ] - line[ i ] ) * ( distance / legLength ); + } + + distance -= legLength; + } + + *legEnd = line.Count() - 1; + return line.Tail(); +} + + +//---------------------------------------------------------------------------------------------------------------- +CNEOBotPathObstacles::CNEOBotPathObstacles() +{ + Reset(); +} + + +//---------------------------------------------------------------------------------------------------------------- +void CNEOBotPathObstacles::Reset() +{ + m_lookTimer.Invalidate(); + m_breakableTimer.Invalidate(); + m_breakable = NULL; + m_detour.Reset(); +} + + +//---------------------------------------------------------------------------------------------------------------- +void CNEOBotPathObstacles::Update( INextBot *bot, const PathFollower &path ) +{ + bool isSearchDue = false; + if ( m_lookTimer.IsElapsed() ) + { + m_lookTimer.Start( PATH_OBSTACLE_LOOK_INTERVAL ); + + if ( !m_detour.IsDetouring() ) + { + Plan( bot, path ); + } + else + { + isSearchDue = true; + } + } + + if ( m_detour.Update( bot, isSearchDue ) ) + { + m_lookTimer.Invalidate(); + } + + // a plan looks for breakables with the props; while none does (detouring, or waiting to retry), they are looked for alone + if ( m_breakableTimer.IsElapsed() ) + { + LookForBreakable( bot, path ); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +// Look for props in the way on the path ahead, and when there are, a way past them on the mesh +void CNEOBotPathObstacles::Plan( INextBot *bot, const PathFollower &path ) +{ + ILocomotion *mover = bot->GetLocomotionInterface(); + const Path::Segment *goal = path.GetCurrentGoal(); + if ( !goal || goal->ladder || goal->type != Path::ON_GROUND || !mover->IsOnGround() ) + { + return; + } + + CUtlVector< Vector > line; + CUtlVector< const Path::Segment * > segments; + GetPathAhead( path, mover->GetFeet(), PATH_OBSTACLE_LOOK_AHEAD_RANGE + PATH_OBSTACLE_REJOIN_EXTRA_RANGE, &line, &segments ); + if ( line.Count() < 2 ) + { + return; + } + + Vector lineLo, lineHi; + GetLineBounds( line, FLT_MAX, &lineLo, &lineHi ); + + CUtlVector< PropObstacle_t > obstacles; + CUtlVector< CBaseEntity * > breakables; + CollectProps( bot, lineLo, lineHi, &obstacles, &breakables ); + + const BodyBox_t body = GetBodyBox( bot ); + m_breakable = FindBreakableInWay( bot, body, line, breakables ); + m_breakableTimer.Start( PATH_OBSTACLE_LOOK_INTERVAL ); + + if ( obstacles.Count() == 0 ) + { + return; + } + + // which props the body would meet along the path, and where it would be past all of them + float rejoinDistance = -1.0f; + bool isPathPushable = true; + Vector2D regionLo = line[ 0 ].AsVector2D(); + Vector2D regionHi = line[ 0 ].AsVector2D(); + FOR_EACH_VEC( obstacles, i ) + { + float legStart = 0.0f; + for ( int leg = 0; leg + 1 < line.Count() && legStart < PATH_OBSTACLE_LOOK_AHEAD_RANGE; ++leg ) + { + const float legLength = ( line[ leg + 1 ] - line[ leg ] ).Length2D(); + float fraction, enter, exit; + if ( BodyMeetsProp( body, line[ leg ], line[ leg + 1 ], obstacles[ i ], &fraction ) + && legStart + fraction * legLength < PATH_OBSTACLE_LOOK_AHEAD_RANGE + && SegmentCrossesBox( line[ leg ].AsVector2D(), line[ leg + 1 ].AsVector2D(), obstacles[ i ].lo, obstacles[ i ].hi, &enter, &exit ) ) + { + rejoinDistance = MAX( rejoinDistance, legStart + exit * legLength + PATH_OBSTACLE_REJOIN_PAST_PROP ); + isPathPushable = isPathPushable && obstacles[ i ].isPushable; + regionLo = regionLo.Min( obstacles[ i ].lo ); + regionHi = regionHi.Max( obstacles[ i ].hi ); + } + + legStart += legLength; + } + } + + if ( rejoinDistance < 0.0f ) + { + return; + } + + int rejoinLegEnd; + const Vector rejoin = GetPointAlong( line, rejoinDistance, &rejoinLegEnd ); + float floorLo = rejoin.z; + float floorHi = rejoin.z; + float distance = 0.0f; + for ( int i = 0; i < line.Count() && distance <= rejoinDistance; ++i ) + { + regionLo = regionLo.Min( line[ i ].AsVector2D() ); + regionHi = regionHi.Max( line[ i ].AsVector2D() ); + floorLo = MIN( floorLo, line[ i ].z ); + floorHi = MAX( floorHi, line[ i ].z ); + distance += ( i + 1 < line.Count() ) ? ( line[ i + 1 ] - line[ i ] ).Length2D() : 0.0f; + } + + PropDetourRequest_t request; + request.rejoin = rejoin; + request.rejoinGoal = segments[ rejoinLegEnd ]; + request.resumeGoal = goal; + request.regionLo = regionLo.Min( rejoin.AsVector2D() ); + request.regionHi = regionHi.Max( rejoin.AsVector2D() ); + request.floorLo = floorLo; + request.floorHi = floorHi; + request.isPathPushable = isPathPushable; + + const float lookAgainIn = m_detour.Plan( bot, request, PATH_OBSTACLE_LOOK_INTERVAL ); + if ( lookAgainIn > 0.0f ) + { + m_lookTimer.Start( lookAgainIn ); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +// Look along the path ahead for a breakable in the way, without the props +void CNEOBotPathObstacles::LookForBreakable( INextBot *bot, const PathFollower &path ) +{ + m_breakableTimer.Start( PATH_OBSTACLE_LOOK_INTERVAL ); + m_breakable = NULL; + + CUtlVector< Vector > line; + GetPathAhead( path, bot->GetLocomotionInterface()->GetFeet(), PATH_OBSTACLE_BREAKABLE_RANGE, &line ); + if ( line.Count() < 2 ) + { + return; + } + + Vector lineLo, lineHi; + GetLineBounds( line, FLT_MAX, &lineLo, &lineHi ); + + CUtlVector< CBaseEntity * > breakables; + FindBreakables( bot, lineLo, lineHi, &breakables ); + m_breakable = FindBreakableInWay( bot, GetBodyBox( bot ), line, breakables ); +} diff --git a/src/game/server/neo/bot/neo_bot_path_obstacles.h b/src/game/server/neo/bot/neo_bot_path_obstacles.h new file mode 100644 index 0000000000..11eade5a2e --- /dev/null +++ b/src/game/server/neo/bot/neo_bot_path_obstacles.h @@ -0,0 +1,39 @@ +#pragma once + +#include "NextBot/Path/NextBotPath.h" +#include "neo_bot_prop_detour.h" + +class INextBot; +class PathFollower; + +//---------------------------------------------------------------------------------------------------------------- +// Looks along a bot's path for what the nav mesh cannot know about: +// props that move, which it detours around, and breakables in the way, which it reports for the bot to clear +class CNEOBotPathObstacles +{ +public: + CNEOBotPathObstacles(); + + void Reset(); + + void Update( INextBot *bot, const PathFollower &path ); + + bool IsDetouring() const { return m_detour.IsDetouring(); } + const Vector &GetMoveGoal() const { return m_detour.GetMoveGoal(); } // where to move next while detouring + + // The path goal the follower should take, if any: the one past the props while detouring, + // and the one it had before the detour when the detour is given up + const Path::Segment *GetPathGoal() const { return m_detour.GetPathGoal(); } + + // The nearest breakable the bot's body would meet on the path ahead, as of the last look + CBaseEntity *GetBreakableInWay() const { return m_breakable.Get(); } + +private: + void Plan( INextBot *bot, const PathFollower &path ); + void LookForBreakable( INextBot *bot, const PathFollower &path ); + + CountdownTimer m_lookTimer; // the look ahead, or the detour's search again while detouring + CountdownTimer m_breakableTimer; // the look for breakables alone, while no plan is looking for them + CHandle< CBaseEntity > m_breakable; + CNEOBotPropDetour m_detour; +}; diff --git a/src/game/server/neo/bot/neo_bot_prop_detour.cpp b/src/game/server/neo/bot/neo_bot_prop_detour.cpp new file mode 100644 index 0000000000..40e4e61399 --- /dev/null +++ b/src/game/server/neo/bot/neo_bot_prop_detour.cpp @@ -0,0 +1,851 @@ +#include "cbase.h" +#include "nav_mesh.h" +#include "NextBot.h" +#include "NextBotLocomotionInterface.h" +#include "NextBotBodyInterface.h" +#include "neo_bot_obstacle_props.h" +#include "neo_bot_prop_detour.h" +#include "tier1/utlpriorityqueue.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +namespace +{ +// While every prop in a wide detour's region rests where the last search saw it, the detour is searched again only this often: +// the bot may since have come to see props that were out of its sight +constexpr float PROP_DETOUR_AT_REST_REPLAN_INTERVAL = 1.0f; + +// A waypoint this close is reached, +constexpr float PROP_DETOUR_WAYPOINT_REACHED_RANGE = 12.0f; +// and a bot this far off the straight line to its next waypoint has left the route the search checked +constexpr float PROP_DETOUR_OFF_ROUTE_RANGE = 16.0f; + +// The grid laid over the floor a detour may use reaches as far past the path and the props as the prop query does, +constexpr float PROP_DETOUR_GRID_MARGIN = OBSTACLE_PROP_QUERY_MARGIN; +// and as far up or down from them +constexpr float PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE = OBSTACLE_PROP_FLOOR_TOLERANCE; +constexpr float PROP_DETOUR_GRID_CELL_SIZE = 16.0f; +constexpr int PROP_DETOUR_GRID_MAX_CELLS_PER_SIDE = 96; + +// With no way around near the path, as past a row of props that runs on beyond the ones the path crosses, +// the search region grows over the props it touches, as far as cells of the finest size reach, +constexpr float PROP_DETOUR_WIDE_SEARCH_MAX_SIZE = PROP_DETOUR_GRID_MAX_CELLS_PER_SIDE * PROP_DETOUR_GRID_CELL_SIZE; +// and when even that finds no way around, the bot looks again after this long, not at every replan +constexpr float PROP_DETOUR_WIDE_SEARCH_RETRY_INTERVAL = 1.0f; + +// Stepping through a cell a prop occupies costs as much as this many free cells: +// the search crosses a prop only to get out from under one, and a route that has to cross one is no detour +constexpr float PROP_DETOUR_OCCUPIED_CELL_COST = 1000.0f; +constexpr float PROP_DETOUR_FREE_CELL_COST = 1.0f; + +// A light prop's cells cost this many free cells, so a bot walks around one chair where there is room, +constexpr float PROP_DETOUR_PUSHABLE_CELL_COST = 4.0f; +// and when every prop in the way on the path is light, a route through them past its first few cells is no detour: +// the bot keeps its path, shoves through or climbs over them as a player does, and looks again after this long +constexpr int PROP_DETOUR_ROUTE_START_CELLS = 3; +constexpr float PROP_DETOUR_PUSH_THROUGH_RETRY_INTERVAL = 1.0f; + +constexpr float PROP_DETOUR_DIAGONAL_STEP_LENGTH = 1.41421356f; +} + +//---------------------------------------------------------------------------------------------------------------- +namespace +{ + // What a search for a way past the props found + enum DetourResult + { + DETOUR_FOUND, + DETOUR_NONE, // no way past that keeps clear of the props + DETOUR_THROUGH_PUSHABLE, // the way past is through props the bot can shove aside + }; + + // Whether a route through light props, which the bot can shove aside, is a way around the props in the way + enum PushableRoute + { + PUSHABLE_ROUTE_IS_DETOUR, // a heavy prop is in the way: going around it through light ones is fine + PUSHABLE_ROUTE_IS_NO_DETOUR, // only light props are in the way: the bot keeps its path through them + }; +} + + +//---------------------------------------------------------------------------------------------------------------- +// Cells over the nav mesh around the path ahead, each off the mesh, free, or occupied by a prop, searched for a way past the props; +// a cell's nav area is looked up only when the search or a prop first reaches it +class CPropDetourGrid +{ +public: + CPropDetourGrid( INextBot *bot, const Vector2D &lo, const Vector2D &hi, float floorLo, float floorHi ); + + void MarkProps( const CUtlVector< PropObstacle_t > &obstacles ); + bool FindRoute( const Vector &from, const Vector &to, CUtlVector< int > *route ) const; + void Straighten( const CUtlVector< int > &route, CUtlVector< Vector > *waypoints ) const; + + bool IsOccupied( int cell ) const { return GetState( cell ) == CELL_OCCUPIED; } + bool IsPushableCell( int cell ) const { return GetState( cell ) == CELL_PUSHABLE; } + bool IsNearFeet( int cell, const Vector &feet ) const; + +private: + enum CellState + { + CELL_UNKNOWN, + CELL_OFF_MESH, + CELL_FREE, + CELL_PUSHABLE, // the body there would touch only props it can shove aside + CELL_OCCUPIED, + }; + + CellState GetState( int cell ) const; + bool IsWalled( int cell ) const; + float GetStepCost( int cell ) const; + + struct OpenCell_t + { + float estimate; + int cell; + }; + + static bool IsFartherFromGoal( const OpenCell_t &a, const OpenCell_t &b ) { return a.estimate > b.estimate; } + + int GetCell( const Vector &pos ) const; + int GetNearestOnMesh( int cell ) const; + Vector2D GetCellCenter( int cell ) const; + Vector GetCellFloor( int cell ) const; + bool CanStep( int from, int to ) const; + bool IsLineClear( int from, int to ) const; + + INextBot *m_bot; + int m_team; + Vector2D m_origin; + float m_cellSize; + int m_width; + int m_height; + float m_floorTop; + float m_floorRange; + + // filled in as cells are first looked at + mutable CUtlVector< unsigned char > m_state; + mutable CUtlVector< float > m_floor; + mutable CUtlVector< const CNavArea * > m_area; +}; + + +//---------------------------------------------------------------------------------------------------------------- +CPropDetourGrid::CPropDetourGrid( INextBot *bot, const Vector2D &lo, const Vector2D &hi, float floorLo, float floorHi ) +{ + m_bot = bot; + m_origin = lo; + + // coarser cells for a big region, so the search stays cheap + m_cellSize = PROP_DETOUR_GRID_CELL_SIZE; + while ( ( hi.x - lo.x ) / m_cellSize > PROP_DETOUR_GRID_MAX_CELLS_PER_SIDE || ( hi.y - lo.y ) / m_cellSize > PROP_DETOUR_GRID_MAX_CELLS_PER_SIDE ) + { + m_cellSize *= 1.5f; + } + + m_width = (int)ceilf( ( hi.x - lo.x ) / m_cellSize ); + m_height = (int)ceilf( ( hi.y - lo.y ) / m_cellSize ); + + const int cellCount = m_width * m_height; + m_state.SetCount( cellCount ); + m_floor.SetCount( cellCount ); + m_area.SetCount( cellCount ); + for ( int cell = 0; cell < cellCount; ++cell ) + { + m_state[ cell ] = CELL_UNKNOWN; + } + + m_team = bot->GetEntity()->GetTeamNumber(); + m_floorTop = floorHi + PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE; + m_floorRange = m_floorTop - ( floorLo - PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE ); +} + + +//---------------------------------------------------------------------------------------------------------------- +// The cell's state, looking up its nav area the first time: free on an area not blocked for the bot's team +CPropDetourGrid::CellState CPropDetourGrid::GetState( int cell ) const +{ + if ( m_state[ cell ] == CELL_UNKNOWN ) + { + const Vector2D center = GetCellCenter( cell ); + const CNavArea *area = TheNavMesh->GetNavArea( Vector( center.x, center.y, m_floorTop ), m_floorRange ); + + m_area[ cell ] = area; + m_state[ cell ] = ( area && !area->IsBlocked( m_team ) ) ? CELL_FREE : CELL_OFF_MESH; + m_floor[ cell ] = area ? area->GetZ( center.x, center.y ) : 0.0f; + } + + return (CellState)m_state[ cell ]; +} + + +//---------------------------------------------------------------------------------------------------------------- +// The cell is off the mesh or under a prop the bot cannot shove: nothing gets past it +bool CPropDetourGrid::IsWalled( int cell ) const +{ + const CellState state = GetState( cell ); + return state == CELL_OFF_MESH || state == CELL_OCCUPIED; +} + + +//---------------------------------------------------------------------------------------------------------------- +// What stepping into the cell costs: a light prop's cell costs a little more than a free one, a heavy prop's a lot +float CPropDetourGrid::GetStepCost( int cell ) const +{ + switch ( GetState( cell ) ) + { + case CELL_OCCUPIED: + return PROP_DETOUR_OCCUPIED_CELL_COST; + case CELL_PUSHABLE: + return PROP_DETOUR_PUSHABLE_CELL_COST; + default: + return PROP_DETOUR_FREE_CELL_COST; + } +} + + +//---------------------------------------------------------------------------------------------------------------- +// A cell is occupied when the bot's body standing there would touch a prop, now or soon, +// and pushable when every prop it would touch is light enough to shove +void CPropDetourGrid::MarkProps( const CUtlVector< PropObstacle_t > &obstacles ) +{ + const BodyBox_t body = GetBodyBox( m_bot ); + + // only a cell whose middle lies in a prop's footprint can touch the prop: visit those, not every cell + FOR_EACH_VEC( obstacles, i ) + { + const PropObstacle_t &obstacle = obstacles[ i ]; + const int x0 = MAX( 0, (int)floorf( ( obstacle.lo.x - m_origin.x ) / m_cellSize ) ); + const int x1 = MIN( m_width - 1, (int)floorf( ( obstacle.hi.x - m_origin.x ) / m_cellSize ) ); + const int y0 = MAX( 0, (int)floorf( ( obstacle.lo.y - m_origin.y ) / m_cellSize ) ); + const int y1 = MIN( m_height - 1, (int)floorf( ( obstacle.hi.y - m_origin.y ) / m_cellSize ) ); + for ( int y = y0; y <= y1; ++y ) + { + for ( int x = x0; x <= x1; ++x ) + { + const int cell = y * m_width + x; + const CellState state = GetState( cell ); + if ( state == CELL_OFF_MESH || state == CELL_OCCUPIED || ( state == CELL_PUSHABLE && obstacle.isPushable ) ) + { + continue; + } + + const Vector at = GetCellFloor( cell ); + if ( at.x < obstacle.lo.x || at.x > obstacle.hi.x || at.y < obstacle.lo.y || at.y > obstacle.hi.y ) + { + continue; + } + + float fraction; + if ( BodyMeetsProp( body, at, at, obstacle, &fraction ) ) + { + m_state[ cell ] = obstacle.isPushable ? CELL_PUSHABLE : CELL_OCCUPIED; + } + } + } + } +} + + +//---------------------------------------------------------------------------------------------------------------- +int CPropDetourGrid::GetCell( const Vector &pos ) const +{ + const int x = clamp( (int)( ( pos.x - m_origin.x ) / m_cellSize ), 0, m_width - 1 ); + const int y = clamp( (int)( ( pos.y - m_origin.y ) / m_cellSize ), 0, m_height - 1 ); + return y * m_width + x; +} + + +//---------------------------------------------------------------------------------------------------------------- +Vector2D CPropDetourGrid::GetCellCenter( int cell ) const +{ + const int x = cell % m_width; + const int y = cell / m_width; + return Vector2D( m_origin.x + ( x + 0.5f ) * m_cellSize, m_origin.y + ( y + 0.5f ) * m_cellSize ); +} + + +//---------------------------------------------------------------------------------------------------------------- +// The cell's middle is within a hull width of the bot's feet +bool CPropDetourGrid::IsNearFeet( int cell, const Vector &feet ) const +{ + const float hullWidth = m_bot->GetBodyInterface()->GetHullWidth(); + return ( GetCellCenter( cell ) - feet.AsVector2D() ).IsLengthLessThan( hullWidth ); +} + + +//---------------------------------------------------------------------------------------------------------------- +// The floor at the middle of the cell +Vector CPropDetourGrid::GetCellFloor( int cell ) const +{ + GetState( cell ); // looks the floor up with the state, the first time the cell is reached + const Vector2D center = GetCellCenter( cell ); + return Vector( center.x, center.y, m_floor[ cell ] ); +} + + +//---------------------------------------------------------------------------------------------------------------- +// The given cell if it is on the mesh, else the nearest one around it that is (-1 if none) +int CPropDetourGrid::GetNearestOnMesh( int cell ) const +{ + const int searchRadius = 2; + const int cx = cell % m_width; + const int cy = cell / m_width; + for ( int r = 0; r <= searchRadius; ++r ) + { + for ( int dy = -r; dy <= r; ++dy ) + { + for ( int dx = -r; dx <= r; ++dx ) + { + const int x = cx + dx; + const int y = cy + dy; + if ( x < 0 || y < 0 || x >= m_width || y >= m_height ) + { + continue; + } + + if ( GetState( y * m_width + x ) != CELL_OFF_MESH ) + { + return y * m_width + x; + } + } + } + } + + return -1; +} + + +//---------------------------------------------------------------------------------------------------------------- +// Walking from one cell to its neighbor stays on the mesh: the same area or a connected one, no higher than a step +// (two areas either side of a wall are both on the mesh, but not connected) +bool CPropDetourGrid::CanStep( int from, int to ) const +{ + if ( GetState( to ) == CELL_OFF_MESH || GetState( from ) == CELL_OFF_MESH ) + { + return false; + } + + if ( m_floor[ to ] - m_floor[ from ] > m_bot->GetLocomotionInterface()->GetStepHeight() ) + { + return false; + } + + return m_area[ from ] == m_area[ to ] || m_area[ from ]->IsConnected( m_area[ to ], NUM_DIRECTIONS ); +} + + +//---------------------------------------------------------------------------------------------------------------- +// A* over the cells, 8-connected. Occupied cells cost more than free ones rather than being walls, +// so a bot already under or against a prop finds its way out +bool CPropDetourGrid::FindRoute( const Vector &from, const Vector &to, CUtlVector< int > *route ) const +{ + const int start = GetNearestOnMesh( GetCell( from ) ); + const int goal = GetNearestOnMesh( GetCell( to ) ); + if ( start < 0 || goal < 0 ) + { + return false; + } + + const int cellCount = m_state.Count(); + CUtlVector< float > costSoFar; + CUtlVector< int > cameFrom; + CUtlVector< bool > isClosed; + costSoFar.SetCount( cellCount ); + cameFrom.SetCount( cellCount ); + isClosed.SetCount( cellCount ); + for ( int cell = 0; cell < cellCount; ++cell ) + { + costSoFar[ cell ] = FLT_MAX; + cameFrom[ cell ] = -1; + isClosed[ cell ] = false; + } + + const int gx = goal % m_width; + const int gy = goal / m_width; + + CUtlPriorityQueue< OpenCell_t > open( 0, 0, IsFartherFromGoal ); + costSoFar[ start ] = 0.0f; + open.Insert( { 0.0f, start } ); + + while ( open.Count() ) + { + const int cell = open.ElementAtHead().cell; + open.RemoveAtHead(); + if ( isClosed[ cell ] ) + { + continue; + } + + isClosed[ cell ] = true; + + if ( cell == goal ) + { + for ( int at = goal; at >= 0; at = cameFrom[ at ] ) + { + route->AddToHead( at ); + } + + return true; + } + + const int cx = cell % m_width; + const int cy = cell / m_width; + for ( int dy = -1; dy <= 1; ++dy ) + { + for ( int dx = -1; dx <= 1; ++dx ) + { + const int x = cx + dx; + const int y = cy + dy; + if ( ( dx == 0 && dy == 0 ) || x < 0 || y < 0 || x >= m_width || y >= m_height ) + { + continue; + } + + const int next = y * m_width + x; + if ( isClosed[ next ] || !CanStep( cell, next ) ) + { + continue; + } + + // no cutting a corner past a cell off the mesh or a prop the bot cannot shove: + // two props that touch at a corner leave no gap, however the cells fall + const bool isDiagonal = ( dx != 0 && dy != 0 ); + if ( isDiagonal && ( IsWalled( cy * m_width + x ) || IsWalled( y * m_width + cx ) ) ) + { + continue; + } + + const float stepLength = isDiagonal ? PROP_DETOUR_DIAGONAL_STEP_LENGTH : 1.0f; + const float stepCost = stepLength * GetStepCost( next ); + const float cost = costSoFar[ cell ] + stepCost; + if ( cost >= costSoFar[ next ] ) + { + continue; + } + + costSoFar[ next ] = cost; + cameFrom[ next ] = cell; + const float remaining = FastSqrt( (float)( ( x - gx ) * ( x - gx ) + ( y - gy ) * ( y - gy ) ) ); + open.Insert( { cost + remaining, next } ); + } + } + } + + return false; +} + + +//---------------------------------------------------------------------------------------------------------------- +// The straight grid line between two cells crosses only free cells the bot can step between +bool CPropDetourGrid::IsLineClear( int from, int to ) const +{ + const int ax = from % m_width; + const int ay = from / m_width; + const int bx = to % m_width; + const int by = to / m_width; + const int steps = 2 * MAX( abs( bx - ax ), abs( by - ay ) ); + + int previous = from; + for ( int k = 1; k <= steps; ++k ) + { + const float t = (float)k / steps; + const int x = (int)( ax + ( bx - ax ) * t + 0.5f ); + const int y = (int)( ay + ( by - ay ) * t + 0.5f ); + const int cell = y * m_width + x; + if ( cell == previous ) + { + continue; + } + + if ( GetState( cell ) != CELL_FREE || !CanStep( previous, cell ) ) + { + return false; + } + + // a diagonal step cuts no corner past a cell off the mesh or a prop the bot cannot shove, as in the search + const int px = previous % m_width; + const int py = previous / m_width; + if ( px != x && py != y && ( IsWalled( py * m_width + x ) || IsWalled( y * m_width + px ) ) ) + { + return false; + } + + previous = cell; + } + + return true; +} + + +//---------------------------------------------------------------------------------------------------------------- +// Keep only the cells of the route a straight line cannot skip +void CPropDetourGrid::Straighten( const CUtlVector< int > &route, CUtlVector< Vector > *waypoints ) const +{ + // a route of one cell: the bot stands in the rejoin cell, which is then the one waypoint + if ( route.Count() == 1 ) + { + waypoints->AddToTail( GetCellFloor( route[ 0 ] ) ); + return; + } + + int anchor = 0; + for ( int k = 1; k < route.Count(); ++k ) + { + const bool isLast = ( k == route.Count() - 1 ); + if ( isLast || IsOccupied( route[ anchor ] ) || !IsLineClear( route[ anchor ], route[ k + 1 ] ) ) + { + waypoints->AddToTail( GetCellFloor( route[ k ] ) ); + anchor = k; + } + } +} + + +//---------------------------------------------------------------------------------------------------------------- +// A route from the bot to the rejoin point as waypoints: +// past the prop cells by the bot's feet, a route through a prop is no way around it +static DetourResult FindDetour( const CPropDetourGrid &grid, const Vector &from, const Vector &rejoin, PushableRoute pushableRoute, + CUtlVector< Vector > *waypoints ) +{ + CUtlVector< int > route; + if ( !grid.FindRoute( from, rejoin, &route ) ) + { + return DETOUR_NONE; + } + + // the route may start through the prop cells by the bot's feet, so a bot under or against a prop gets out, + // but no further: a bot against a wall of props would otherwise route through the whole wall + int k = 0; + while ( k < route.Count() && grid.IsOccupied( route[ k ] ) && grid.IsNearFeet( route[ k ], from ) ) + { + ++k; + } + + for ( ; k < route.Count(); ++k ) + { + if ( grid.IsOccupied( route[ k ] ) ) + { + return DETOUR_NONE; + } + } + + // with only light props in the way, a route through them is no detour, + // and as a long run of their cells costs little, only the route's first cells count as where the bot starts + if ( pushableRoute == PUSHABLE_ROUTE_IS_NO_DETOUR ) + { + int start = 0; + while ( start < route.Count() && start < PROP_DETOUR_ROUTE_START_CELLS && ( grid.IsOccupied( route[ start ] ) || grid.IsPushableCell( route[ start ] ) ) ) + { + ++start; + } + + for ( k = start; k < route.Count(); ++k ) + { + if ( grid.IsPushableCell( route[ k ] ) ) + { + return DETOUR_THROUGH_PUSHABLE; + } + } + } + + grid.Straighten( route, waypoints ); + return ( waypoints->Count() > 0 ) ? DETOUR_FOUND : DETOUR_NONE; +} + + +//---------------------------------------------------------------------------------------------------------------- +// Search the region for a way from the bot's feet to the rejoin point, around every prop on it +static DetourResult SearchRegion( INextBot *bot, const Vector2D &lo, const Vector2D &hi, float floorLo, float floorHi, + const Vector &rejoin, PushableRoute pushableRoute, CUtlVector< Vector > *waypoints ) +{ + CUtlVector< PropObstacle_t > obstacles; + CollectProps( bot, Vector( lo.x, lo.y, floorLo ), Vector( hi.x, hi.y, floorHi ), &obstacles ); + + CPropDetourGrid grid( bot, lo, hi, floorLo, floorHi ); + grid.MarkProps( obstacles ); + return FindDetour( grid, bot->GetLocomotionInterface()->GetFeet(), rejoin, pushableRoute, waypoints ); +} + + +//---------------------------------------------------------------------------------------------------------------- +// Grow the region over every prop it touches, with a margin around each so the way around the prop is in it too, +// as long as the region stays within the wide search's size +static void GrowOverProps( const CUtlVector< PropObstacle_t > &obstacles, Vector2D *regionLo, Vector2D *regionHi ) +{ + const Vector2D margin( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN ); + for ( ;; ) + { + // the prop that grows the region least goes in first, + // so the near end of a long row is in before its far end uses up the size + Vector2D bestLo = *regionLo; + Vector2D bestHi = *regionHi; + float bestArea = FLT_MAX; + FOR_EACH_VEC( obstacles, i ) + { + const PropObstacle_t &obstacle = obstacles[ i ]; + const bool isTouching = obstacle.hi.x > regionLo->x && obstacle.lo.x < regionHi->x + && obstacle.hi.y > regionLo->y && obstacle.lo.y < regionHi->y; + if ( !isTouching ) + { + continue; + } + + const Vector2D lo = regionLo->Min( obstacle.lo - margin ); + const Vector2D hi = regionHi->Max( obstacle.hi + margin ); + const Vector2D size = hi - lo; + if ( ( lo == *regionLo && hi == *regionHi ) || size.x > PROP_DETOUR_WIDE_SEARCH_MAX_SIZE || size.y > PROP_DETOUR_WIDE_SEARCH_MAX_SIZE ) + { + continue; + } + + if ( size.x * size.y < bestArea ) + { + bestArea = size.x * size.y; + bestLo = lo; + bestHi = hi; + } + } + + if ( bestArea == FLT_MAX ) + { + return; + } + + *regionLo = bestLo; + *regionHi = bestHi; + } +} + + +//---------------------------------------------------------------------------------------------------------------- +// One wide search a server tick between all bots, so a crowd of blocked bots cannot stall a frame: +// return true if this tick's is still free, and take it +static int s_wideSearchTick = -1; +static bool ClaimWideSearch() +{ + if ( s_wideSearchTick == gpGlobals->tickcount ) + { + return false; + } + + s_wideSearchTick = gpGlobals->tickcount; + return true; +} + + +//---------------------------------------------------------------------------------------------------------------- +// With no way around near the path, as past a wall of props that runs on beyond the ones the path crosses, +// search again over a region grown over the props it touches, and on success hand back that region for the replans +static bool FindWideDetour( INextBot *bot, float floorLo, float floorHi, const Vector &rejoin, PushableRoute pushableRoute, + Vector2D *regionLo, Vector2D *regionHi, CUtlVector< Vector > *waypoints ) +{ + // the region grows to at most the wide search's size either way from the near one, + // so every prop it could reach lies in this box: twice that size less the near region a side, and at least the near region + const Vector2D reach( PROP_DETOUR_WIDE_SEARCH_MAX_SIZE, PROP_DETOUR_WIDE_SEARCH_MAX_SIZE ); + const Vector2D reachLo = ( *regionHi - reach ).Min( *regionLo ); + const Vector2D reachHi = ( *regionLo + reach ).Max( *regionHi ); + + // every prop in the box within the floor's height band costs a sight trace, before the grown region is known + CUtlVector< PropObstacle_t > obstacles; + CollectProps( bot, Vector( reachLo.x, reachLo.y, floorLo ), Vector( reachHi.x, reachHi.y, floorHi ), &obstacles ); + + Vector2D lo = *regionLo; + Vector2D hi = *regionHi; + GrowOverProps( obstacles, &lo, &hi ); + if ( lo == *regionLo && hi == *regionHi ) + { + return false; + } + + CPropDetourGrid grid( bot, lo, hi, floorLo, floorHi ); + grid.MarkProps( obstacles ); + if ( FindDetour( grid, bot->GetLocomotionInterface()->GetFeet(), rejoin, pushableRoute, waypoints ) != DETOUR_FOUND ) + { + return false; + } + + *regionLo = lo; + *regionHi = hi; + return true; +} + + +//---------------------------------------------------------------------------------------------------------------- +CNEOBotPropDetour::CNEOBotPropDetour() +{ + Reset(); +} + + +//---------------------------------------------------------------------------------------------------------------- +void CNEOBotPropDetour::Reset() +{ + m_waypoints.RemoveAll(); + m_pathGoal = NULL; + m_rejoinGoal = NULL; + m_resumeGoal = NULL; + m_isWide = false; + m_isPathPushable = false; + m_legStart = vec3_origin; + m_restingPropCount = OBSTACLE_PROPS_MOVING; + m_searchAgeTimer.Invalidate(); +} + + +//---------------------------------------------------------------------------------------------------------------- +float CNEOBotPropDetour::Plan( INextBot *bot, const PropDetourRequest_t &request, float lookInterval ) +{ + const Vector2D margin( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN ); + Vector2D regionLo = request.regionLo - margin; + Vector2D regionHi = request.regionHi + margin; + + // the region reaches past the props looked for around the path, by the footprints of those it crosses, + // so the search looks for every prop on it again, as a replan does + bool isWide = false; + const PushableRoute pushableRoute = request.isPathPushable ? PUSHABLE_ROUTE_IS_NO_DETOUR : PUSHABLE_ROUTE_IS_DETOUR; + const DetourResult result = SearchRegion( bot, regionLo, regionHi, request.floorLo, request.floorHi, request.rejoin, pushableRoute, &m_waypoints ); + if ( result == DETOUR_THROUGH_PUSHABLE ) + { + // the bot keeps its path through props it can shove, so no wide search for a way around them + return PROP_DETOUR_PUSH_THROUGH_RETRY_INTERVAL; + } + + if ( result == DETOUR_NONE ) + { + // another bot had this tick's wide search: plan again within a look interval, + // at a random tick so blocked bots take turns instead of all planning again at the next one + if ( !ClaimWideSearch() ) + { + return RandomFloat( gpGlobals->interval_per_tick, lookInterval ); + } + + if ( !FindWideDetour( bot, request.floorLo, request.floorHi, request.rejoin, pushableRoute, ®ionLo, ®ionHi, &m_waypoints ) ) + { + // a bot pushing a prop with no way around looks again less often: nothing changes quickly + return PROP_DETOUR_WIDE_SEARCH_RETRY_INTERVAL; + } + + isWide = true; + } + + // the detour keeps this target while the props and the bot move + m_rejoin = request.rejoin; + m_rejoinGoal = request.rejoinGoal; + m_resumeGoal = request.resumeGoal; + m_regionLo = regionLo; + m_regionHi = regionHi; + m_floorLo = request.floorLo; + m_floorHi = request.floorHi; + m_isWide = isWide; + m_isPathPushable = request.isPathPushable; + NoteSearch( bot->GetLocomotionInterface()->GetFeet() ); + return 0.0f; +} + + +//---------------------------------------------------------------------------------------------------------------- +bool CNEOBotPropDetour::Update( INextBot *bot, bool isSearchDue ) +{ + m_pathGoal = NULL; + + bool isWaitingForTick = false; + if ( isSearchDue && IsDetouring() ) + { + if ( m_isWide && IsLastSearchValid( bot ) ) + { + // nothing in the wide detour's region has moved and the bot keeps to its route: the detour holds + } + else if ( m_isWide && !ClaimWideSearch() ) + { + // a detour the wide search found searches its whole region again: wait for a free tick + isWaitingForTick = true; + } + else if ( !Replan( bot ) ) + { + // no way around from where the bot is now: take up the path again where the detour left it + m_waypoints.RemoveAll(); + m_pathGoal = m_resumeGoal; + } + } + + const Vector &feet = bot->GetLocomotionInterface()->GetFeet(); + while ( m_waypoints.Count() && ( m_waypoints[ 0 ].AsVector2D() - feet.AsVector2D() ).IsLengthLessThan( PROP_DETOUR_WAYPOINT_REACHED_RANGE ) ) + { + m_legStart = m_waypoints[ 0 ]; + m_waypoints.Remove( 0 ); + } + + if ( IsDetouring() ) + { + m_pathGoal = m_rejoinGoal; + } + + return isWaitingForTick; +} + + +//---------------------------------------------------------------------------------------------------------------- +// Search again for a way to the same rejoin point, from where the bot is and around the props where they are now +bool CNEOBotPropDetour::Replan( INextBot *bot ) +{ + // in the air, the bot keeps its detour until it lands + ILocomotion *mover = bot->GetLocomotionInterface(); + if ( !mover->IsOnGround() ) + { + return true; + } + + const Vector &feet = mover->GetFeet(); + const Vector2D margin( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN ); + const Vector2D regionLo = m_regionLo.Min( feet.AsVector2D() - margin ); + const Vector2D regionHi = m_regionHi.Max( feet.AsVector2D() + margin ); + const float floorLo = MIN( m_floorLo, feet.z ); + const float floorHi = MAX( m_floorHi, feet.z ); + + // the same question the plan answered: whether only light props were in the way + const PushableRoute pushableRoute = m_isPathPushable ? PUSHABLE_ROUTE_IS_NO_DETOUR : PUSHABLE_ROUTE_IS_DETOUR; + m_waypoints.RemoveAll(); + if ( SearchRegion( bot, regionLo, regionHi, floorLo, floorHi, m_rejoin, pushableRoute, &m_waypoints ) != DETOUR_FOUND ) + { + return false; + } + + NoteSearch( feet ); + return true; +} + + +//---------------------------------------------------------------------------------------------------------------- +// After a search that found a detour: note where the bot sets out from, and whether the props in the region rest +void CNEOBotPropDetour::NoteSearch( const Vector &feet ) +{ + // only a wide detour's replans are skipped while its props rest + if ( !m_isWide ) + { + return; + } + + m_legStart = feet; + m_restingPropCount = CountRestingProps( Vector( m_regionLo.x, m_regionLo.y, m_floorLo ), Vector( m_regionHi.x, m_regionHi.y, m_floorHi ) ); + m_searchAgeTimer.Start( PROP_DETOUR_AT_REST_REPLAN_INTERVAL ); +} + + +//---------------------------------------------------------------------------------------------------------------- +// The last search still holds: the props in the region rest where it saw them, and the bot keeps to its route +bool CNEOBotPropDetour::IsLastSearchValid( INextBot *bot ) const +{ + if ( m_restingPropCount == OBSTACLE_PROPS_MOVING || m_searchAgeTimer.IsElapsed() ) + { + return false; + } + + const Vector &feet = bot->GetLocomotionInterface()->GetFeet(); + if ( CalcDistanceToLineSegment2D( feet.AsVector2D(), m_legStart.AsVector2D(), m_waypoints[ 0 ].AsVector2D() ) > PROP_DETOUR_OFF_ROUTE_RANGE ) + { + return false; + } + + return CountRestingProps( Vector( m_regionLo.x, m_regionLo.y, m_floorLo ), Vector( m_regionHi.x, m_regionHi.y, m_floorHi ) ) == m_restingPropCount; +} diff --git a/src/game/server/neo/bot/neo_bot_prop_detour.h b/src/game/server/neo/bot/neo_bot_prop_detour.h new file mode 100644 index 0000000000..f01c0325d1 --- /dev/null +++ b/src/game/server/neo/bot/neo_bot_prop_detour.h @@ -0,0 +1,72 @@ +#pragma once + +#include "NextBot/Path/NextBotPath.h" + +class INextBot; + +//---------------------------------------------------------------------------------------------------------------- +// What a look ahead hands a detour to plan: where past the props in the way it takes up the path again, +// the path goals there and before the props, the floor box from the bot past them, and whether only light props were in the way +struct PropDetourRequest_t +{ + Vector rejoin; + const Path::Segment *rejoinGoal; + const Path::Segment *resumeGoal; + Vector2D regionLo; + Vector2D regionHi; + float floorLo; + float floorHi; + bool isPathPushable; +}; + +//---------------------------------------------------------------------------------------------------------------- +// A way around props in the bot's way, found by searching a grid laid over the mesh, +// and kept while the props and the bot move until the bot is past them +class CNEOBotPropDetour +{ +public: + CNEOBotPropDetour(); + + void Reset(); + + // Search for a way around, and return how long to wait before looking again, + // or 0 to look again at the look's own interval + float Plan( INextBot *bot, const PropDetourRequest_t &request, float lookInterval ); + + // Follow the detour, and search it again when the look is due: + // return true if that search has to wait for a free tick + bool Update( INextBot *bot, bool isSearchDue ); + + bool IsDetouring() const { return m_waypoints.Count() > 0; } + const Vector &GetMoveGoal() const { return m_waypoints[ 0 ]; } // where to move next while detouring + + // The path goal the follower should take, if any: the one past the props while detouring, + // and the one it had before the detour when the detour is given up + const Path::Segment *GetPathGoal() const { return m_pathGoal; } + +private: + bool Replan( INextBot *bot ); + void NoteSearch( const Vector &feet ); + bool IsLastSearchValid( INextBot *bot ) const; + + CUtlVector< Vector > m_waypoints; + const Path::Segment *m_pathGoal; + + // what a detour keeps while the props and the bot move: where it takes up the path again, + // the path goal past that point and the one before the detour, and the floor it may use + Vector m_rejoin; + const Path::Segment *m_rejoinGoal; + const Path::Segment *m_resumeGoal; + Vector2D m_regionLo; + Vector2D m_regionHi; + float m_floorLo; + float m_floorHi; + bool m_isWide; // found by the wide search, so its replans share the one wide search a tick + bool m_isPathPushable; // every prop in the way on the path was light, so a route through light props is no detour + + // what the last search saw, so the detour is searched again only when that changes or the search grows old: + // where the bot set out for its next waypoint, and how many props in the region rested (OBSTACLE_PROPS_MOVING if any was awake) + Vector m_legStart; + int m_restingPropCount; + CountdownTimer m_searchAgeTimer; +};