Changeset View
Changeset View
Standalone View
Standalone View
ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp
Show First 20 Lines • Show All 64 Lines • ▼ Show 20 Lines | |||||
/** | /** | ||||
* If we are this close to our target entity/point, then think about heading | * If we are this close to our target entity/point, then think about heading | ||||
* for it in a straight line instead of pathfinding. | * for it in a straight line instead of pathfinding. | ||||
*/ | */ | ||||
static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); | 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. | * 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. | * Experimentally, this number needs to be somewhat high or moving groups of units will lead to stuck units. | ||||
* However, too high means units will look idle for a long time when they are failing to move. | * However, too high means units will look idle for a long time when they are failing to move. | ||||
* TODO: if UnitMotion could send differentiated "unreachable" and "currently stuck" failing messages, | * TODO: if UnitMotion could send differentiated "unreachable" and "currently stuck" failing messages, | ||||
* this could probably be lowered. | * this could probably be lowered. | ||||
* TODO: when unit pushing is implemented, this number can probably be lowered. | * TODO: when unit pushing is implemented, this number can probably be lowered. | ||||
*/ | */ | ||||
static const u8 MAX_FAILED_PATH_COMPUTATIONS = 15; | static const u8 MAX_FAILED_PATH_COMPUTATIONS = 15; | ||||
Show All 37 Lines | public: | ||||
bool m_FacePointAfterMove; | bool m_FacePointAfterMove; | ||||
// Number of path computations that failed (in a row). | // Number of path computations that failed (in a row). | ||||
// When this gets above MAX_FAILED_PATH_COMPUTATIONS, inform other components | // When this gets above MAX_FAILED_PATH_COMPUTATIONS, inform other components | ||||
// that the move will likely fail. | // that the move will likely fail. | ||||
u8 m_FailedPathComputations = 0; | 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 { | struct Ticket { | ||||
u32 m_Ticket = 0; // asynchronous request ID we're waiting for, or 0 if none | u32 m_Ticket = 0; // asynchronous request ID we're waiting for, or 0 if none | ||||
enum Type { | enum Type { | ||||
SHORT_PATH, | SHORT_PATH, | ||||
LONG_PATH | LONG_PATH | ||||
} m_Type = SHORT_PATH; // Pick some default value to avoid UB. | } m_Type = SHORT_PATH; // Pick some default value to avoid UB. | ||||
void clear() { m_Ticket = 0; } | void clear() { m_Ticket = 0; } | ||||
▲ Show 20 Lines • Show All 93 Lines • ▼ Show 20 Lines | public: | ||||
void SerializeCommon(S& serialize) | void SerializeCommon(S& serialize) | ||||
{ | { | ||||
serialize.StringASCII("pass class", m_PassClassName, 0, 64); | serialize.StringASCII("pass class", m_PassClassName, 0, 64); | ||||
serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket.m_Ticket); | serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket.m_Ticket); | ||||
SerializeU8_Enum<Ticket::Type, Ticket::Type::LONG_PATH>()(serialize, "ticket type", m_ExpectedPathTicket.m_Type); | 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.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); | SerializeU8_Enum<MoveRequest::Type, MoveRequest::Type::OFFSET>()(serialize, "target type", m_MoveRequest.m_Type); | ||||
serialize.NumberU32_Unbounded("target entity", m_MoveRequest.m_Entity); | serialize.NumberU32_Unbounded("target entity", m_MoveRequest.m_Entity); | ||||
serialize.NumberFixed_Unbounded("target pos x", m_MoveRequest.m_Position.X); | serialize.NumberFixed_Unbounded("target pos x", m_MoveRequest.m_Position.X); | ||||
serialize.NumberFixed_Unbounded("target pos y", m_MoveRequest.m_Position.Y); | serialize.NumberFixed_Unbounded("target pos y", m_MoveRequest.m_Position.Y); | ||||
serialize.NumberFixed_Unbounded("target min range", m_MoveRequest.m_MinRange); | serialize.NumberFixed_Unbounded("target min range", m_MoveRequest.m_MinRange); | ||||
serialize.NumberFixed_Unbounded("target max range", m_MoveRequest.m_MaxRange); | serialize.NumberFixed_Unbounded("target max range", m_MoveRequest.m_MaxRange); | ||||
▲ Show 20 Lines • Show All 390 Lines • ▼ Show 20 Lines | void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path) | ||||
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle()); | CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle()); | ||||
if (!cmpPosition || !cmpPosition->IsInWorld()) | if (!cmpPosition || !cmpPosition->IsInWorld()) | ||||
{ | { | ||||
// We will probably fail to move so inform components but keep on trying anyways. | // We will probably fail to move so inform components but keep on trying anyways. | ||||
MoveFailed(); | MoveFailed(); | ||||
return; | return; | ||||
} | } | ||||
CFixedVector2D pos = cmpPosition->GetPosition2D(); | |||||
if (ticketType == Ticket::LONG_PATH) | if (ticketType == Ticket::LONG_PATH) | ||||
{ | { | ||||
m_LongPath = path; | m_LongPath = path; | ||||
m_PretendLongPathIsCorrect = false; | |||||
// If there's no waypoints then we couldn't get near the target. | // 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 | // 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 | // (via the short pathfinder over the next turns), so if we're stuck and the user clicks | ||||
// close enough to the unit then we can probably get unstuck | // close enough to the unit then we can probably get unstuck | ||||
// NB: this relies on HandleObstructedMove requesting short paths if we still have long waypoints. | // NB: this relies on HandleObstructedMove requesting short paths if we still have long waypoints. | ||||
if (m_LongPath.m_Waypoints.empty()) | if (m_LongPath.m_Waypoints.empty()) | ||||
{ | { | ||||
IncrementFailedPathComputationAndMaybeNotify(); | IncrementFailedPathComputationAndMaybeNotify(); | ||||
CFixedVector2D targetPos; | CFixedVector2D targetPos; | ||||
if (ComputeTargetPosition(targetPos)) | if (ComputeTargetPosition(targetPos)) | ||||
m_LongPath.m_Waypoints.emplace_back(Waypoint{ targetPos.X, targetPos.Y }); | 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; | return; | ||||
} | } | ||||
m_ShortPath = path; | m_ShortPath = path; | ||||
if (!m_ShortPath.m_Waypoints.empty()) | if (!m_ShortPath.m_Waypoints.empty()) | ||||
return; | return; | ||||
// Don't notify if we are a formation member - we can occasionally be stuck for a long time | // Don't notify if we are a formation member - we can occasionally be stuck for a long time | ||||
// if our current offset is unreachable. | // if our current offset is unreachable. | ||||
if (!IsFormationMember()) | if (!IsFormationMember()) | ||||
IncrementFailedPathComputationAndMaybeNotify(); | IncrementFailedPathComputationAndMaybeNotify(); | ||||
CFixedVector2D pos = cmpPosition->GetPosition2D(); | |||||
// If there's no waypoints then we couldn't get near the target | // 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, | // 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). | // it might be obstructed (e.g. by idle entities which the long-range pathfinder doesn't see). | ||||
if (!m_LongPath.m_Waypoints.empty()) | if (!m_LongPath.m_Waypoints.empty()) | ||||
{ | { | ||||
m_LongPath.m_Waypoints.pop_back(); | m_LongPath.m_Waypoints.pop_back(); | ||||
if (!m_LongPath.m_Waypoints.empty()) | if (!m_LongPath.m_Waypoints.empty()) | ||||
{ | { | ||||
▲ Show 20 Lines • Show All 41 Lines • ▼ Show 20 Lines | void CCmpUnitMotion::Move(fixed dt) | ||||
CFixedVector2D initialPos = cmpPosition->GetPosition2D(); | CFixedVector2D initialPos = cmpPosition->GetPosition2D(); | ||||
// Keep track of the current unit's position during the update | // Keep track of the current unit's position during the update | ||||
CFixedVector2D pos = initialPos; | CFixedVector2D pos = initialPos; | ||||
// If we're chasing a potentially-moving unit and are currently close | // 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 | // 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 | // 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); | bool wasObstructed = PerformMove(dt, m_ShortPath, m_LongPath, pos); | ||||
// Update our speed over this turn so that the visual actor shows the correct animation. | // Update our speed over this turn so that the visual actor shows the correct animation. | ||||
if (pos == initialPos) | if (pos == initialPos) | ||||
UpdateMovementState(fixed::Zero()); | UpdateMovementState(fixed::Zero()); | ||||
else | else | ||||
{ | { | ||||
Show All 10 Lines | void CCmpUnitMotion::Move(fixed dt) | ||||
if (wasObstructed && HandleObstructedMove()) | if (wasObstructed && HandleObstructedMove()) | ||||
return; | return; | ||||
else if (!wasObstructed) | else if (!wasObstructed) | ||||
m_FailedPathComputations = 0; | m_FailedPathComputations = 0; | ||||
// We may need to recompute our path sometimes (e.g. if our target moves). | // 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. | // Since we request paths asynchronously anyways, this does not need to be done before moving. | ||||
if (PathingUpdateNeeded(pos)) | if (!wentStraight && PathingUpdateNeeded(pos)) | ||||
{ | { | ||||
PathGoal goal; | PathGoal goal; | ||||
if (ComputeGoal(goal, m_MoveRequest)) | if (ComputeGoal(goal, m_MoveRequest)) | ||||
BeginPathing(pos, goal); | BeginPathing(pos, goal); | ||||
} | } | ||||
} | } | ||||
bool CCmpUnitMotion::PossiblyAtDestination() const | bool CCmpUnitMotion::PossiblyAtDestination() const | ||||
▲ Show 20 Lines • Show All 225 Lines • ▼ Show 20 Lines | |||||
{ | { | ||||
if (m_MoveRequest.m_Type == MoveRequest::NONE) | if (m_MoveRequest.m_Type == MoveRequest::NONE) | ||||
return false; | return false; | ||||
CFixedVector2D targetPos; | CFixedVector2D targetPos; | ||||
if (!ComputeTargetPosition(targetPos)) | if (!ComputeTargetPosition(targetPos)) | ||||
return false; | return false; | ||||
if (m_PretendLongPathIsCorrect) | |||||
return false; | |||||
if (PossiblyAtDestination()) | if (PossiblyAtDestination()) | ||||
return false; | return false; | ||||
// Get the obstruction shape and translate it where we estimate the target to be. | // Get the obstruction shape and translate it where we estimate the target to be. | ||||
ICmpObstructionManager::ObstructionSquare estimatedTargetShape; | ICmpObstructionManager::ObstructionSquare estimatedTargetShape; | ||||
if (m_MoveRequest.m_Type == MoveRequest::ENTITY) | if (m_MoveRequest.m_Type == MoveRequest::ENTITY) | ||||
{ | { | ||||
CmpPtr<ICmpObstruction> cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity); | CmpPtr<ICmpObstruction> cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity); | ||||
Show All 12 Lines | bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const | ||||
// Translate our own obstruction shape to our last waypoint or our current position, lacking that. | // Translate our own obstruction shape to our last waypoint or our current position, lacking that. | ||||
if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty()) | if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty()) | ||||
{ | { | ||||
shape.x = from.X; | shape.x = from.X; | ||||
shape.z = from.Y; | shape.z = from.Y; | ||||
} | } | ||||
else | 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.x = lastWaypoint.x; | ||||
shape.z = lastWaypoint.z; | shape.z = lastWaypoint.z; | ||||
} | } | ||||
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity()); | CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity()); | ||||
ENSURE(cmpObstructionManager); | ENSURE(cmpObstructionManager); | ||||
if (cmpObstructionManager->AreShapesInRange(shape, estimatedTargetShape, | // Increase the ranges with distance, to avoid recomputing every turn against units that are moving and far-away for example. | ||||
m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false)) | 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 false; | ||||
return true; | return true; | ||||
} | } | ||||
bool CCmpUnitMotion::PathIsShort(const WaypointPath& path, const CFixedVector2D& from, entity_pos_t minDistance) const | bool CCmpUnitMotion::PathIsShort(const WaypointPath& path, const CFixedVector2D& from, entity_pos_t minDistance) const | ||||
{ | { | ||||
CFixedVector2D prev = from; | CFixedVector2D prev = from; | ||||
▲ Show 20 Lines • Show All 250 Lines • ▼ Show 20 Lines | if (!ComputeGoal(goal, moveRequest)) | ||||
return false; | return false; | ||||
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle()); | CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle()); | ||||
if (!cmpPosition || !cmpPosition->IsInWorld()) | if (!cmpPosition || !cmpPosition->IsInWorld()) | ||||
return false; | return false; | ||||
m_MoveRequest = moveRequest; | m_MoveRequest = moveRequest; | ||||
m_FailedPathComputations = 0; | m_FailedPathComputations = 0; | ||||
m_PretendLongPathIsCorrect = false; | |||||
BeginPathing(cmpPosition->GetPosition2D(), goal); | BeginPathing(cmpPosition->GetPosition2D(), goal); | ||||
return true; | return true; | ||||
} | } | ||||
bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) | bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) | ||||
{ | { | ||||
PROFILE("MoveToTargetRange"); | PROFILE("MoveToTargetRange"); | ||||
MoveRequest moveRequest(target, minRange, maxRange); | MoveRequest moveRequest(target, minRange, maxRange); | ||||
PathGoal goal; | PathGoal goal; | ||||
if (!ComputeGoal(goal, moveRequest)) | if (!ComputeGoal(goal, moveRequest)) | ||||
return false; | return false; | ||||
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle()); | CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle()); | ||||
if (!cmpPosition || !cmpPosition->IsInWorld()) | if (!cmpPosition || !cmpPosition->IsInWorld()) | ||||
return false; | return false; | ||||
m_MoveRequest = moveRequest; | m_MoveRequest = moveRequest; | ||||
m_FailedPathComputations = 0; | m_FailedPathComputations = 0; | ||||
m_PretendLongPathIsCorrect = false; | |||||
BeginPathing(cmpPosition->GetPosition2D(), goal); | BeginPathing(cmpPosition->GetPosition2D(), goal); | ||||
return true; | return true; | ||||
} | } | ||||
void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) | void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) | ||||
{ | { | ||||
MoveRequest moveRequest(target, CFixedVector2D(x, z)); | MoveRequest moveRequest(target, CFixedVector2D(x, z)); | ||||
PathGoal goal; | PathGoal goal; | ||||
if (!ComputeGoal(goal, moveRequest)) | if (!ComputeGoal(goal, moveRequest)) | ||||
return; | return; | ||||
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), target); | CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), target); | ||||
if (!cmpPosition || !cmpPosition->IsInWorld()) | if (!cmpPosition || !cmpPosition->IsInWorld()) | ||||
return; | return; | ||||
m_MoveRequest = moveRequest; | m_MoveRequest = moveRequest; | ||||
m_FailedPathComputations = 0; | m_FailedPathComputations = 0; | ||||
m_PretendLongPathIsCorrect = false; | |||||
BeginPathing(cmpPosition->GetPosition2D(), goal); | BeginPathing(cmpPosition->GetPosition2D(), goal); | ||||
} | } | ||||
void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector<SOverlayLine>& lines, CColor color) | void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector<SOverlayLine>& lines, CColor color) | ||||
{ | { | ||||
bool floating = false; | bool floating = false; | ||||
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle()); | CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle()); | ||||
Show All 39 Lines |
Wildfire Games · Phabricator