Unit Motion - optimisations to avoid recomputing paths too often

Three changes:
- Assume a certain incertain based on distance to the target, to avoid
recompute paths every turn when the target is far way and moving.
- Handle cases where the target is unreachable to the long-range
pathfinder and we would be recomputing every turn.
- If we went straight, assume we don't need to recompute a path.

These together make moving entities recompute paths far less often,
speeding up the game.

Differential Revision: https://code.wildfiregames.com/D2066
This was SVN commit r22474.
This commit is contained in:
wraitii 2019-07-14 11:08:15 +00:00
parent 5c642611c4
commit b43904aae1

View File

@ -69,6 +69,13 @@ static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TIL
*/
static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4);
/**
* To avoid recomputing paths too often, have some leeway for target range checks
* based on our distance to the target. Increase that incertainty by one navcell
* for every this many tiles of distance.
*/
static const entity_pos_t TARGET_UNCERTAINTY_MULTIPLIER = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2);
/**
* When we fail more than this many path computations in a row, inform other components that the move will fail.
* Experimentally, this number needs to be somewhat high or moving groups of units will lead to stuck units.
@ -123,6 +130,11 @@ public:
// that the move will likely fail.
u8 m_FailedPathComputations = 0;
// If true, PathingUpdateNeeded returns false always.
// This is an optimisation against unreachable goals, where otherwise we would always
// be recomputing a path.
bool m_PretendLongPathIsCorrect = false;
struct Ticket {
u32 m_Ticket = 0; // asynchronous request ID we're waiting for, or 0 if none
enum Type {
@ -232,6 +244,7 @@ public:
SerializeU8_Enum<Ticket::Type, Ticket::Type::LONG_PATH>()(serialize, "ticket type", m_ExpectedPathTicket.m_Type);
serialize.NumberU8("failed path computations", m_FailedPathComputations, 0, 255);
serialize.Bool("pretendLongPathIsCorrect", m_PretendLongPathIsCorrect);
SerializeU8_Enum<MoveRequest::Type, MoveRequest::Type::OFFSET>()(serialize, "target type", m_MoveRequest.m_Type);
serialize.NumberU32_Unbounded("target entity", m_MoveRequest.m_Entity);
@ -638,10 +651,14 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
return;
}
CFixedVector2D pos = cmpPosition->GetPosition2D();
if (ticketType == Ticket::LONG_PATH)
{
m_LongPath = path;
m_PretendLongPathIsCorrect = false;
// If there's no waypoints then we couldn't get near the target.
// Sort of hack: Just try going directly to the goal point instead
// (via the short pathfinder over the next turns), so if we're stuck and the user clicks
@ -654,6 +671,16 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
if (ComputeTargetPosition(targetPos))
m_LongPath.m_Waypoints.emplace_back(Waypoint{ targetPos.X, targetPos.Y });
}
// If this new path won't put us in range, it's highly likely that we are going somewhere unreachable.
// This means we will try to recompute the path every turn.
// To avoid this, act as if our current path leads us to the correct destination.
// (we will still fail the move when we arrive to the best possible position, and if we were blocked by
// an obstruction and it goes away we will notice when getting there as having no waypoint goes through
// HandleObstructedMove, so this is safe).
// TODO: For now, we won't warn components straight away as that could lead to units idling earlier than expected,
// but it should be done someday when the message can differentiate between different failure causes.
else if (PathingUpdateNeeded(pos))
m_PretendLongPathIsCorrect = true;
return;
}
@ -667,8 +694,6 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
if (!IsFormationMember())
IncrementFailedPathComputationAndMaybeNotify();
CFixedVector2D pos = cmpPosition->GetPosition2D();
// If there's no waypoints then we couldn't get near the target
// If we're globally following a long path, try to remove the next waypoint,
// it might be obstructed (e.g. by idle entities which the long-range pathfinder doesn't see).
@ -726,7 +751,7 @@ void CCmpUnitMotion::Move(fixed dt)
// If we're chasing a potentially-moving unit and are currently close
// enough to its current position, and we can head in a straight line
// to it, then throw away our current path and go straight to it
TryGoingStraightToTarget(initialPos);
bool wentStraight = TryGoingStraightToTarget(initialPos);
bool wasObstructed = PerformMove(dt, m_ShortPath, m_LongPath, pos);
@ -753,7 +778,7 @@ void CCmpUnitMotion::Move(fixed dt)
// We may need to recompute our path sometimes (e.g. if our target moves).
// Since we request paths asynchronously anyways, this does not need to be done before moving.
if (PathingUpdateNeeded(pos))
if (!wentStraight && PathingUpdateNeeded(pos))
{
PathGoal goal;
if (ComputeGoal(goal, m_MoveRequest))
@ -995,6 +1020,9 @@ bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const
if (!ComputeTargetPosition(targetPos))
return false;
if (m_PretendLongPathIsCorrect)
return false;
if (PossiblyAtDestination())
return false;
@ -1023,7 +1051,7 @@ bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const
}
else
{
const Waypoint& lastWaypoint = m_ShortPath.m_Waypoints.empty() ? m_LongPath.m_Waypoints.front() : m_ShortPath.m_Waypoints.front();
const Waypoint& lastWaypoint = m_LongPath.m_Waypoints.empty() ? m_ShortPath.m_Waypoints.front() : m_LongPath.m_Waypoints.front();
shape.x = lastWaypoint.x;
shape.z = lastWaypoint.z;
}
@ -1031,8 +1059,17 @@ bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
ENSURE(cmpObstructionManager);
if (cmpObstructionManager->AreShapesInRange(shape, estimatedTargetShape,
m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false))
// Increase the ranges with distance, to avoid recomputing every turn against units that are moving and far-away for example.
entity_pos_t distance = (from - CFixedVector2D(estimatedTargetShape.x, estimatedTargetShape.z)).Length();
// When in straight-path distance, we want perfect detection.
distance = std::max(distance - DIRECT_PATH_RANGE, entity_pos_t::Zero());
// TODO: it could be worth computing this based on time to collision instead of linear distance.
entity_pos_t minRange = std::max(m_MoveRequest.m_MinRange - distance / TARGET_UNCERTAINTY_MULTIPLIER, entity_pos_t::Zero());
entity_pos_t maxRange = m_MoveRequest.m_MaxRange < entity_pos_t::Zero() ? m_MoveRequest.m_MaxRange :
m_MoveRequest.m_MaxRange + distance / TARGET_UNCERTAINTY_MULTIPLIER;
if (cmpObstructionManager->AreShapesInRange(shape, estimatedTargetShape, minRange, maxRange, false))
return false;
return true;
@ -1299,6 +1336,7 @@ bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos
m_MoveRequest = moveRequest;
m_FailedPathComputations = 0;
m_PretendLongPathIsCorrect = false;
BeginPathing(cmpPosition->GetPosition2D(), goal);
@ -1322,6 +1360,7 @@ bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange
m_MoveRequest = moveRequest;
m_FailedPathComputations = 0;
m_PretendLongPathIsCorrect = false;
BeginPathing(cmpPosition->GetPosition2D(), goal);
@ -1342,6 +1381,7 @@ void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, e
m_MoveRequest = moveRequest;
m_FailedPathComputations = 0;
m_PretendLongPathIsCorrect = false;
BeginPathing(cmpPosition->GetPosition2D(), goal);
}