From e600c5a825c83d0a818c02dbafd8dce178b0beee Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 4 Oct 2026 02:28:59 -0600 Subject: [PATCH 1/8] Steer bots around movable props in their way A navmesh is generated without entities, so it cannot know where a physics prop or an animated prop's bone followers stand. A bot's path runs straight through such a prop, the stock Avoid probe leans away only from props narrower than the bot, and the bot walks into the prop until the stuck monitor fires. CNEOBotPropDetour, a member of PathFollower under NEO, looks along the bot's path every 0.25 s for movable props it can see and sweeps its body against them. When one is in the way, it searches a grid of cells over the nav mesh around the props for a route to a point on the path past them, and the follower steers through that route's waypoints, heading for the path segment past the props. Light props that physics moves freely are shoved as a player does: a bot walks around one where there is room, and keeps its path through them when only light props are in the way. When the props run on past the ones the path crosses, as a wall or a row does, the region grows over them to 1536 u, one such wide search a server tick between all bots. A detour is searched again to follow props that move; a wide one only when its props wake or the bot leaves its route. With no way around, the bot keeps the path it had, as before. --- src/game/server/CMakeLists.txt | 2 + .../server/NextBot/Path/NextBotPathFollow.cpp | 37 +- .../server/NextBot/Path/NextBotPathFollow.h | 4 + .../server/neo/bot/neo_bot_prop_detour.cpp | 1273 +++++++++++++++++ src/game/server/neo/bot/neo_bot_prop_detour.h | 54 + 5 files changed, 1369 insertions(+), 1 deletion(-) create mode 100644 src/game/server/neo/bot/neo_bot_prop_detour.cpp create mode 100644 src/game/server/neo/bot/neo_bot_prop_detour.h diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 5c18f81818..4c8a113b68 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1515,6 +1515,7 @@ 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_prop_detour.cpp neo/bot/neo_bot_path_reservation.cpp neo/bot/neo_bot_body.cpp neo/bot/neo_bot_locomotion.cpp @@ -1577,6 +1578,7 @@ 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_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..94eabbd60a 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_propDetour.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_propDetour.Reset(); +#endif } @@ -684,6 +692,19 @@ void PathFollower::Update( INextBot *bot ) return; } +#ifdef NEO + // 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_propDetour.Update( bot, *this ); + const Path::Segment *detourPathGoal = m_propDetour.GetPathGoal(); + if ( detourPathGoal ) + { + m_goal = detourPathGoal; + } + + const bool isDetouring = m_propDetour.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_propDetour.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..63bb71d696 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_prop_detour.h" +#endif class INextBot; class ILocomotion; @@ -83,6 +86,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; + CNEOBotPropDetour m_propDetour; // steers around props the nav mesh does not know about #endif float m_goalTolerance; 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..3dbbdab2bf --- /dev/null +++ b/src/game/server/neo/bot/neo_bot_prop_detour.cpp @@ -0,0 +1,1273 @@ +#include "cbase.h" +#include "nav_mesh.h" +#include "NextBot.h" +#include "NextBot/Path/NextBotPathFollow.h" +#include "NextBotLocomotionInterface.h" +#include "NextBotBodyInterface.h" +#include "neo_bot_prop_detour.h" +#include "tier1/utlpriorityqueue.h" +#include "vphysics_interface.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 +constexpr float PROP_DETOUR_REPLAN_INTERVAL = 0.25f; +// 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; +// What a count of the props resting in a region says when one of them is awake +constexpr int PROP_DETOUR_PROPS_MOVING = -1; + +// 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; + +// How far along the path props are looked for, and how much further the detour may rejoin it +constexpr float PROP_DETOUR_LOOK_AHEAD_RANGE = 256.0f; +constexpr float PROP_DETOUR_REJOIN_EXTRA_RANGE = 128.0f; + +// The detour rejoins the path this far past the last prop in the way +constexpr float PROP_DETOUR_REJOIN_PAST_PROP = 32.0f; + +// Floor around the path and the props that a detour may use +constexpr float PROP_DETOUR_GRID_MARGIN = 48.0f; +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; + +// A detour stays on floor this close in height to the bot's feet or to where it rejoins the path +constexpr float PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE = 40.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 prop this light that physics moves is shoved out of the way, as a player does: +// its cells cost this many free cells, so a bot walks around one chair where there is room, +constexpr float PROP_DETOUR_PUSHABLE_PROP_MAX_MASS = 200.0f; +constexpr float PROP_DETOUR_PUSHABLE_CELL_COST = 4.0f; +// and when every prop in the way on the path is that 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; + +// A moving prop is kept clear of where it will be within this time, at its current velocity +constexpr float PROP_DETOUR_MOTION_PREDICT_TIME = 1.0f; +constexpr float PROP_DETOUR_MOVING_PROP_MIN_SPEED = 10.0f; + +// Props are looked for up to this high above the path, so one coming down is seen in time +constexpr float PROP_DETOUR_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 PROP_DETOUR_SMALL_PROP_SIZE( 16.0f, 16.0f, 40.0f ); + +constexpr float PROP_DETOUR_DIAGONAL_STEP_LENGTH = 1.41421356f; + +// The body box is this much narrower than the hull, so a prop the bot only brushes is not in its way +constexpr float PROP_DETOUR_BODY_CLEARANCE = 1.0f; +} + +//---------------------------------------------------------------------------------------------------------------- +namespace +{ + struct PropObstacle_t + { + CBaseEntity *entity; + Vector sweep; // how far it moves within PROP_DETOUR_MOTION_PREDICT_TIME + Vector2D lo, hi; // footprint over that time, grown by a hull half-width + bool isPushable; // light enough to shove aside + }; + + // 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 + }; + + // The bot's body as a box: from a step over the floor to its standing height + struct BodyBox_t + { + Vector mins, maxs; + }; + + // Return true if the line from a to b crosses the box, with the entry and exit fractions + 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; + } +} + + +//---------------------------------------------------------------------------------------------------------------- +// 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 solid to players a spatial partition query meets, however much else lies there: + // a box query into a fixed-size list fills up with everything it finds, cans, weapons and triggers alike + class CMovablePropEnum : public IPartitionEnumerator + { + public: + explicit CMovablePropEnum( CUtlVector< CBaseEntity * > *props ) : m_props( props ) {} + + virtual IterationRetval_t EnumElement( IHandleEntity *handleEntity ) + { + CBaseEntity *entity = gEntList.GetBaseEntity( handleEntity->GetRefEHandle() ); + if ( entity && IsMovableProp( entity ) && IsSolidToPlayers( entity ) ) + { + m_props->AddToTail( entity ); + } + + return ITERATION_CONTINUE; + } + + private: + CUtlVector< CBaseEntity * > *m_props; + }; +} + + +//---------------------------------------------------------------------------------------------------------------- +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( PROP_DETOUR_MOVING_PROP_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() <= PROP_DETOUR_PUSHABLE_PROP_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(); +} + + +//---------------------------------------------------------------------------------------------------------------- +static BodyBox_t GetBodyBox( INextBot *bot ) +{ + const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth() - PROP_DETOUR_BODY_CLEARANCE; + + BodyBox_t box; + box.mins.Init( -halfWidth, -halfWidth, bot->GetLocomotionInterface()->GetStepHeight() ); + box.maxs.Init( halfWidth, halfWidth, bot->GetBodyInterface()->GetStandHullHeight() ); + return box; +} + + +//---------------------------------------------------------------------------------------------------------------- +// Return true if a body moving from 'from' to 'to' touches the prop, now or where it will be +static bool BodyMeetsProp( const BodyBox_t &body, const Vector &from, const Vector &to, const PropObstacle_t &obstacle, float *fraction ) +{ + *fraction = 1.0f; + + // moving the body back along the prop's motion is the same as moving the prop forward + const int tests = obstacle.sweep.IsZero() ? 1 : 2; + for ( int i = 0; i < tests; ++i ) + { + const Vector shift = ( i == 0 ) ? vec3_origin : -obstacle.sweep; + + Ray_t ray; + ray.Init( from + shift, to + shift, body.mins, body.maxs ); + + trace_t result; + enginetrace->ClipRayToEntity( ray, MASK_PLAYERSOLID, obstacle.entity, &result ); + if ( result.startsolid ) + { + *fraction = 0.0f; + return true; + } + + *fraction = MIN( *fraction, result.fraction ); + } + + return *fraction < 1.0f; +} + + +//---------------------------------------------------------------------------------------------------------------- +// The path ahead as a line from the bot's feet, over walked segments only, up to the range, +// with 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 ) +{ + line->AddToTail( feet ); + 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(); + 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(); +} + + +//---------------------------------------------------------------------------------------------------------------- +// Every movable prop solid to players around the floor in the box, from below it up to the headroom above it +static void FindMovableProps( const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *props ) +{ + const Vector queryLo = floorLo - Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE ); + const Vector queryHi = floorHi + Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_PROP_QUERY_HEADROOM ); + + CMovablePropEnum propEnum( props ); + partition->EnumerateElementsInBox( PARTITION_ENGINE_NON_STATIC_EDICTS, queryLo, queryHi, false, &propEnum ); +} + + +//---------------------------------------------------------------------------------------------------------------- +// Movable props the bot can see that stand, or will soon stand, in its body space over the floor in the box +static void CollectProps( INextBot *bot, const Vector &floorLo, const Vector &floorHi, CUtlVector< PropObstacle_t > *obstacles ) +{ + const BodyBox_t body = GetBodyBox( bot ); + const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth(); + + CUtlVector< CBaseEntity * > props; + FindMovableProps( floorLo, floorHi, &props ); + 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 < PROP_DETOUR_SMALL_PROP_SIZE.x && size.y < PROP_DETOUR_SMALL_PROP_SIZE.y && size.z < PROP_DETOUR_SMALL_PROP_SIZE.z ) + { + continue; + } + + const Vector sweep = GetPropVelocity( entity ) * PROP_DETOUR_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 ); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +// How many movable props stand around the floor in the box, or PROP_DETOUR_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 +static 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 PROP_DETOUR_PROPS_MOVING; + } + } + + return props.Count(); +} + + +//---------------------------------------------------------------------------------------------------------------- +// 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_replanTimer.Invalidate(); + m_waypoints.RemoveAll(); + m_pathGoal = NULL; + m_rejoinGoal = NULL; + m_resumeGoal = NULL; + m_isWide = false; + m_isPathPushable = false; + m_legStart = vec3_origin; + m_restingPropCount = PROP_DETOUR_PROPS_MOVING; + m_searchAgeTimer.Invalidate(); +} + + +//---------------------------------------------------------------------------------------------------------------- +void CNEOBotPropDetour::Update( INextBot *bot, const PathFollower &path ) +{ + m_pathGoal = NULL; + + if ( m_replanTimer.IsElapsed() ) + { + m_replanTimer.Start( PROP_DETOUR_REPLAN_INTERVAL ); + + if ( !IsDetouring() ) + { + Plan( bot, path ); + } + else 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 + m_replanTimer.Invalidate(); + } + 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; + } +} + + +//---------------------------------------------------------------------------------------------------------------- +// Look for props in the way on the path ahead, and when there are, a way past them on the mesh +void CNEOBotPropDetour::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(), PROP_DETOUR_LOOK_AHEAD_RANGE + PROP_DETOUR_REJOIN_EXTRA_RANGE, &line, &segments ); + if ( line.Count() < 2 ) + { + return; + } + + Vector lineLo = line[ 0 ]; + Vector lineHi = line[ 0 ]; + for ( int i = 1; i < line.Count(); ++i ) + { + VectorMin( lineLo, line[ i ], lineLo ); + VectorMax( lineHi, line[ i ], lineHi ); + } + + CUtlVector< PropObstacle_t > obstacles; + CollectProps( bot, lineLo, lineHi, &obstacles ); + if ( obstacles.Count() == 0 ) + { + return; + } + + // which props the body would meet along the path, and where it would be past all of them + const BodyBox_t body = GetBodyBox( bot ); + 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 < PROP_DETOUR_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 < PROP_DETOUR_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 + PROP_DETOUR_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; + } + + regionLo = regionLo.Min( rejoin.AsVector2D() ) - Vector2D( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN ); + regionHi = regionHi.Max( rejoin.AsVector2D() ) + Vector2D( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_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 = isPathPushable ? PUSHABLE_ROUTE_IS_NO_DETOUR : PUSHABLE_ROUTE_IS_DETOUR; + const DetourResult result = SearchRegion( bot, regionLo, regionHi, floorLo, floorHi, 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 + m_replanTimer.Start( PROP_DETOUR_PUSH_THROUGH_RETRY_INTERVAL ); + return; + } + + if ( result == DETOUR_NONE ) + { + // another bot had this tick's wide search: plan again within a replan interval, + // at a random tick so blocked bots take turns instead of all planning again at the next one + if ( !ClaimWideSearch() ) + { + m_replanTimer.Start( RandomFloat( gpGlobals->interval_per_tick, PROP_DETOUR_REPLAN_INTERVAL ) ); + return; + } + + if ( !FindWideDetour( bot, floorLo, floorHi, rejoin, pushableRoute, ®ionLo, ®ionHi, &m_waypoints ) ) + { + // a bot pushing a prop with no way around looks again less often: nothing changes quickly + m_replanTimer.Start( PROP_DETOUR_WIDE_SEARCH_RETRY_INTERVAL ); + return; + } + + isWide = true; + } + + // the detour keeps this target while the props and the bot move + m_rejoin = rejoin; + m_rejoinGoal = segments[ rejoinLegEnd ]; + m_resumeGoal = goal; + m_regionLo = regionLo; + m_regionHi = regionHi; + m_floorLo = floorLo; + m_floorHi = floorHi; + m_isWide = isWide; + m_isPathPushable = isPathPushable; + NoteSearch( mover->GetFeet() ); +} + + +//---------------------------------------------------------------------------------------------------------------- +// 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 == PROP_DETOUR_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..66ca4c30e8 --- /dev/null +++ b/src/game/server/neo/bot/neo_bot_prop_detour.h @@ -0,0 +1,54 @@ +#pragma once + +#include "NextBot/Path/NextBotPath.h" + +class INextBot; +class PathFollower; + +//---------------------------------------------------------------------------------------------------------------- +// Steers a bot around props the nav mesh cannot know about, which physics or an animation moves, +// by searching a grid laid over the mesh on the path ahead for a way past the ones it sees +class CNEOBotPropDetour +{ +public: + CNEOBotPropDetour(); + + void Reset(); + + void Update( INextBot *bot, const PathFollower &path ); + + 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: + void Plan( INextBot *bot, const PathFollower &path ); + bool Replan( INextBot *bot ); + void NoteSearch( const Vector &feet ); + bool IsLastSearchValid( INextBot *bot ) const; + + CountdownTimer m_replanTimer; + 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 (PROP_DETOUR_PROPS_MOVING if any was awake) + Vector m_legStart; + int m_restingPropCount; + CountdownTimer m_searchAgeTimer; +}; From 819c3fdd0495d2ae370f59b926ba0338b7b9b471 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 4 Oct 2026 10:47:46 -0600 Subject: [PATCH 2/8] Find breakables in the prop look ahead The breakable check traced the bot's hull to its current path goal on every update. The prop detour already looks along the path ahead; its partition query now also keeps breakables, and the first one the body would meet within the look-ahead range is what the check returns, so the separate hull trace goes. The look runs every 0.3 s instead of 0.25 s. While detouring or waiting to retry a plan, breakables are looked for alone on the same cadence. --- .../server/NextBot/Path/NextBotPathFollow.h | 4 + .../behavior/neo_bot_path_clear_breakable.cpp | 27 +--- .../server/neo/bot/neo_bot_prop_detour.cpp | 124 ++++++++++++++++-- src/game/server/neo/bot/neo_bot_prop_detour.h | 6 + 4 files changed, 128 insertions(+), 33 deletions(-) diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.h b/src/game/server/NextBot/Path/NextBotPathFollow.h index 63bb71d696..7f213cb138 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.h +++ b/src/game/server/NextBot/Path/NextBotPathFollow.h @@ -45,6 +45,10 @@ class PathFollower : public Path Path::ResultType GetResult() const { return m_result; } +#ifdef NEO + CBaseEntity *GetBreakableInWay() const { return m_propDetour.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; 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_prop_detour.cpp b/src/game/server/neo/bot/neo_bot_prop_detour.cpp index 3dbbdab2bf..9dae4670ec 100644 --- a/src/game/server/neo/bot/neo_bot_prop_detour.cpp +++ b/src/game/server/neo/bot/neo_bot_prop_detour.cpp @@ -14,7 +14,8 @@ namespace { // How often the path ahead is checked, so a prop that is pushed or animates is followed -constexpr float PROP_DETOUR_REPLAN_INTERVAL = 0.25f; +// and a breakable in the way is seen before the bot walks into it +constexpr float PROP_DETOUR_REPLAN_INTERVAL = 0.3f; // 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; @@ -174,25 +175,39 @@ static bool IsSolidToPlayers( CBaseEntity *entity ) namespace { // The movable props solid to players a spatial partition query meets, however much else lies there: - // a box query into a fixed-size list fills up with everything it finds, cans, weapons and triggers alike + // a box query into a fixed-size list fills up with everything it finds, cans, weapons and triggers alike. + // Given a list for them, it also keeps the solid entities the bot can break, such as glass class CMovablePropEnum : public IPartitionEnumerator { public: - explicit CMovablePropEnum( CUtlVector< CBaseEntity * > *props ) : m_props( props ) {} + CMovablePropEnum( CUtlVector< CBaseEntity * > *props, INextBot *bot, CUtlVector< CBaseEntity * > *breakables ) + : m_props( props ), m_bot( bot ), m_breakables( breakables ) {} virtual IterationRetval_t EnumElement( IHandleEntity *handleEntity ) { CBaseEntity *entity = gEntList.GetBaseEntity( handleEntity->GetRefEHandle() ); - if ( entity && IsMovableProp( entity ) && IsSolidToPlayers( entity ) ) + if ( !entity || !IsSolidToPlayers( entity ) ) + { + return ITERATION_CONTINUE; + } + + if ( IsMovableProp( entity ) ) { m_props->AddToTail( entity ); } + if ( m_breakables && m_bot->IsAbleToBreak( entity ) && entity->GetHealth() > 0 ) + { + m_breakables->AddToTail( entity ); + } + return ITERATION_CONTINUE; } private: CUtlVector< CBaseEntity * > *m_props; + INextBot *m_bot; + CUtlVector< CBaseEntity * > *m_breakables; }; } @@ -276,6 +291,49 @@ static bool BodyMeetsProp( const BodyBox_t &body, const Vector &from, const Vect } +//---------------------------------------------------------------------------------------------------------------- +// Return true if a body moving from 'from' to 'to' touches the entity where it stands +static 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; +} + + +//---------------------------------------------------------------------------------------------------------------- +// The breakable the body meets first walking the line, within the range props are looked for in: +// about as far as the next path goal usually lies, which is where a hull trace for breakables used to end +static CBaseEntity *FindBreakableInWay( const BodyBox_t &body, const CUtlVector< Vector > &line, const CUtlVector< CBaseEntity * > &breakables ) +{ + CBaseEntity *nearest = NULL; + float nearestDistance = PROP_DETOUR_LOOK_AHEAD_RANGE; + FOR_EACH_VEC( breakables, i ) + { + 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 ]; + nearestDistance = legStart + fraction * legLength; + break; + } + + legStart += legLength; + } + } + + return nearest; +} + + //---------------------------------------------------------------------------------------------------------------- // The path ahead as a line from the bot's feet, over walked segments only, up to the range, // with the segment each point of the line belongs to @@ -330,25 +388,28 @@ static Vector GetPointAlong( const CUtlVector< Vector > &line, float distance, i //---------------------------------------------------------------------------------------------------------------- // Every movable prop solid to players around the floor in the box, from below it up to the headroom above it -static void FindMovableProps( const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *props ) +static void FindMovableProps( const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *props, + INextBot *bot = NULL, CUtlVector< CBaseEntity * > *breakables = NULL ) { const Vector queryLo = floorLo - Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE ); const Vector queryHi = floorHi + Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_PROP_QUERY_HEADROOM ); - CMovablePropEnum propEnum( props ); + CMovablePropEnum propEnum( props, bot, breakables ); partition->EnumerateElementsInBox( PARTITION_ENGINE_NON_STATIC_EDICTS, queryLo, queryHi, false, &propEnum ); } //---------------------------------------------------------------------------------------------------------------- // Movable props the bot can see that stand, or will soon stand, in its body space over the floor in the box -static void CollectProps( INextBot *bot, const Vector &floorLo, const Vector &floorHi, CUtlVector< PropObstacle_t > *obstacles ) +// and, given a list for them, the breakables there +static void CollectProps( INextBot *bot, const Vector &floorLo, const Vector &floorHi, CUtlVector< PropObstacle_t > *obstacles, + CUtlVector< CBaseEntity * > *breakables = NULL ) { const BodyBox_t body = GetBodyBox( bot ); const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth(); CUtlVector< CBaseEntity * > props; - FindMovableProps( floorLo, floorHi, &props ); + FindMovableProps( floorLo, floorHi, &props, bot, breakables ); FOR_EACH_VEC( props, i ) { CBaseEntity *entity = props[ i ]; @@ -1022,6 +1083,8 @@ CNEOBotPropDetour::CNEOBotPropDetour() void CNEOBotPropDetour::Reset() { m_replanTimer.Invalidate(); + m_breakableTimer.Invalidate(); + m_breakable = NULL; m_waypoints.RemoveAll(); m_pathGoal = NULL; m_rejoinGoal = NULL; @@ -1064,6 +1127,12 @@ void CNEOBotPropDetour::Update( INextBot *bot, const PathFollower &path ) } } + // 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 ); + } + const Vector &feet = bot->GetLocomotionInterface()->GetFeet(); while ( m_waypoints.Count() && ( m_waypoints[ 0 ].AsVector2D() - feet.AsVector2D() ).IsLengthLessThan( PROP_DETOUR_WAYPOINT_REACHED_RANGE ) ) { @@ -1106,14 +1175,19 @@ void CNEOBotPropDetour::Plan( INextBot *bot, const PathFollower &path ) } CUtlVector< PropObstacle_t > obstacles; - CollectProps( bot, lineLo, lineHi, &obstacles ); + CUtlVector< CBaseEntity * > breakables; + CollectProps( bot, lineLo, lineHi, &obstacles, &breakables ); + + const BodyBox_t body = GetBodyBox( bot ); + m_breakable = FindBreakableInWay( body, line, breakables ); + m_breakableTimer.Start( PROP_DETOUR_REPLAN_INTERVAL ); + if ( obstacles.Count() == 0 ) { return; } // which props the body would meet along the path, and where it would be past all of them - const BodyBox_t body = GetBodyBox( bot ); float rejoinDistance = -1.0f; bool isPathPushable = true; Vector2D regionLo = line[ 0 ].AsVector2D(); @@ -1207,6 +1281,36 @@ void CNEOBotPropDetour::Plan( INextBot *bot, const PathFollower &path ) } +//---------------------------------------------------------------------------------------------------------------- +// Look along the path ahead for a breakable in the way, without the props +void CNEOBotPropDetour::LookForBreakable( INextBot *bot, const PathFollower &path ) +{ + m_breakableTimer.Start( PROP_DETOUR_REPLAN_INTERVAL ); + m_breakable = NULL; + + CUtlVector< Vector > line; + CUtlVector< const Path::Segment * > segments; + GetPathAhead( path, bot->GetLocomotionInterface()->GetFeet(), PROP_DETOUR_LOOK_AHEAD_RANGE, &line, &segments ); + if ( line.Count() < 2 ) + { + return; + } + + Vector lineLo = line[ 0 ]; + Vector lineHi = line[ 0 ]; + for ( int i = 1; i < line.Count(); ++i ) + { + VectorMin( lineLo, line[ i ], lineLo ); + VectorMax( lineHi, line[ i ], lineHi ); + } + + CUtlVector< CBaseEntity * > props; + CUtlVector< CBaseEntity * > breakables; + FindMovableProps( lineLo, lineHi, &props, bot, &breakables ); + m_breakable = FindBreakableInWay( GetBodyBox( bot ), line, breakables ); +} + + //---------------------------------------------------------------------------------------------------------------- // 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 ) diff --git a/src/game/server/neo/bot/neo_bot_prop_detour.h b/src/game/server/neo/bot/neo_bot_prop_detour.h index 66ca4c30e8..089d7cf0f6 100644 --- a/src/game/server/neo/bot/neo_bot_prop_detour.h +++ b/src/game/server/neo/bot/neo_bot_prop_detour.h @@ -24,13 +24,19 @@ class CNEOBotPropDetour // and the one it had before the detour when the detour is given up const Path::Segment *GetPathGoal() const { return m_pathGoal; } + // 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 ); bool Replan( INextBot *bot ); void NoteSearch( const Vector &feet ); bool IsLastSearchValid( INextBot *bot ) const; CountdownTimer m_replanTimer; + CountdownTimer m_breakableTimer; // the look for breakables, when no plan this often looks for them too + CHandle< CBaseEntity > m_breakable; CUtlVector< Vector > m_waypoints; const Path::Segment *m_pathGoal; From 001d8111e2ddaded1b59a229d781be1c4e4bdf6b Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 4 Oct 2026 10:53:13 -0600 Subject: [PATCH 3/8] Look for breakables over the nearest 128 u At the look-ahead range of 256 u bots found glass panes about 240 u away and stopped there to shoot them. 128 u is still beyond the distance a bot runs between two looks, and keeps the find near where the hull trace used to make it (65-73 u from the pane on the test maps), close enough for the knife. --- src/game/server/neo/bot/neo_bot_prop_detour.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/game/server/neo/bot/neo_bot_prop_detour.cpp b/src/game/server/neo/bot/neo_bot_prop_detour.cpp index 9dae4670ec..89858ee75e 100644 --- a/src/game/server/neo/bot/neo_bot_prop_detour.cpp +++ b/src/game/server/neo/bot/neo_bot_prop_detour.cpp @@ -31,6 +31,10 @@ constexpr float PROP_DETOUR_OFF_ROUTE_RANGE = 16.0f; constexpr float PROP_DETOUR_LOOK_AHEAD_RANGE = 256.0f; constexpr float PROP_DETOUR_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 PROP_DETOUR_BREAKABLE_RANGE = 128.0f; + // The detour rejoins the path this far past the last prop in the way constexpr float PROP_DETOUR_REJOIN_PAST_PROP = 32.0f; @@ -306,12 +310,11 @@ static bool BodyMeetsEntity( const BodyBox_t &body, const Vector &from, const Ve //---------------------------------------------------------------------------------------------------------------- -// The breakable the body meets first walking the line, within the range props are looked for in: -// about as far as the next path goal usually lies, which is where a hull trace for breakables used to end +// The breakable the body meets first walking the line, within PROP_DETOUR_BREAKABLE_RANGE static CBaseEntity *FindBreakableInWay( const BodyBox_t &body, const CUtlVector< Vector > &line, const CUtlVector< CBaseEntity * > &breakables ) { CBaseEntity *nearest = NULL; - float nearestDistance = PROP_DETOUR_LOOK_AHEAD_RANGE; + float nearestDistance = PROP_DETOUR_BREAKABLE_RANGE; FOR_EACH_VEC( breakables, i ) { float legStart = 0.0f; @@ -1290,7 +1293,7 @@ void CNEOBotPropDetour::LookForBreakable( INextBot *bot, const PathFollower &pat CUtlVector< Vector > line; CUtlVector< const Path::Segment * > segments; - GetPathAhead( path, bot->GetLocomotionInterface()->GetFeet(), PROP_DETOUR_LOOK_AHEAD_RANGE, &line, &segments ); + GetPathAhead( path, bot->GetLocomotionInterface()->GetFeet(), PROP_DETOUR_BREAKABLE_RANGE, &line, &segments ); if ( line.Count() < 2 ) { return; From ba09a4a2419190c7eb0b5900aa9e1c9c95c57d03 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 4 Oct 2026 11:36:51 -0600 Subject: [PATCH 4/8] Rename the prop detour to path obstacles The component now looks ahead for breakables as well as props, so it is named for that shared job: CNEOBotPathObstacles in neo_bot_path_obstacles.{h,cpp}, PathFollower::m_pathObstacles. The look's constants become PATH_OBSTACLE_*; the prop-only detour search keeps its prop-detour names. --- src/game/server/CMakeLists.txt | 4 +- .../server/NextBot/Path/NextBotPathFollow.cpp | 14 ++-- .../server/NextBot/Path/NextBotPathFollow.h | 6 +- ..._detour.cpp => neo_bot_path_obstacles.cpp} | 64 +++++++++---------- ...prop_detour.h => neo_bot_path_obstacles.h} | 11 ++-- 5 files changed, 50 insertions(+), 49 deletions(-) rename src/game/server/neo/bot/{neo_bot_prop_detour.cpp => neo_bot_path_obstacles.cpp} (96%) rename src/game/server/neo/bot/{neo_bot_prop_detour.h => neo_bot_path_obstacles.h} (84%) diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 4c8a113b68..f85a2575fd 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1515,7 +1515,7 @@ 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_prop_detour.cpp + neo/bot/neo_bot_path_obstacles.cpp neo/bot/neo_bot_path_reservation.cpp neo/bot/neo_bot_body.cpp neo/bot/neo_bot_locomotion.cpp @@ -1578,7 +1578,7 @@ 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_prop_detour.h + neo/bot/neo_bot_path_obstacles.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 94eabbd60a..d9c427bb1c 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.cpp +++ b/src/game/server/NextBot/Path/NextBotPathFollow.cpp @@ -104,7 +104,7 @@ void PathFollower::Invalidate( void ) m_hindrance = NULL; #ifdef NEO - m_propDetour.Reset(); + m_pathObstacles.Reset(); #endif } @@ -120,7 +120,7 @@ void PathFollower::OnPathChanged( INextBot *bot, Path::ResultType result ) m_result = result; #ifdef NEO - m_propDetour.Reset(); + m_pathObstacles.Reset(); #endif } @@ -693,16 +693,16 @@ void PathFollower::Update( INextBot *bot ) } #ifdef NEO - // walk around props in the way, which the nav mesh does not know about, + // 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_propDetour.Update( bot, *this ); - const Path::Segment *detourPathGoal = m_propDetour.GetPathGoal(); + m_pathObstacles.Update( bot, *this ); + const Path::Segment *detourPathGoal = m_pathObstacles.GetPathGoal(); if ( detourPathGoal ) { m_goal = detourPathGoal; } - const bool isDetouring = m_propDetour.IsDetouring(); + const bool isDetouring = m_pathObstacles.IsDetouring(); #endif // use the direction towards the goal as 'forward' direction @@ -860,7 +860,7 @@ void PathFollower::Update( INextBot *bot ) #ifdef NEO if ( isDetouring ) { - goalPos = m_propDetour.GetMoveGoal(); + goalPos = m_pathObstacles.GetMoveGoal(); } #endif diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.h b/src/game/server/NextBot/Path/NextBotPathFollow.h index 7f213cb138..c421f82852 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.h +++ b/src/game/server/NextBot/Path/NextBotPathFollow.h @@ -10,7 +10,7 @@ #include "nav_pathfind.h" #include "NextBotPath.h" #ifdef NEO -#include "neo/bot/neo_bot_prop_detour.h" +#include "neo/bot/neo_bot_path_obstacles.h" #endif class INextBot; @@ -46,7 +46,7 @@ class PathFollower : public Path Path::ResultType GetResult() const { return m_result; } #ifdef NEO - CBaseEntity *GetBreakableInWay() const { return m_propDetour.GetBreakableInWay(); } // a breakable the last look ahead met on the path + CBaseEntity *GetBreakableInWay() const { return m_pathObstacles.GetBreakableInWay(); } // a breakable the last look ahead met on the path #endif private: @@ -90,7 +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; - CNEOBotPropDetour m_propDetour; // steers around props the nav mesh does not know about + 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/neo_bot_prop_detour.cpp b/src/game/server/neo/bot/neo_bot_path_obstacles.cpp similarity index 96% rename from src/game/server/neo/bot/neo_bot_prop_detour.cpp rename to src/game/server/neo/bot/neo_bot_path_obstacles.cpp index 89858ee75e..833d9b23c6 100644 --- a/src/game/server/neo/bot/neo_bot_prop_detour.cpp +++ b/src/game/server/neo/bot/neo_bot_path_obstacles.cpp @@ -4,7 +4,7 @@ #include "NextBot/Path/NextBotPathFollow.h" #include "NextBotLocomotionInterface.h" #include "NextBotBodyInterface.h" -#include "neo_bot_prop_detour.h" +#include "neo_bot_path_obstacles.h" #include "tier1/utlpriorityqueue.h" #include "vphysics_interface.h" @@ -15,7 +15,7 @@ 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 PROP_DETOUR_REPLAN_INTERVAL = 0.3f; +constexpr float PATH_OBSTACLE_LOOK_INTERVAL = 0.3f; // 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; @@ -28,12 +28,12 @@ constexpr float PROP_DETOUR_WAYPOINT_REACHED_RANGE = 12.0f; constexpr float PROP_DETOUR_OFF_ROUTE_RANGE = 16.0f; // How far along the path props are looked for, and how much further the detour may rejoin it -constexpr float PROP_DETOUR_LOOK_AHEAD_RANGE = 256.0f; -constexpr float PROP_DETOUR_REJOIN_EXTRA_RANGE = 128.0f; +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 PROP_DETOUR_BREAKABLE_RANGE = 128.0f; +constexpr float PATH_OBSTACLE_BREAKABLE_RANGE = 128.0f; // The detour rejoins the path this far past the last prop in the way constexpr float PROP_DETOUR_REJOIN_PAST_PROP = 32.0f; @@ -71,7 +71,7 @@ constexpr float PROP_DETOUR_MOTION_PREDICT_TIME = 1.0f; constexpr float PROP_DETOUR_MOVING_PROP_MIN_SPEED = 10.0f; // Props are looked for up to this high above the path, so one coming down is seen in time -constexpr float PROP_DETOUR_PROP_QUERY_HEADROOM = 512.0f; +constexpr float PATH_OBSTACLE_QUERY_HEADROOM = 512.0f; // A prop smaller than this in every direction (a can, a bottle) is pushed aside, not walked around const Vector PROP_DETOUR_SMALL_PROP_SIZE( 16.0f, 16.0f, 40.0f ); @@ -79,7 +79,7 @@ const Vector PROP_DETOUR_SMALL_PROP_SIZE( 16.0f, 16.0f, 40.0f ); constexpr float PROP_DETOUR_DIAGONAL_STEP_LENGTH = 1.41421356f; // The body box is this much narrower than the hull, so a prop the bot only brushes is not in its way -constexpr float PROP_DETOUR_BODY_CLEARANCE = 1.0f; +constexpr float PATH_OBSTACLE_BODY_CLEARANCE = 1.0f; } //---------------------------------------------------------------------------------------------------------------- @@ -256,7 +256,7 @@ static bool IsInSight( INextBot *bot, CBaseEntity *entity ) //---------------------------------------------------------------------------------------------------------------- static BodyBox_t GetBodyBox( INextBot *bot ) { - const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth() - PROP_DETOUR_BODY_CLEARANCE; + const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth() - PATH_OBSTACLE_BODY_CLEARANCE; BodyBox_t box; box.mins.Init( -halfWidth, -halfWidth, bot->GetLocomotionInterface()->GetStepHeight() ); @@ -310,11 +310,11 @@ static bool BodyMeetsEntity( const BodyBox_t &body, const Vector &from, const Ve //---------------------------------------------------------------------------------------------------------------- -// The breakable the body meets first walking the line, within PROP_DETOUR_BREAKABLE_RANGE +// The breakable the body meets first walking the line, within PATH_OBSTACLE_BREAKABLE_RANGE static CBaseEntity *FindBreakableInWay( const BodyBox_t &body, const CUtlVector< Vector > &line, const CUtlVector< CBaseEntity * > &breakables ) { CBaseEntity *nearest = NULL; - float nearestDistance = PROP_DETOUR_BREAKABLE_RANGE; + float nearestDistance = PATH_OBSTACLE_BREAKABLE_RANGE; FOR_EACH_VEC( breakables, i ) { float legStart = 0.0f; @@ -395,7 +395,7 @@ static void FindMovableProps( const Vector &floorLo, const Vector &floorHi, CUtl INextBot *bot = NULL, CUtlVector< CBaseEntity * > *breakables = NULL ) { const Vector queryLo = floorLo - Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE ); - const Vector queryHi = floorHi + Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_PROP_QUERY_HEADROOM ); + const Vector queryHi = floorHi + Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PATH_OBSTACLE_QUERY_HEADROOM ); CMovablePropEnum propEnum( props, bot, breakables ); partition->EnumerateElementsInBox( PARTITION_ENGINE_NON_STATIC_EDICTS, queryLo, queryHi, false, &propEnum ); @@ -1076,16 +1076,16 @@ static bool FindWideDetour( INextBot *bot, float floorLo, float floorHi, const V //---------------------------------------------------------------------------------------------------------------- -CNEOBotPropDetour::CNEOBotPropDetour() +CNEOBotPathObstacles::CNEOBotPathObstacles() { Reset(); } //---------------------------------------------------------------------------------------------------------------- -void CNEOBotPropDetour::Reset() +void CNEOBotPathObstacles::Reset() { - m_replanTimer.Invalidate(); + m_lookTimer.Invalidate(); m_breakableTimer.Invalidate(); m_breakable = NULL; m_waypoints.RemoveAll(); @@ -1101,13 +1101,13 @@ void CNEOBotPropDetour::Reset() //---------------------------------------------------------------------------------------------------------------- -void CNEOBotPropDetour::Update( INextBot *bot, const PathFollower &path ) +void CNEOBotPathObstacles::Update( INextBot *bot, const PathFollower &path ) { m_pathGoal = NULL; - if ( m_replanTimer.IsElapsed() ) + if ( m_lookTimer.IsElapsed() ) { - m_replanTimer.Start( PROP_DETOUR_REPLAN_INTERVAL ); + m_lookTimer.Start( PATH_OBSTACLE_LOOK_INTERVAL ); if ( !IsDetouring() ) { @@ -1120,7 +1120,7 @@ void CNEOBotPropDetour::Update( INextBot *bot, const PathFollower &path ) else if ( m_isWide && !ClaimWideSearch() ) { // a detour the wide search found searches its whole region again: wait for a free tick - m_replanTimer.Invalidate(); + m_lookTimer.Invalidate(); } else if ( !Replan( bot ) ) { @@ -1152,7 +1152,7 @@ void CNEOBotPropDetour::Update( INextBot *bot, const PathFollower &path ) //---------------------------------------------------------------------------------------------------------------- // Look for props in the way on the path ahead, and when there are, a way past them on the mesh -void CNEOBotPropDetour::Plan( INextBot *bot, const PathFollower &path ) +void CNEOBotPathObstacles::Plan( INextBot *bot, const PathFollower &path ) { ILocomotion *mover = bot->GetLocomotionInterface(); const Path::Segment *goal = path.GetCurrentGoal(); @@ -1163,7 +1163,7 @@ void CNEOBotPropDetour::Plan( INextBot *bot, const PathFollower &path ) CUtlVector< Vector > line; CUtlVector< const Path::Segment * > segments; - GetPathAhead( path, mover->GetFeet(), PROP_DETOUR_LOOK_AHEAD_RANGE + PROP_DETOUR_REJOIN_EXTRA_RANGE, &line, &segments ); + GetPathAhead( path, mover->GetFeet(), PATH_OBSTACLE_LOOK_AHEAD_RANGE + PATH_OBSTACLE_REJOIN_EXTRA_RANGE, &line, &segments ); if ( line.Count() < 2 ) { return; @@ -1183,7 +1183,7 @@ void CNEOBotPropDetour::Plan( INextBot *bot, const PathFollower &path ) const BodyBox_t body = GetBodyBox( bot ); m_breakable = FindBreakableInWay( body, line, breakables ); - m_breakableTimer.Start( PROP_DETOUR_REPLAN_INTERVAL ); + m_breakableTimer.Start( PATH_OBSTACLE_LOOK_INTERVAL ); if ( obstacles.Count() == 0 ) { @@ -1198,12 +1198,12 @@ void CNEOBotPropDetour::Plan( INextBot *bot, const PathFollower &path ) FOR_EACH_VEC( obstacles, i ) { float legStart = 0.0f; - for ( int leg = 0; leg + 1 < line.Count() && legStart < PROP_DETOUR_LOOK_AHEAD_RANGE; ++leg ) + 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 < PROP_DETOUR_LOOK_AHEAD_RANGE + && 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 + PROP_DETOUR_REJOIN_PAST_PROP ); @@ -1246,7 +1246,7 @@ void CNEOBotPropDetour::Plan( INextBot *bot, const PathFollower &path ) if ( result == DETOUR_THROUGH_PUSHABLE ) { // the bot keeps its path through props it can shove, so no wide search for a way around them - m_replanTimer.Start( PROP_DETOUR_PUSH_THROUGH_RETRY_INTERVAL ); + m_lookTimer.Start( PROP_DETOUR_PUSH_THROUGH_RETRY_INTERVAL ); return; } @@ -1256,14 +1256,14 @@ void CNEOBotPropDetour::Plan( INextBot *bot, const PathFollower &path ) // at a random tick so blocked bots take turns instead of all planning again at the next one if ( !ClaimWideSearch() ) { - m_replanTimer.Start( RandomFloat( gpGlobals->interval_per_tick, PROP_DETOUR_REPLAN_INTERVAL ) ); + m_lookTimer.Start( RandomFloat( gpGlobals->interval_per_tick, PATH_OBSTACLE_LOOK_INTERVAL ) ); return; } if ( !FindWideDetour( bot, floorLo, floorHi, rejoin, pushableRoute, ®ionLo, ®ionHi, &m_waypoints ) ) { // a bot pushing a prop with no way around looks again less often: nothing changes quickly - m_replanTimer.Start( PROP_DETOUR_WIDE_SEARCH_RETRY_INTERVAL ); + m_lookTimer.Start( PROP_DETOUR_WIDE_SEARCH_RETRY_INTERVAL ); return; } @@ -1286,14 +1286,14 @@ void CNEOBotPropDetour::Plan( INextBot *bot, const PathFollower &path ) //---------------------------------------------------------------------------------------------------------------- // Look along the path ahead for a breakable in the way, without the props -void CNEOBotPropDetour::LookForBreakable( INextBot *bot, const PathFollower &path ) +void CNEOBotPathObstacles::LookForBreakable( INextBot *bot, const PathFollower &path ) { - m_breakableTimer.Start( PROP_DETOUR_REPLAN_INTERVAL ); + m_breakableTimer.Start( PATH_OBSTACLE_LOOK_INTERVAL ); m_breakable = NULL; CUtlVector< Vector > line; CUtlVector< const Path::Segment * > segments; - GetPathAhead( path, bot->GetLocomotionInterface()->GetFeet(), PROP_DETOUR_BREAKABLE_RANGE, &line, &segments ); + GetPathAhead( path, bot->GetLocomotionInterface()->GetFeet(), PATH_OBSTACLE_BREAKABLE_RANGE, &line, &segments ); if ( line.Count() < 2 ) { return; @@ -1316,7 +1316,7 @@ void CNEOBotPropDetour::LookForBreakable( INextBot *bot, const PathFollower &pat //---------------------------------------------------------------------------------------------------------------- // 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 ) +bool CNEOBotPathObstacles::Replan( INextBot *bot ) { // in the air, the bot keeps its detour until it lands ILocomotion *mover = bot->GetLocomotionInterface(); @@ -1347,7 +1347,7 @@ bool CNEOBotPropDetour::Replan( INextBot *bot ) //---------------------------------------------------------------------------------------------------------------- // 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 ) +void CNEOBotPathObstacles::NoteSearch( const Vector &feet ) { // only a wide detour's replans are skipped while its props rest if ( !m_isWide ) @@ -1363,7 +1363,7 @@ void CNEOBotPropDetour::NoteSearch( const Vector &feet ) //---------------------------------------------------------------------------------------------------------------- // 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 +bool CNEOBotPathObstacles::IsLastSearchValid( INextBot *bot ) const { if ( m_restingPropCount == PROP_DETOUR_PROPS_MOVING || m_searchAgeTimer.IsElapsed() ) { diff --git a/src/game/server/neo/bot/neo_bot_prop_detour.h b/src/game/server/neo/bot/neo_bot_path_obstacles.h similarity index 84% rename from src/game/server/neo/bot/neo_bot_prop_detour.h rename to src/game/server/neo/bot/neo_bot_path_obstacles.h index 089d7cf0f6..12e89ede51 100644 --- a/src/game/server/neo/bot/neo_bot_prop_detour.h +++ b/src/game/server/neo/bot/neo_bot_path_obstacles.h @@ -6,12 +6,13 @@ class INextBot; class PathFollower; //---------------------------------------------------------------------------------------------------------------- -// Steers a bot around props the nav mesh cannot know about, which physics or an animation moves, -// by searching a grid laid over the mesh on the path ahead for a way past the ones it sees -class CNEOBotPropDetour +// Looks along a bot's path for what the nav mesh cannot know about: +// props that physics or an animation moves, which it steers around by searching a grid laid over the mesh for a way past, +// and breakables in the way, which it reports for the bot to clear +class CNEOBotPathObstacles { public: - CNEOBotPropDetour(); + CNEOBotPathObstacles(); void Reset(); @@ -34,7 +35,7 @@ class CNEOBotPropDetour void NoteSearch( const Vector &feet ); bool IsLastSearchValid( INextBot *bot ) const; - CountdownTimer m_replanTimer; + CountdownTimer m_lookTimer; // the look ahead, or the detour's search again while detouring CountdownTimer m_breakableTimer; // the look for breakables, when no plan this often looks for them too CHandle< CBaseEntity > m_breakable; CUtlVector< Vector > m_waypoints; From 98c5219b22559c680ada32f83b8c6d3654222f8d Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 4 Oct 2026 11:42:27 -0600 Subject: [PATCH 5/8] Split the prop detour out of the path obstacles look CNEOBotPathObstacles keeps the look ahead every 0.3 s and the breakable find; CNEOBotPropDetour (neo_bot_prop_detour.{h,cpp}) holds the grid, the route and wide searches, and the detour with its replans. The look hands a plan request to the detour and learns when to look again; neither reaches into the other. The prop queries both use (body box, movable props, resting count) are in neo_bot_obstacle_props.{h,cpp}. No behavior change. --- src/game/server/CMakeLists.txt | 4 + .../server/neo/bot/neo_bot_obstacle_props.cpp | 244 ++++ .../server/neo/bot/neo_bot_obstacle_props.h | 48 + .../server/neo/bot/neo_bot_path_obstacles.cpp | 1082 +---------------- .../server/neo/bot/neo_bot_path_obstacles.h | 33 +- .../server/neo/bot/neo_bot_prop_detour.cpp | 848 +++++++++++++ src/game/server/neo/bot/neo_bot_prop_detour.h | 72 ++ 7 files changed, 1245 insertions(+), 1086 deletions(-) create mode 100644 src/game/server/neo/bot/neo_bot_obstacle_props.cpp create mode 100644 src/game/server/neo/bot/neo_bot_obstacle_props.h create mode 100644 src/game/server/neo/bot/neo_bot_prop_detour.cpp create mode 100644 src/game/server/neo/bot/neo_bot_prop_detour.h diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index f85a2575fd..fec1fd9998 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1515,7 +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 @@ -1578,7 +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/neo/bot/neo_bot_obstacle_props.cpp b/src/game/server/neo/bot/neo_bot_obstacle_props.cpp new file mode 100644 index 0000000000..bae17646cd --- /dev/null +++ b/src/game/server/neo/bot/neo_bot_obstacle_props.cpp @@ -0,0 +1,244 @@ +#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 PROP_DETOUR_MOTION_PREDICT_TIME = 1.0f; +constexpr float PROP_DETOUR_MOVING_PROP_MIN_SPEED = 10.0f; + +// A prop this light that physics moves is shoved out of the way, as a player does +constexpr float PROP_DETOUR_PUSHABLE_PROP_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 PATH_OBSTACLE_QUERY_HEADROOM = 512.0f; + +// A prop smaller than this in every direction (a can, a bottle) is pushed aside, not walked around +const Vector PROP_DETOUR_SMALL_PROP_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 PATH_OBSTACLE_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 solid to players a spatial partition query meets, however much else lies there: + // a box query into a fixed-size list fills up with everything it finds, cans, weapons and triggers alike. + // Given a list for them, it also keeps the solid entities the bot can break, such as glass + class CMovablePropEnum : public IPartitionEnumerator + { + public: + CMovablePropEnum( CUtlVector< CBaseEntity * > *props, INextBot *bot, CUtlVector< CBaseEntity * > *breakables ) + : m_props( props ), m_bot( bot ), m_breakables( breakables ) {} + + virtual IterationRetval_t EnumElement( IHandleEntity *handleEntity ) + { + CBaseEntity *entity = gEntList.GetBaseEntity( handleEntity->GetRefEHandle() ); + if ( !entity || !IsSolidToPlayers( entity ) ) + { + return ITERATION_CONTINUE; + } + + if ( IsMovableProp( entity ) ) + { + m_props->AddToTail( entity ); + } + + if ( m_breakables && m_bot->IsAbleToBreak( entity ) && entity->GetHealth() > 0 ) + { + m_breakables->AddToTail( entity ); + } + + return ITERATION_CONTINUE; + } + + private: + CUtlVector< CBaseEntity * > *m_props; + INextBot *m_bot; + 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( PROP_DETOUR_MOVING_PROP_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() <= PROP_DETOUR_PUSHABLE_PROP_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() - PATH_OBSTACLE_BODY_CLEARANCE; + + BodyBox_t box; + box.mins.Init( -halfWidth, -halfWidth, bot->GetLocomotionInterface()->GetStepHeight() ); + box.maxs.Init( halfWidth, halfWidth, bot->GetBodyInterface()->GetStandHullHeight() ); + return box; +} + +//---------------------------------------------------------------------------------------------------------------- +bool BodyMeetsProp( const BodyBox_t &body, const Vector &from, const Vector &to, const PropObstacle_t &obstacle, float *fraction ) +{ + *fraction = 1.0f; + + // moving the body back along the prop's motion is the same as moving the prop forward + const int tests = obstacle.sweep.IsZero() ? 1 : 2; + for ( int i = 0; i < tests; ++i ) + { + const Vector shift = ( i == 0 ) ? vec3_origin : -obstacle.sweep; + + Ray_t ray; + ray.Init( from + shift, to + shift, body.mins, body.maxs ); + + trace_t result; + enginetrace->ClipRayToEntity( ray, MASK_PLAYERSOLID, obstacle.entity, &result ); + if ( result.startsolid ) + { + *fraction = 0.0f; + return true; + } + + *fraction = MIN( *fraction, result.fraction ); + } + + return *fraction < 1.0f; +} + +//---------------------------------------------------------------------------------------------------------------- +void FindMovableProps( const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *props, + INextBot *bot, CUtlVector< CBaseEntity * > *breakables ) +{ + const Vector queryLo = floorLo - Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE ); + const Vector queryHi = floorHi + Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PATH_OBSTACLE_QUERY_HEADROOM ); + + CMovablePropEnum propEnum( props, bot, breakables ); + partition->EnumerateElementsInBox( PARTITION_ENGINE_NON_STATIC_EDICTS, queryLo, queryHi, false, &propEnum ); +} + + +//---------------------------------------------------------------------------------------------------------------- +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; + FindMovableProps( floorLo, floorHi, &props, bot, 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 < PROP_DETOUR_SMALL_PROP_SIZE.x && size.y < PROP_DETOUR_SMALL_PROP_SIZE.y && size.z < PROP_DETOUR_SMALL_PROP_SIZE.z ) + { + continue; + } + + const Vector sweep = GetPropVelocity( entity ) * PROP_DETOUR_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 PROP_DETOUR_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..16defbbe49 --- /dev/null +++ b/src/game/server/neo/bot/neo_bot_obstacle_props.h @@ -0,0 +1,48 @@ +#pragma once + +class INextBot; +class CBaseEntity; + +// Floor around the path and the props that a detour may use reaches this far past them +constexpr float PROP_DETOUR_GRID_MARGIN = 48.0f; + +// A detour stays on floor this close in height to the bot's feet or to where it rejoins the path +constexpr float PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE = 40.0f; + +// What a count of the props resting in a region says when one of them is awake +constexpr int PROP_DETOUR_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 PROP_DETOUR_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 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, +// and, given a bot and a list for them, the solid entities it can break there +void FindMovableProps( const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *props, + INextBot *bot = NULL, CUtlVector< CBaseEntity * > *breakables = NULL ); + +// 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 PROP_DETOUR_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 index 833d9b23c6..fb1c7e5718 100644 --- a/src/game/server/neo/bot/neo_bot_path_obstacles.cpp +++ b/src/game/server/neo/bot/neo_bot_path_obstacles.cpp @@ -1,12 +1,10 @@ #include "cbase.h" -#include "nav_mesh.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" -#include "tier1/utlpriorityqueue.h" -#include "vphysics_interface.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" @@ -16,16 +14,6 @@ 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; -// 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; -// What a count of the props resting in a region says when one of them is awake -constexpr int PROP_DETOUR_PROPS_MOVING = -1; - -// 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; // 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; @@ -37,83 +25,11 @@ constexpr float PATH_OBSTACLE_BREAKABLE_RANGE = 128.0f; // The detour rejoins the path this far past the last prop in the way constexpr float PROP_DETOUR_REJOIN_PAST_PROP = 32.0f; - -// Floor around the path and the props that a detour may use -constexpr float PROP_DETOUR_GRID_MARGIN = 48.0f; -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; - -// A detour stays on floor this close in height to the bot's feet or to where it rejoins the path -constexpr float PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE = 40.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 prop this light that physics moves is shoved out of the way, as a player does: -// its cells cost this many free cells, so a bot walks around one chair where there is room, -constexpr float PROP_DETOUR_PUSHABLE_PROP_MAX_MASS = 200.0f; -constexpr float PROP_DETOUR_PUSHABLE_CELL_COST = 4.0f; -// and when every prop in the way on the path is that 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; - -// A moving prop is kept clear of where it will be within this time, at its current velocity -constexpr float PROP_DETOUR_MOTION_PREDICT_TIME = 1.0f; -constexpr float PROP_DETOUR_MOVING_PROP_MIN_SPEED = 10.0f; - -// Props are looked for up to this high above the path, so one coming down is seen in time -constexpr float PATH_OBSTACLE_QUERY_HEADROOM = 512.0f; - -// A prop smaller than this in every direction (a can, a bottle) is pushed aside, not walked around -const Vector PROP_DETOUR_SMALL_PROP_SIZE( 16.0f, 16.0f, 40.0f ); - -constexpr float PROP_DETOUR_DIAGONAL_STEP_LENGTH = 1.41421356f; - -// The body box is this much narrower than the hull, so a prop the bot only brushes is not in its way -constexpr float PATH_OBSTACLE_BODY_CLEARANCE = 1.0f; } //---------------------------------------------------------------------------------------------------------------- namespace { - struct PropObstacle_t - { - CBaseEntity *entity; - Vector sweep; // how far it moves within PROP_DETOUR_MOTION_PREDICT_TIME - Vector2D lo, hi; // footprint over that time, grown by a hull half-width - bool isPushable; // light enough to shove aside - }; - - // 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 - }; - - // The bot's body as a box: from a step over the floor to its standing height - struct BodyBox_t - { - Vector mins, maxs; - }; - // Return true if the line from a to b crosses the box, with the entry and exit fractions bool SegmentCrossesBox( const Vector2D &a, const Vector2D &b, const Vector2D &lo, const Vector2D &hi, float *enter, float *exit ) { @@ -154,147 +70,6 @@ namespace } -//---------------------------------------------------------------------------------------------------------------- -// 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 solid to players a spatial partition query meets, however much else lies there: - // a box query into a fixed-size list fills up with everything it finds, cans, weapons and triggers alike. - // Given a list for them, it also keeps the solid entities the bot can break, such as glass - class CMovablePropEnum : public IPartitionEnumerator - { - public: - CMovablePropEnum( CUtlVector< CBaseEntity * > *props, INextBot *bot, CUtlVector< CBaseEntity * > *breakables ) - : m_props( props ), m_bot( bot ), m_breakables( breakables ) {} - - virtual IterationRetval_t EnumElement( IHandleEntity *handleEntity ) - { - CBaseEntity *entity = gEntList.GetBaseEntity( handleEntity->GetRefEHandle() ); - if ( !entity || !IsSolidToPlayers( entity ) ) - { - return ITERATION_CONTINUE; - } - - if ( IsMovableProp( entity ) ) - { - m_props->AddToTail( entity ); - } - - if ( m_breakables && m_bot->IsAbleToBreak( entity ) && entity->GetHealth() > 0 ) - { - m_breakables->AddToTail( entity ); - } - - return ITERATION_CONTINUE; - } - - private: - CUtlVector< CBaseEntity * > *m_props; - INextBot *m_bot; - 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( PROP_DETOUR_MOVING_PROP_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() <= PROP_DETOUR_PUSHABLE_PROP_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(); -} - - -//---------------------------------------------------------------------------------------------------------------- -static BodyBox_t GetBodyBox( INextBot *bot ) -{ - const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth() - PATH_OBSTACLE_BODY_CLEARANCE; - - BodyBox_t box; - box.mins.Init( -halfWidth, -halfWidth, bot->GetLocomotionInterface()->GetStepHeight() ); - box.maxs.Init( halfWidth, halfWidth, bot->GetBodyInterface()->GetStandHullHeight() ); - return box; -} - - -//---------------------------------------------------------------------------------------------------------------- -// Return true if a body moving from 'from' to 'to' touches the prop, now or where it will be -static bool BodyMeetsProp( const BodyBox_t &body, const Vector &from, const Vector &to, const PropObstacle_t &obstacle, float *fraction ) -{ - *fraction = 1.0f; - - // moving the body back along the prop's motion is the same as moving the prop forward - const int tests = obstacle.sweep.IsZero() ? 1 : 2; - for ( int i = 0; i < tests; ++i ) - { - const Vector shift = ( i == 0 ) ? vec3_origin : -obstacle.sweep; - - Ray_t ray; - ray.Init( from + shift, to + shift, body.mins, body.maxs ); - - trace_t result; - enginetrace->ClipRayToEntity( ray, MASK_PLAYERSOLID, obstacle.entity, &result ); - if ( result.startsolid ) - { - *fraction = 0.0f; - return true; - } - - *fraction = MIN( *fraction, result.fraction ); - } - - return *fraction < 1.0f; -} - - //---------------------------------------------------------------------------------------------------------------- // Return true if a body moving from 'from' to 'to' touches the entity where it stands static bool BodyMeetsEntity( const BodyBox_t &body, const Vector &from, const Vector &to, CBaseEntity *entity, float *fraction ) @@ -336,7 +111,6 @@ static CBaseEntity *FindBreakableInWay( const BodyBox_t &body, const CUtlVector< return nearest; } - //---------------------------------------------------------------------------------------------------------------- // The path ahead as a line from the bot's feet, over walked segments only, up to the range, // with the segment each point of the line belongs to @@ -389,692 +163,6 @@ static Vector GetPointAlong( const CUtlVector< Vector > &line, float distance, i } -//---------------------------------------------------------------------------------------------------------------- -// Every movable prop solid to players around the floor in the box, from below it up to the headroom above it -static void FindMovableProps( const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *props, - INextBot *bot = NULL, CUtlVector< CBaseEntity * > *breakables = NULL ) -{ - const Vector queryLo = floorLo - Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE ); - const Vector queryHi = floorHi + Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PATH_OBSTACLE_QUERY_HEADROOM ); - - CMovablePropEnum propEnum( props, bot, breakables ); - partition->EnumerateElementsInBox( PARTITION_ENGINE_NON_STATIC_EDICTS, queryLo, queryHi, false, &propEnum ); -} - - -//---------------------------------------------------------------------------------------------------------------- -// 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 -static void CollectProps( INextBot *bot, const Vector &floorLo, const Vector &floorHi, CUtlVector< PropObstacle_t > *obstacles, - CUtlVector< CBaseEntity * > *breakables = NULL ) -{ - const BodyBox_t body = GetBodyBox( bot ); - const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth(); - - CUtlVector< CBaseEntity * > props; - FindMovableProps( floorLo, floorHi, &props, bot, 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 < PROP_DETOUR_SMALL_PROP_SIZE.x && size.y < PROP_DETOUR_SMALL_PROP_SIZE.y && size.z < PROP_DETOUR_SMALL_PROP_SIZE.z ) - { - continue; - } - - const Vector sweep = GetPropVelocity( entity ) * PROP_DETOUR_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 ); - } -} - - -//---------------------------------------------------------------------------------------------------------------- -// How many movable props stand around the floor in the box, or PROP_DETOUR_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 -static 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 PROP_DETOUR_PROPS_MOVING; - } - } - - return props.Count(); -} - - -//---------------------------------------------------------------------------------------------------------------- -// 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; -} - - //---------------------------------------------------------------------------------------------------------------- CNEOBotPathObstacles::CNEOBotPathObstacles() { @@ -1088,64 +176,37 @@ void CNEOBotPathObstacles::Reset() m_lookTimer.Invalidate(); m_breakableTimer.Invalidate(); m_breakable = NULL; - m_waypoints.RemoveAll(); - m_pathGoal = NULL; - m_rejoinGoal = NULL; - m_resumeGoal = NULL; - m_isWide = false; - m_isPathPushable = false; - m_legStart = vec3_origin; - m_restingPropCount = PROP_DETOUR_PROPS_MOVING; - m_searchAgeTimer.Invalidate(); + m_detour.Reset(); } //---------------------------------------------------------------------------------------------------------------- void CNEOBotPathObstacles::Update( INextBot *bot, const PathFollower &path ) { - m_pathGoal = NULL; - + bool isSearchDue = false; if ( m_lookTimer.IsElapsed() ) { m_lookTimer.Start( PATH_OBSTACLE_LOOK_INTERVAL ); - if ( !IsDetouring() ) + if ( !m_detour.IsDetouring() ) { Plan( bot, path ); } - else 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 - m_lookTimer.Invalidate(); - } - else if ( !Replan( bot ) ) + else { - // 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; + isSearchDue = true; } } - // 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() ) + if ( m_detour.Update( bot, isSearchDue ) ) { - LookForBreakable( bot, path ); + m_lookTimer.Invalidate(); } - 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() ) + // 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() ) { - m_pathGoal = m_rejoinGoal; + LookForBreakable( bot, path ); } } @@ -1235,52 +296,21 @@ void CNEOBotPathObstacles::Plan( INextBot *bot, const PathFollower &path ) distance += ( i + 1 < line.Count() ) ? ( line[ i + 1 ] - line[ i ] ).Length2D() : 0.0f; } - regionLo = regionLo.Min( rejoin.AsVector2D() ) - Vector2D( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN ); - regionHi = regionHi.Max( rejoin.AsVector2D() ) + Vector2D( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN ); + PropDetourRequest_t request; + request.rejoin = rejoin; + request.rejoinGoal = segments[ rejoinLegEnd ]; + request.resumeGoal = goal; + request.regionLo = regionLo.Min( rejoin.AsVector2D() ) - Vector2D( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN ); + request.regionHi = regionHi.Max( rejoin.AsVector2D() ) + Vector2D( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN ); + request.floorLo = floorLo; + request.floorHi = floorHi; + request.isPathPushable = isPathPushable; - // 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 = isPathPushable ? PUSHABLE_ROUTE_IS_NO_DETOUR : PUSHABLE_ROUTE_IS_DETOUR; - const DetourResult result = SearchRegion( bot, regionLo, regionHi, floorLo, floorHi, rejoin, pushableRoute, &m_waypoints ); - if ( result == DETOUR_THROUGH_PUSHABLE ) + const float lookAgainIn = m_detour.Plan( bot, request, PATH_OBSTACLE_LOOK_INTERVAL ); + if ( lookAgainIn > 0.0f ) { - // the bot keeps its path through props it can shove, so no wide search for a way around them - m_lookTimer.Start( PROP_DETOUR_PUSH_THROUGH_RETRY_INTERVAL ); - return; + m_lookTimer.Start( lookAgainIn ); } - - if ( result == DETOUR_NONE ) - { - // another bot had this tick's wide search: plan again within a replan interval, - // at a random tick so blocked bots take turns instead of all planning again at the next one - if ( !ClaimWideSearch() ) - { - m_lookTimer.Start( RandomFloat( gpGlobals->interval_per_tick, PATH_OBSTACLE_LOOK_INTERVAL ) ); - return; - } - - if ( !FindWideDetour( bot, floorLo, floorHi, rejoin, pushableRoute, ®ionLo, ®ionHi, &m_waypoints ) ) - { - // a bot pushing a prop with no way around looks again less often: nothing changes quickly - m_lookTimer.Start( PROP_DETOUR_WIDE_SEARCH_RETRY_INTERVAL ); - return; - } - - isWide = true; - } - - // the detour keeps this target while the props and the bot move - m_rejoin = rejoin; - m_rejoinGoal = segments[ rejoinLegEnd ]; - m_resumeGoal = goal; - m_regionLo = regionLo; - m_regionHi = regionHi; - m_floorLo = floorLo; - m_floorHi = floorHi; - m_isWide = isWide; - m_isPathPushable = isPathPushable; - NoteSearch( mover->GetFeet() ); } @@ -1312,69 +342,3 @@ void CNEOBotPathObstacles::LookForBreakable( INextBot *bot, const PathFollower & FindMovableProps( lineLo, lineHi, &props, bot, &breakables ); m_breakable = FindBreakableInWay( GetBodyBox( bot ), line, breakables ); } - - -//---------------------------------------------------------------------------------------------------------------- -// Search again for a way to the same rejoin point, from where the bot is and around the props where they are now -bool CNEOBotPathObstacles::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 CNEOBotPathObstacles::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 CNEOBotPathObstacles::IsLastSearchValid( INextBot *bot ) const -{ - if ( m_restingPropCount == PROP_DETOUR_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_path_obstacles.h b/src/game/server/neo/bot/neo_bot_path_obstacles.h index 12e89ede51..5e996ba362 100644 --- a/src/game/server/neo/bot/neo_bot_path_obstacles.h +++ b/src/game/server/neo/bot/neo_bot_path_obstacles.h @@ -1,13 +1,14 @@ #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 physics or an animation moves, which it steers around by searching a grid laid over the mesh for a way past, +// props that physics or an animation moves, which it steers around with a detour, // and breakables in the way, which it reports for the bot to clear class CNEOBotPathObstacles { @@ -18,12 +19,12 @@ class CNEOBotPathObstacles void Update( INextBot *bot, const PathFollower &path ); - bool IsDetouring() const { return m_waypoints.Count() > 0; } - const Vector &GetMoveGoal() const { return m_waypoints[ 0 ]; } // where to move next while detouring + 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_pathGoal; } + 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(); } @@ -31,31 +32,9 @@ class CNEOBotPathObstacles private: void Plan( INextBot *bot, const PathFollower &path ); void LookForBreakable( INextBot *bot, const PathFollower &path ); - bool Replan( INextBot *bot ); - void NoteSearch( const Vector &feet ); - bool IsLastSearchValid( INextBot *bot ) const; CountdownTimer m_lookTimer; // the look ahead, or the detour's search again while detouring CountdownTimer m_breakableTimer; // the look for breakables, when no plan this often looks for them too CHandle< CBaseEntity > m_breakable; - 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 (PROP_DETOUR_PROPS_MOVING if any was awake) - Vector m_legStart; - int m_restingPropCount; - CountdownTimer m_searchAgeTimer; + 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..86fe5dc432 --- /dev/null +++ b/src/game/server/neo/bot/neo_bot_prop_detour.cpp @@ -0,0 +1,848 @@ +#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; + +// Cells of the grid laid over the floor a detour may use +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 = PROP_DETOUR_PROPS_MOVING; + m_searchAgeTimer.Invalidate(); +} + + +//---------------------------------------------------------------------------------------------------------------- +float CNEOBotPropDetour::Plan( INextBot *bot, const PropDetourRequest_t &request, float lookInterval ) +{ + Vector2D regionLo = request.regionLo; + Vector2D regionHi = request.regionHi; + + // 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 == PROP_DETOUR_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..f94288cc16 --- /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 it may use, 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 (PROP_DETOUR_PROPS_MOVING if any was awake) + Vector m_legStart; + int m_restingPropCount; + CountdownTimer m_searchAgeTimer; +}; From 5bf24f091780aec9d9a14026d348613d22bdbd20 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 4 Oct 2026 11:58:05 -0600 Subject: [PATCH 6/8] Tidy the path obstacles look after review One constant prefix per file: OBSTACLE_PROP_* for the shared prop query, which no longer names the detour's grid; the detour defines its grid margin and floor tolerance from the query's, and adds the margin to the region the look hands it. The query's two optional arguments become FindMovableProps and FindBreakables over one enumerator that asserts a breaker for a breakables list; the cheap health test runs before IsAbleToBreak. BodyMeetsProp is built on BodyMeetsEntity, now in the shared file. A breakable outside the box of the first 128 u is not swept; the breakable-only look no longer builds lists it does not read. Comments cut to two lines. --- .../server/neo/bot/neo_bot_obstacle_props.cpp | 116 +++++++++------ .../server/neo/bot/neo_bot_obstacle_props.h | 27 ++-- .../server/neo/bot/neo_bot_path_obstacles.cpp | 136 +++++++++--------- .../server/neo/bot/neo_bot_path_obstacles.h | 5 +- .../server/neo/bot/neo_bot_prop_detour.cpp | 15 +- src/game/server/neo/bot/neo_bot_prop_detour.h | 4 +- 6 files changed, 171 insertions(+), 132 deletions(-) diff --git a/src/game/server/neo/bot/neo_bot_obstacle_props.cpp b/src/game/server/neo/bot/neo_bot_obstacle_props.cpp index bae17646cd..afa7897adc 100644 --- a/src/game/server/neo/bot/neo_bot_obstacle_props.cpp +++ b/src/game/server/neo/bot/neo_bot_obstacle_props.cpp @@ -11,20 +11,20 @@ namespace { // A moving prop is kept clear of where it will be within this time, at its current velocity -constexpr float PROP_DETOUR_MOTION_PREDICT_TIME = 1.0f; -constexpr float PROP_DETOUR_MOVING_PROP_MIN_SPEED = 10.0f; +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 PROP_DETOUR_PUSHABLE_PROP_MAX_MASS = 200.0f; +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 PATH_OBSTACLE_QUERY_HEADROOM = 512.0f; +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 PROP_DETOUR_SMALL_PROP_SIZE( 16.0f, 16.0f, 40.0f ); +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 PATH_OBSTACLE_BODY_CLEARANCE = 1.0f; +constexpr float OBSTACLE_PROP_BODY_CLEARANCE = 1.0f; } //---------------------------------------------------------------------------------------------------------------- @@ -51,14 +51,16 @@ static bool IsSolidToPlayers( CBaseEntity *entity ) //---------------------------------------------------------------------------------------------------------------- namespace { - // The movable props solid to players a spatial partition query meets, however much else lies there: - // a box query into a fixed-size list fills up with everything it finds, cans, weapons and triggers alike. - // Given a list for them, it also keeps the solid entities the bot can break, such as glass - class CMovablePropEnum : public IPartitionEnumerator + // 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: - CMovablePropEnum( CUtlVector< CBaseEntity * > *props, INextBot *bot, CUtlVector< CBaseEntity * > *breakables ) - : m_props( props ), m_bot( bot ), m_breakables( breakables ) {} + 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 ) { @@ -68,12 +70,12 @@ namespace return ITERATION_CONTINUE; } - if ( IsMovableProp( entity ) ) + if ( m_props && IsMovableProp( entity ) ) { m_props->AddToTail( entity ); } - if ( m_breakables && m_bot->IsAbleToBreak( entity ) && entity->GetHealth() > 0 ) + if ( m_breakables && entity->GetHealth() > 0 && m_breaker->IsAbleToBreak( entity ) ) { m_breakables->AddToTail( entity ); } @@ -83,7 +85,7 @@ namespace private: CUtlVector< CBaseEntity * > *m_props; - INextBot *m_bot; + INextBot *m_breaker; CUtlVector< CBaseEntity * > *m_breakables; }; } @@ -100,7 +102,7 @@ static Vector GetPropVelocity( CBaseEntity *entity ) Vector velocity; physics->GetVelocity( &velocity, NULL ); - return velocity.IsLengthGreaterThan( PROP_DETOUR_MOVING_PROP_MIN_SPEED ) ? velocity : vec3_origin; + return velocity.IsLengthGreaterThan( OBSTACLE_PROP_MOVING_MIN_SPEED ) ? velocity : vec3_origin; } @@ -110,7 +112,7 @@ static Vector GetPropVelocity( CBaseEntity *entity ) static bool IsPushable( CBaseEntity *entity ) { IPhysicsObject *physics = entity->VPhysicsGetObject(); - return physics && physics->IsMoveable() && physics->GetMass() <= PROP_DETOUR_PUSHABLE_PROP_MAX_MASS + return physics && physics->IsMoveable() && physics->GetMass() <= OBSTACLE_PROP_PUSHABLE_MAX_MASS && !physics->IsAttachedToConstraint( false ) && !FClassnameIs( entity, "phys_bone_follower" ); } @@ -129,7 +131,7 @@ static bool IsInSight( INextBot *bot, CBaseEntity *entity ) //---------------------------------------------------------------------------------------------------------------- BodyBox_t GetBodyBox( INextBot *bot ) { - const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth() - PATH_OBSTACLE_BODY_CLEARANCE; + const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth() - OBSTACLE_PROP_BODY_CLEARANCE; BodyBox_t box; box.mins.Init( -halfWidth, -halfWidth, bot->GetLocomotionInterface()->GetStepHeight() ); @@ -137,43 +139,67 @@ BodyBox_t GetBodyBox( INextBot *bot ) return box; } + //---------------------------------------------------------------------------------------------------------------- -bool BodyMeetsProp( const BodyBox_t &body, const Vector &from, const Vector &to, const PropObstacle_t &obstacle, float *fraction ) +bool BodyMeetsEntity( const BodyBox_t &body, const Vector &from, const Vector &to, CBaseEntity *entity, float *fraction ) { - *fraction = 1.0f; + Ray_t ray; + ray.Init( from, to, body.mins, body.maxs ); - // moving the body back along the prop's motion is the same as moving the prop forward - const int tests = obstacle.sweep.IsZero() ? 1 : 2; - for ( int i = 0; i < tests; ++i ) - { - const Vector shift = ( i == 0 ) ? vec3_origin : -obstacle.sweep; + trace_t result; + enginetrace->ClipRayToEntity( ray, MASK_PLAYERSOLID, entity, &result ); + *fraction = result.startsolid ? 0.0f : result.fraction; + return result.startsolid || result.fraction < 1.0f; +} - Ray_t ray; - ray.Init( from + shift, to + shift, body.mins, body.maxs ); - trace_t result; - enginetrace->ClipRayToEntity( ray, MASK_PLAYERSOLID, obstacle.entity, &result ); - if ( result.startsolid ) - { - *fraction = 0.0f; - return true; - } +//---------------------------------------------------------------------------------------------------------------- +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; + } - *fraction = MIN( *fraction, result.fraction ); + 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; } //---------------------------------------------------------------------------------------------------------------- -void FindMovableProps( const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *props, - INextBot *bot, CUtlVector< CBaseEntity * > *breakables ) +// 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( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE ); - const Vector queryHi = floorHi + Vector( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN, PATH_OBSTACLE_QUERY_HEADROOM ); + 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 ); +} + - CMovablePropEnum propEnum( props, bot, breakables ); - partition->EnumerateElementsInBox( PARTITION_ENGINE_NON_STATIC_EDICTS, queryLo, queryHi, false, &propEnum ); +//---------------------------------------------------------------------------------------------------------------- +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 ); } @@ -185,7 +211,7 @@ void CollectProps( INextBot *bot, const Vector &floorLo, const Vector &floorHi, const float halfWidth = 0.5f * bot->GetBodyInterface()->GetHullWidth(); CUtlVector< CBaseEntity * > props; - FindMovableProps( floorLo, floorHi, &props, bot, breakables ); + QueryObstacleEntities( floorLo, floorHi, &props, breakables ? bot : NULL, breakables ); FOR_EACH_VEC( props, i ) { CBaseEntity *entity = props[ i ]; @@ -193,12 +219,12 @@ void CollectProps( INextBot *bot, const Vector &floorLo, const Vector &floorHi, entity->CollisionProp()->WorldSpaceAABB( &propLo, &propHi ); const Vector size = propHi - propLo; - if ( size.x < PROP_DETOUR_SMALL_PROP_SIZE.x && size.y < PROP_DETOUR_SMALL_PROP_SIZE.y && size.z < PROP_DETOUR_SMALL_PROP_SIZE.z ) + 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 ) * PROP_DETOUR_MOTION_PREDICT_TIME; + const Vector sweep = GetPropVelocity( entity ) * OBSTACLE_PROP_MOTION_PREDICT_TIME; Vector sweptLo = propLo; Vector sweptHi = propHi; VectorMin( sweptLo, propLo + sweep, sweptLo ); @@ -236,7 +262,7 @@ int CountRestingProps( const Vector &floorLo, const Vector &floorHi ) IPhysicsObject *physics = props[ i ]->VPhysicsGetObject(); if ( physics && !physics->IsAsleep() ) { - return PROP_DETOUR_PROPS_MOVING; + return OBSTACLE_PROPS_MOVING; } } diff --git a/src/game/server/neo/bot/neo_bot_obstacle_props.h b/src/game/server/neo/bot/neo_bot_obstacle_props.h index 16defbbe49..75c47b7ca2 100644 --- a/src/game/server/neo/bot/neo_bot_obstacle_props.h +++ b/src/game/server/neo/bot/neo_bot_obstacle_props.h @@ -3,21 +3,20 @@ class INextBot; class CBaseEntity; -// Floor around the path and the props that a detour may use reaches this far past them -constexpr float PROP_DETOUR_GRID_MARGIN = 48.0f; - -// A detour stays on floor this close in height to the bot's feet or to where it rejoins the path -constexpr float PROP_DETOUR_FLOOR_HEIGHT_TOLERANCE = 40.0f; +// 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 PROP_DETOUR_PROPS_MOVING = -1; +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 PROP_DETOUR_MOTION_PREDICT_TIME + 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 }; @@ -30,19 +29,23 @@ struct BodyBox_t 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, -// and, given a bot and a list for them, the solid entities it can break there -void FindMovableProps( const Vector &floorLo, const Vector &floorHi, CUtlVector< CBaseEntity * > *props, - INextBot *bot = NULL, CUtlVector< CBaseEntity * > *breakables = NULL ); +// 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 PROP_DETOUR_PROPS_MOVING if any of them is awake: +// 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 index fb1c7e5718..99c5374b19 100644 --- a/src/game/server/neo/bot/neo_bot_path_obstacles.cpp +++ b/src/game/server/neo/bot/neo_bot_path_obstacles.cpp @@ -1,4 +1,5 @@ #include "cbase.h" +#include "collisionutils.h" #include "NextBot.h" #include "NextBot/Path/NextBotPathFollow.h" #include "NextBotLocomotionInterface.h" @@ -24,63 +25,62 @@ constexpr float PATH_OBSTACLE_REJOIN_EXTRA_RANGE = 128.0f; constexpr float PATH_OBSTACLE_BREAKABLE_RANGE = 128.0f; // The detour rejoins the path this far past the last prop in the way -constexpr float PROP_DETOUR_REJOIN_PAST_PROP = 32.0f; +constexpr float PATH_OBSTACLE_REJOIN_PAST_PROP = 32.0f; } //---------------------------------------------------------------------------------------------------------------- -namespace +// 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 ) { - // Return true if the line from a to b crosses the box, with the entry and exit fractions - 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 ) { - 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 ( fabsf( delta[ axis ] ) < 1e-4f ) + if ( a[ axis ] < lo[ axis ] || a[ axis ] > hi[ axis ] ) { - if ( a[ axis ] < lo[ axis ] || a[ axis ] > hi[ axis ] ) - { - return false; - } - - continue; + return false; } - float ta = ( lo[ axis ] - a[ axis ] ) / delta[ axis ]; - float tb = ( hi[ axis ] - a[ axis ] ) / delta[ axis ]; - if ( ta > tb ) - { - V_swap( ta, tb ); - } + continue; + } - t0 = MAX( t0, ta ); - t1 = MIN( t1, tb ); - if ( t0 > t1 ) - { - return false; - } + float ta = ( lo[ axis ] - a[ axis ] ) / delta[ axis ]; + float tb = ( hi[ axis ] - a[ axis ] ) / delta[ axis ]; + if ( ta > tb ) + { + V_swap( ta, tb ); } - *enter = t0; - *exit = t1; - return true; + t0 = MAX( t0, ta ); + t1 = MIN( t1, tb ); + if ( t0 > t1 ) + { + return false; + } } + + *enter = t0; + *exit = t1; + return true; } //---------------------------------------------------------------------------------------------------------------- -// Return true if a body moving from 'from' to 'to' touches the entity where it stands -static bool BodyMeetsEntity( const BodyBox_t &body, const Vector &from, const Vector &to, CBaseEntity *entity, float *fraction ) +// 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 ) { - 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; + *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 ); + } } @@ -88,10 +88,23 @@ static bool BodyMeetsEntity( const BodyBox_t &body, const Vector &from, const Ve // The breakable the body meets first walking the line, within PATH_OBSTACLE_BREAKABLE_RANGE static CBaseEntity *FindBreakableInWay( 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; 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 ) { @@ -113,11 +126,14 @@ static CBaseEntity *FindBreakableInWay( const BodyBox_t &body, const CUtlVector< //---------------------------------------------------------------------------------------------------------------- // The path ahead as a line from the bot's feet, over walked segments only, up to the range, -// with 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 ) +// 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 ); - segments->AddToTail( path.GetCurrentGoal() ); + if ( segments ) + { + segments->AddToTail( path.GetCurrentGoal() ); + } float length = 0.0f; for ( const Path::Segment *seg = path.GetCurrentGoal(); seg && length < range; seg = path.NextSegment( seg ) ) @@ -129,7 +145,11 @@ static void GetPathAhead( const PathFollower &path, const Vector &feet, float ra const Vector leg = seg->pos - line->Tail(); const float legLength = leg.Length2D(); - segments->AddToTail( seg ); + if ( segments ) + { + segments->AddToTail( seg ); + } + if ( length + legLength > range ) { line->AddToTail( line->Tail() + leg * ( ( range - length ) / legLength ) ); @@ -230,13 +250,8 @@ void CNEOBotPathObstacles::Plan( INextBot *bot, const PathFollower &path ) return; } - Vector lineLo = line[ 0 ]; - Vector lineHi = line[ 0 ]; - for ( int i = 1; i < line.Count(); ++i ) - { - VectorMin( lineLo, line[ i ], lineLo ); - VectorMax( lineHi, line[ i ], lineHi ); - } + Vector lineLo, lineHi; + GetLineBounds( line, FLT_MAX, &lineLo, &lineHi ); CUtlVector< PropObstacle_t > obstacles; CUtlVector< CBaseEntity * > breakables; @@ -267,7 +282,7 @@ void CNEOBotPathObstacles::Plan( INextBot *bot, const PathFollower &path ) && 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 + PROP_DETOUR_REJOIN_PAST_PROP ); + 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 ); @@ -300,8 +315,8 @@ void CNEOBotPathObstacles::Plan( INextBot *bot, const PathFollower &path ) request.rejoin = rejoin; request.rejoinGoal = segments[ rejoinLegEnd ]; request.resumeGoal = goal; - request.regionLo = regionLo.Min( rejoin.AsVector2D() ) - Vector2D( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN ); - request.regionHi = regionHi.Max( rejoin.AsVector2D() ) + Vector2D( PROP_DETOUR_GRID_MARGIN, PROP_DETOUR_GRID_MARGIN ); + request.regionLo = regionLo.Min( rejoin.AsVector2D() ); + request.regionHi = regionHi.Max( rejoin.AsVector2D() ); request.floorLo = floorLo; request.floorHi = floorHi; request.isPathPushable = isPathPushable; @@ -322,23 +337,16 @@ void CNEOBotPathObstacles::LookForBreakable( INextBot *bot, const PathFollower & m_breakable = NULL; CUtlVector< Vector > line; - CUtlVector< const Path::Segment * > segments; - GetPathAhead( path, bot->GetLocomotionInterface()->GetFeet(), PATH_OBSTACLE_BREAKABLE_RANGE, &line, &segments ); + GetPathAhead( path, bot->GetLocomotionInterface()->GetFeet(), PATH_OBSTACLE_BREAKABLE_RANGE, &line ); if ( line.Count() < 2 ) { return; } - Vector lineLo = line[ 0 ]; - Vector lineHi = line[ 0 ]; - for ( int i = 1; i < line.Count(); ++i ) - { - VectorMin( lineLo, line[ i ], lineLo ); - VectorMax( lineHi, line[ i ], lineHi ); - } + Vector lineLo, lineHi; + GetLineBounds( line, FLT_MAX, &lineLo, &lineHi ); - CUtlVector< CBaseEntity * > props; CUtlVector< CBaseEntity * > breakables; - FindMovableProps( lineLo, lineHi, &props, bot, &breakables ); + FindBreakables( bot, lineLo, lineHi, &breakables ); m_breakable = FindBreakableInWay( 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 index 5e996ba362..11eade5a2e 100644 --- a/src/game/server/neo/bot/neo_bot_path_obstacles.h +++ b/src/game/server/neo/bot/neo_bot_path_obstacles.h @@ -8,8 +8,7 @@ class PathFollower; //---------------------------------------------------------------------------------------------------------------- // Looks along a bot's path for what the nav mesh cannot know about: -// props that physics or an animation moves, which it steers around with a detour, -// and breakables in the way, which it reports for the bot to clear +// props that move, which it detours around, and breakables in the way, which it reports for the bot to clear class CNEOBotPathObstacles { public: @@ -34,7 +33,7 @@ class CNEOBotPathObstacles 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, when no plan this often looks for them too + 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 index 86fe5dc432..40e4e61399 100644 --- a/src/game/server/neo/bot/neo_bot_prop_detour.cpp +++ b/src/game/server/neo/bot/neo_bot_prop_detour.cpp @@ -21,7 +21,10 @@ 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; -// Cells of the grid laid over the floor a detour may use +// 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; @@ -685,7 +688,7 @@ void CNEOBotPropDetour::Reset() m_isWide = false; m_isPathPushable = false; m_legStart = vec3_origin; - m_restingPropCount = PROP_DETOUR_PROPS_MOVING; + m_restingPropCount = OBSTACLE_PROPS_MOVING; m_searchAgeTimer.Invalidate(); } @@ -693,8 +696,9 @@ void CNEOBotPropDetour::Reset() //---------------------------------------------------------------------------------------------------------------- float CNEOBotPropDetour::Plan( INextBot *bot, const PropDetourRequest_t &request, float lookInterval ) { - Vector2D regionLo = request.regionLo; - Vector2D regionHi = request.regionHi; + 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 @@ -832,7 +836,7 @@ void CNEOBotPropDetour::NoteSearch( const Vector &feet ) // 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 == PROP_DETOUR_PROPS_MOVING || m_searchAgeTimer.IsElapsed() ) + if ( m_restingPropCount == OBSTACLE_PROPS_MOVING || m_searchAgeTimer.IsElapsed() ) { return false; } @@ -845,4 +849,3 @@ bool CNEOBotPropDetour::IsLastSearchValid( INextBot *bot ) const 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 index f94288cc16..f01c0325d1 100644 --- a/src/game/server/neo/bot/neo_bot_prop_detour.h +++ b/src/game/server/neo/bot/neo_bot_prop_detour.h @@ -6,7 +6,7 @@ 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 it may use, and whether only light props were in the way +// 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; @@ -65,7 +65,7 @@ class CNEOBotPropDetour 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 (PROP_DETOUR_PROPS_MOVING if any was awake) + // 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; From 9e4abebe3d318403d2042923f737b086f0ce0a49 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 4 Oct 2026 11:58:10 -0600 Subject: [PATCH 7/8] Report a breakable only when nothing stands before it The look clipped the body against each breakable on its own, so a pane behind a crate, a door or a wall was reported and the bot stood still shooting the obstacle. When the look has a breakable, the body hull is traced along the legs up to it, and the breakable is kept only if it is the first thing met. The trace runs only on looks that found one. --- .../server/neo/bot/neo_bot_path_obstacles.cpp | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/game/server/neo/bot/neo_bot_path_obstacles.cpp b/src/game/server/neo/bot/neo_bot_path_obstacles.cpp index 99c5374b19..003916f9a6 100644 --- a/src/game/server/neo/bot/neo_bot_path_obstacles.cpp +++ b/src/game/server/neo/bot/neo_bot_path_obstacles.cpp @@ -85,8 +85,9 @@ static void GetLineBounds( const CUtlVector< Vector > &line, float range, Vector //---------------------------------------------------------------------------------------------------------------- -// The breakable the body meets first walking the line, within PATH_OBSTACLE_BREAKABLE_RANGE -static CBaseEntity *FindBreakableInWay( const BodyBox_t &body, const CUtlVector< Vector > &line, const CUtlVector< CBaseEntity * > &breakables ) +// 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; @@ -95,6 +96,7 @@ static CBaseEntity *FindBreakableInWay( const BodyBox_t &body, const CUtlVector< reachHi += body.maxs; CBaseEntity *nearest = NULL; + int nearestLeg = -1; float nearestDistance = PATH_OBSTACLE_BREAKABLE_RANGE; FOR_EACH_VEC( breakables, i ) { @@ -113,6 +115,7 @@ static CBaseEntity *FindBreakableInWay( const BodyBox_t &body, const CUtlVector< 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; } @@ -121,7 +124,24 @@ static CBaseEntity *FindBreakableInWay( const BodyBox_t &body, const CUtlVector< } } - return nearest; + if ( !nearest ) + { + return NULL; + } + + // what the body meets first on the way may not be the breakable: a crate, a door or a wall in front of it, + // so walk the legs up to it with the body and keep the breakable only if it is the first thing met + for ( int leg = 0; leg <= nearestLeg; ++leg ) + { + trace_t result; + UTIL_TraceHull( line[ leg ], line[ leg + 1 ], body.mins, body.maxs, MASK_PLAYERSOLID, bot->GetEntity(), COLLISION_GROUP_NONE, &result ); + if ( result.DidHit() ) + { + return ( result.m_pEnt == nearest ) ? nearest : NULL; + } + } + + return NULL; } //---------------------------------------------------------------------------------------------------------------- @@ -258,7 +278,7 @@ void CNEOBotPathObstacles::Plan( INextBot *bot, const PathFollower &path ) CollectProps( bot, lineLo, lineHi, &obstacles, &breakables ); const BodyBox_t body = GetBodyBox( bot ); - m_breakable = FindBreakableInWay( body, line, breakables ); + m_breakable = FindBreakableInWay( bot, body, line, breakables ); m_breakableTimer.Start( PATH_OBSTACLE_LOOK_INTERVAL ); if ( obstacles.Count() == 0 ) @@ -348,5 +368,5 @@ void CNEOBotPathObstacles::LookForBreakable( INextBot *bot, const PathFollower & CUtlVector< CBaseEntity * > breakables; FindBreakables( bot, lineLo, lineHi, &breakables ); - m_breakable = FindBreakableInWay( GetBodyBox( bot ), line, breakables ); + m_breakable = FindBreakableInWay( bot, GetBodyBox( bot ), line, breakables ); } From be9602438cafb2fd0f9b1721734bed40aa3b38de Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 4 Oct 2026 12:03:31 -0600 Subject: [PATCH 8/8] Let a breakable's hull check pass through players Two bots side by side at a pane each met the other first in the hull check, so neither reported the pane and both stood there until the stuck monitor fired. Players move, and the clear behavior already backs off from a teammate in its line of fire. --- src/game/server/neo/bot/neo_bot_path_obstacles.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/game/server/neo/bot/neo_bot_path_obstacles.cpp b/src/game/server/neo/bot/neo_bot_path_obstacles.cpp index 003916f9a6..a5763938c4 100644 --- a/src/game/server/neo/bot/neo_bot_path_obstacles.cpp +++ b/src/game/server/neo/bot/neo_bot_path_obstacles.cpp @@ -129,12 +129,13 @@ static CBaseEntity *FindBreakableInWay( INextBot *bot, const BodyBox_t &body, co return NULL; } - // what the body meets first on the way may not be the breakable: a crate, a door or a wall in front of it, - // so walk the legs up to it with the body and keep the breakable only if it is the first thing met + // 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, bot->GetEntity(), COLLISION_GROUP_NONE, &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;