mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
The 0 tolerance to prevent the "waltzing" that was set before this cannot happen anymore as we now since check for being at destination before sending a move request in UnitAI. Adding a new small tolerance now prevents some small movement adjustments of formation members near their destination. Fixes #8592
1976 lines
71 KiB
C++
1976 lines
71 KiB
C++
/* Copyright (C) 2026 Wildfire Games.
|
|
* This file is part of 0 A.D.
|
|
*
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#ifndef INCLUDED_CCMPUNITMOTION
|
|
#define INCLUDED_CCMPUNITMOTION
|
|
|
|
#include "graphics/Color.h"
|
|
#include "graphics/Overlay.h"
|
|
#include "lib/debug.h"
|
|
#include "lib/types.h"
|
|
#include "maths/Fixed.h"
|
|
#include "maths/FixedVector2D.h"
|
|
#include "maths/FixedVector3D.h"
|
|
#include "maths/MathUtil.h"
|
|
#include "ps/CLogger.h"
|
|
#include "ps/Profile.h"
|
|
#include "renderer/Scene.h"
|
|
#include "simulation2/MessageTypes.h"
|
|
#include "simulation2/components/CCmpUnitMotionManager.h"
|
|
#include "simulation2/components/ICmpObstruction.h"
|
|
#include "simulation2/components/ICmpObstructionManager.h"
|
|
#include "simulation2/components/ICmpPathfinder.h"
|
|
#include "simulation2/components/ICmpPosition.h"
|
|
#include "simulation2/components/ICmpUnitMotion.h"
|
|
#include "simulation2/components/ICmpUnitMotionManager.h"
|
|
#include "simulation2/components/ICmpValueModificationManager.h"
|
|
#include "simulation2/components/ICmpVisual.h"
|
|
#include "simulation2/helpers/PathGoal.h"
|
|
#include "simulation2/helpers/Pathfinding.h"
|
|
#include "simulation2/helpers/Position.h"
|
|
#include "simulation2/helpers/Render.h"
|
|
#include "simulation2/serialization/SerializeTemplates.h"
|
|
#include "simulation2/serialization/SerializedPathfinder.h"
|
|
#include "simulation2/serialization/SerializedTypes.h"
|
|
#include "simulation2/system/Component.h"
|
|
#include "simulation2/system/Entity.h"
|
|
#include "simulation2/system/Message.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
// NB: this implementation of ICmpUnitMotion is very tightly coupled with UnitMotionManager.
|
|
// As such, both are compiled in the same TU.
|
|
|
|
// For debugging; units will start going straight to the target
|
|
// instead of calling the pathfinder
|
|
#define DISABLE_PATHFINDER 0
|
|
|
|
namespace
|
|
{
|
|
/**
|
|
* Min/Max range to restrict short path queries to. (Larger ranges are (much) slower,
|
|
* smaller ranges might miss some legitimate routes around large obstacles.)
|
|
* NB: keep the max-range in sync with the vertex pathfinder "move the search space" heuristic.
|
|
*/
|
|
constexpr entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(12 * Pathfinding::NAVCELL_SIZE_INT);
|
|
constexpr entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(56 * Pathfinding::NAVCELL_SIZE_INT);
|
|
constexpr entity_pos_t SHORT_PATH_SEARCH_RANGE_INCREMENT = entity_pos_t::FromInt(4 * Pathfinding::NAVCELL_SIZE_INT);
|
|
constexpr u8 SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY = 1;
|
|
|
|
/**
|
|
* When using the short-pathfinder to rejoin a long-path waypoint, aim for a circle of this radius around the waypoint.
|
|
*/
|
|
constexpr entity_pos_t SHORT_PATH_LONG_WAYPOINT_RANGE = entity_pos_t::FromInt(4 * Pathfinding::NAVCELL_SIZE_INT);
|
|
|
|
/**
|
|
* Minimum distance to goal for a long path request
|
|
*/
|
|
constexpr entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(16 * Pathfinding::NAVCELL_SIZE_INT);
|
|
|
|
/**
|
|
* If we are this close to our target entity/point, then think about heading
|
|
* for it in a straight line instead of pathfinding.
|
|
*/
|
|
constexpr entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(24 * Pathfinding::NAVCELL_SIZE_INT);
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
constexpr entity_pos_t TARGET_UNCERTAINTY_MULTIPLIER = entity_pos_t::FromInt(8 * Pathfinding::NAVCELL_SIZE_INT);
|
|
|
|
/**
|
|
* When following a known imperfect path (i.e. a path that won't take us in range of our goal
|
|
* we still recompute a new path every N turn to adapt to moving targets (for example, ships that must pickup
|
|
* units may easily end up in this state, they still need to adjust to moving units).
|
|
* This is rather arbitrary and mostly for simplicity & optimisation (a better recomputing algorithm
|
|
* would not need this).
|
|
*/
|
|
constexpr u8 KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN = 12;
|
|
|
|
/**
|
|
* When we fail to move this many turns 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.
|
|
* 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,
|
|
* this could probably be lowered.
|
|
*/
|
|
constexpr u8 MAX_FAILED_MOVEMENTS = 35;
|
|
|
|
/**
|
|
* When computing paths but failing to move, we want to occasionally alternate pathfinder systems
|
|
* to avoid getting stuck (the short pathfinder can unstuck the long-range one and vice-versa, depending).
|
|
*/
|
|
constexpr u8 ALTERNATE_PATH_TYPE_DELAY = 3;
|
|
constexpr u8 ALTERNATE_PATH_TYPE_EVERY = 6;
|
|
|
|
/**
|
|
* Units can occasionally get stuck near corners. The cause is a mismatch between CheckMovement and the short pathfinder.
|
|
* The problem is the short pathfinder finds an impassable path when units are right on an obstruction edge.
|
|
* Fixing this math mismatch is perhaps possible, but fixing it in UM is rather easy: just try backing up a bit
|
|
* and that will probably un-stuck the unit. This is the 'failed movement' turn on which to try that.
|
|
*/
|
|
constexpr u8 BACKUP_HACK_DELAY = 10;
|
|
|
|
/**
|
|
* After this many failed computations, start sending "VERY_OBSTRUCTED" messages instead.
|
|
* Should probably be larger than ALTERNATE_PATH_TYPE_DELAY.
|
|
*/
|
|
constexpr u8 VERY_OBSTRUCTED_THRESHOLD = 10;
|
|
|
|
struct PathColorPalette
|
|
{
|
|
CColor longPath;
|
|
CColor shortPath;
|
|
};
|
|
|
|
constexpr PathColorPalette REGULAR_UNIT_PALETTE{{1, 1, 1, 1}, {1, 0, 0, 1}};
|
|
constexpr PathColorPalette FORMATION_CONTROLLER_PALETTE{{0, 0, 1, 1}, {0, 1, 0, 1}};
|
|
} // anonymous namespace
|
|
|
|
class CCmpUnitMotion final : public ICmpUnitMotion
|
|
{
|
|
friend class CCmpUnitMotionManager;
|
|
public:
|
|
static void ClassInit(CComponentManager& componentManager)
|
|
{
|
|
componentManager.SubscribeToMessageType(MT_Create);
|
|
componentManager.SubscribeToMessageType(MT_Destroy);
|
|
componentManager.SubscribeToMessageType(MT_PathResult);
|
|
componentManager.SubscribeToMessageType(MT_OwnershipChanged);
|
|
componentManager.SubscribeToMessageType(MT_ValueModification);
|
|
componentManager.SubscribeToMessageType(MT_MovementObstructionChanged);
|
|
componentManager.SubscribeToMessageType(MT_Deserialized);
|
|
}
|
|
|
|
DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
|
|
|
|
bool m_DebugOverlayEnabled;
|
|
std::vector<SOverlayLine> m_DebugOverlayLongPathLines;
|
|
std::vector<SOverlayLine> m_DebugOverlayShortPathLines;
|
|
|
|
// Template state:
|
|
|
|
bool m_IsFormationController;
|
|
|
|
fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier, m_TemplateAcceleration, m_TemplateWeight;
|
|
pass_class_t m_PassClass;
|
|
std::string m_PassClassName;
|
|
|
|
// Dynamic state:
|
|
|
|
entity_pos_t m_Clearance;
|
|
|
|
// cached for efficiency
|
|
fixed m_WalkSpeed, m_RunMultiplier;
|
|
|
|
bool m_FacePointAfterMove;
|
|
|
|
// Whether the unit participates in pushing.
|
|
bool m_Pushing = false;
|
|
|
|
// Whether the unit blocks movement (& is blocked by movement blockers)
|
|
// Cached from ICmpObstruction.
|
|
bool m_BlockMovement = false;
|
|
|
|
// Internal counter used when recovering from obstructed movement.
|
|
// Most notably, increases the search range of the vertex pathfinder.
|
|
// See HandleObstructedMove() for more details.
|
|
u8 m_FailedMovements = 0;
|
|
|
|
// If > 0, PathingUpdateNeeded returns false always.
|
|
// This exists because the goal may be unreachable to the short/long pathfinder.
|
|
// In such cases, we would compute inacceptable paths and PathingUpdateNeeded would trigger every turn,
|
|
// which would be quite bad for performance.
|
|
// To avoid that, when we know the new path is imperfect, treat it as OK and follow it anyways.
|
|
// When reaching the end, we'll go through HandleObstructedMove and reset regardless.
|
|
// To still recompute now and then (the target may be moving), this is a countdown decremented on each frame.
|
|
u8 m_FollowKnownImperfectPathCountdown = 0;
|
|
|
|
struct Ticket {
|
|
u32 m_Ticket = 0; // asynchronous request ID we're waiting for, or 0 if none
|
|
enum Type {
|
|
SHORT_PATH,
|
|
LONG_PATH
|
|
} m_Type = SHORT_PATH; // Pick some default value to avoid UB.
|
|
|
|
void clear() { m_Ticket = 0; }
|
|
} m_ExpectedPathTicket;
|
|
|
|
struct MoveRequest {
|
|
enum Type {
|
|
NONE,
|
|
POINT,
|
|
ENTITY,
|
|
OFFSET
|
|
} m_Type = NONE;
|
|
entity_id_t m_Entity = INVALID_ENTITY;
|
|
CFixedVector2D m_Position;
|
|
entity_pos_t m_MinRange, m_MaxRange;
|
|
|
|
// For readability
|
|
CFixedVector2D GetOffset() const { return m_Position; };
|
|
|
|
MoveRequest() = default;
|
|
MoveRequest(CFixedVector2D pos, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(POINT), m_Position(pos), m_MinRange(minRange), m_MaxRange(maxRange) {};
|
|
MoveRequest(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(ENTITY), m_Entity(target), m_MinRange(minRange), m_MaxRange(maxRange) {};
|
|
MoveRequest(entity_id_t target, CFixedVector2D offset) : m_Type(OFFSET), m_Entity(target), m_Position(offset) {};
|
|
} m_MoveRequest;
|
|
|
|
// If this is not INVALID_ENTITY, the unit is a formation member.
|
|
entity_id_t m_FormationController = INVALID_ENTITY;
|
|
|
|
// If the entity moves, it will do so at m_WalkSpeed * m_SpeedMultiplier.
|
|
fixed m_SpeedMultiplier;
|
|
// This caches the resulting speed from m_WalkSpeed * m_SpeedMultiplier for convenience.
|
|
fixed m_Speed;
|
|
|
|
// Mean speed over the last turn.
|
|
fixed m_LastTurnSpeed;
|
|
|
|
// The speed achieved at the end of the current turn.
|
|
fixed m_CurrentSpeed;
|
|
|
|
fixed m_InstantTurnAngle;
|
|
|
|
fixed m_Acceleration;
|
|
|
|
// Currently active paths (storing waypoints in reverse order).
|
|
// The last item in each path is the point we're currently heading towards.
|
|
WaypointPath m_LongPath;
|
|
WaypointPath m_ShortPath;
|
|
|
|
static std::string GetSchema()
|
|
{
|
|
return
|
|
"<a:help>Provides the unit with the ability to move around the world by itself.</a:help>"
|
|
"<a:example>"
|
|
"<WalkSpeed>7.0</WalkSpeed>"
|
|
"<PassabilityClass>default</PassabilityClass>"
|
|
"</a:example>"
|
|
"<element name='FormationController'>"
|
|
"<data type='boolean'/>"
|
|
"</element>"
|
|
"<element name='WalkSpeed' a:help='Basic movement speed (in meters per second).'>"
|
|
"<ref name='positiveDecimal'/>"
|
|
"</element>"
|
|
"<optional>"
|
|
"<element name='RunMultiplier' a:help='How much faster the unit goes when running (as a multiple of walk speed).'>"
|
|
"<ref name='positiveDecimal'/>"
|
|
"</element>"
|
|
"</optional>"
|
|
"<element name='InstantTurnAngle' a:help='Angle we can turn instantly. Any value greater than pi will disable turning times. Avoid zero since it stops the entity every turn.'>"
|
|
"<ref name='positiveDecimal'/>"
|
|
"</element>"
|
|
"<element name='Acceleration' a:help='Acceleration (in meters per second^2).'>"
|
|
"<ref name='positiveDecimal'/>"
|
|
"</element>"
|
|
"<element name='PassabilityClass' a:help='Identifies the terrain passability class (values are defined in special/pathfinder.xml).'>"
|
|
"<text/>"
|
|
"</element>"
|
|
"<element name='Weight' a:help='Makes this unit both push harder and harder to push. 10 is considered the base value.'>"
|
|
"<ref name='positiveDecimal'/>"
|
|
"</element>"
|
|
"<optional>"
|
|
"<element name='DisablePushing'>"
|
|
"<data type='boolean'/>"
|
|
"</element>"
|
|
"</optional>";
|
|
}
|
|
|
|
void Init(const CParamNode& paramNode) override
|
|
{
|
|
m_IsFormationController = paramNode.GetChild("FormationController").ToBool();
|
|
|
|
m_FacePointAfterMove = true;
|
|
|
|
m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed();
|
|
m_SpeedMultiplier = fixed::FromInt(1);
|
|
m_LastTurnSpeed = m_CurrentSpeed = fixed::Zero();
|
|
|
|
m_RunMultiplier = m_TemplateRunMultiplier = fixed::FromInt(1);
|
|
if (paramNode.GetChild("RunMultiplier").IsOk())
|
|
m_RunMultiplier = m_TemplateRunMultiplier = paramNode.GetChild("RunMultiplier").ToFixed();
|
|
|
|
m_InstantTurnAngle = paramNode.GetChild("InstantTurnAngle").ToFixed();
|
|
|
|
m_Acceleration = m_TemplateAcceleration = paramNode.GetChild("Acceleration").ToFixed();
|
|
|
|
m_TemplateWeight = paramNode.GetChild("Weight").ToFixed();
|
|
|
|
m_PassClassName = paramNode.GetChild("PassabilityClass").ToString();
|
|
SetPassabilityData(m_PassClassName);
|
|
|
|
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
|
|
if (cmpObstruction)
|
|
m_BlockMovement = cmpObstruction->GetBlockMovementFlag(true);
|
|
|
|
SetParticipateInPushing(!paramNode.GetChild("DisablePushing").IsOk() || !paramNode.GetChild("DisablePushing").ToBool());
|
|
|
|
m_DebugOverlayEnabled = false;
|
|
}
|
|
|
|
void Deinit() override
|
|
{
|
|
}
|
|
|
|
template<typename S>
|
|
void SerializeCommon(S& serialize)
|
|
{
|
|
// m_Clearance and m_PassClass are constructed from this.
|
|
serialize.StringASCII("pass class", m_PassClassName, 0, 64);
|
|
|
|
serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket.m_Ticket);
|
|
Serializer(serialize, "ticket type", m_ExpectedPathTicket.m_Type, Ticket::Type::LONG_PATH);
|
|
|
|
serialize.NumberU8_Unbounded("failed movements", m_FailedMovements);
|
|
serialize.NumberU8_Unbounded("followknownimperfectpath", m_FollowKnownImperfectPathCountdown);
|
|
|
|
Serializer(serialize, "target type", m_MoveRequest.m_Type, MoveRequest::Type::OFFSET);
|
|
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 y", m_MoveRequest.m_Position.Y);
|
|
serialize.NumberFixed_Unbounded("target min range", m_MoveRequest.m_MinRange);
|
|
serialize.NumberFixed_Unbounded("target max range", m_MoveRequest.m_MaxRange);
|
|
|
|
serialize.NumberU32_Unbounded("formation controller", m_FormationController);
|
|
|
|
serialize.NumberFixed_Unbounded("speed multiplier", m_SpeedMultiplier);
|
|
|
|
serialize.NumberFixed_Unbounded("last turn speed", m_LastTurnSpeed);
|
|
serialize.NumberFixed_Unbounded("current speed", m_CurrentSpeed);
|
|
|
|
serialize.NumberFixed_Unbounded("instant turn angle", m_InstantTurnAngle);
|
|
|
|
serialize.NumberFixed_Unbounded("acceleration", m_Acceleration);
|
|
|
|
serialize.Bool("facePointAfterMove", m_FacePointAfterMove);
|
|
serialize.Bool("pushing", m_Pushing);
|
|
|
|
Serializer(serialize, "long path", m_LongPath.m_Waypoints);
|
|
Serializer(serialize, "short path", m_ShortPath.m_Waypoints);
|
|
}
|
|
|
|
void Serialize(ISerializer& serialize) override
|
|
{
|
|
SerializeCommon(serialize);
|
|
}
|
|
|
|
void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) override
|
|
{
|
|
Init(paramNode);
|
|
|
|
SerializeCommon(deserialize);
|
|
|
|
SetPassabilityData(m_PassClassName);
|
|
|
|
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
|
|
if (cmpObstruction)
|
|
m_BlockMovement = cmpObstruction->GetBlockMovementFlag(false);
|
|
}
|
|
|
|
void HandleMessage(const CMessage& msg, bool /*global*/) override
|
|
{
|
|
switch (msg.GetType())
|
|
{
|
|
case MT_RenderSubmit:
|
|
{
|
|
PROFILE("UnitMotion::RenderSubmit");
|
|
const CMessageRenderSubmit& msgData = static_cast<const CMessageRenderSubmit&> (msg);
|
|
RenderSubmit(msgData.collector);
|
|
break;
|
|
}
|
|
case MT_PathResult:
|
|
{
|
|
const CMessagePathResult& msgData = static_cast<const CMessagePathResult&> (msg);
|
|
PathResult(msgData.ticket, msgData.path);
|
|
break;
|
|
}
|
|
case MT_Create:
|
|
{
|
|
if (!ENTITY_IS_LOCAL(GetEntityId()))
|
|
CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController);
|
|
break;
|
|
}
|
|
case MT_Destroy:
|
|
{
|
|
if (!ENTITY_IS_LOCAL(GetEntityId()))
|
|
CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Unregister(GetEntityId());
|
|
break;
|
|
}
|
|
case MT_MovementObstructionChanged:
|
|
{
|
|
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
|
|
if (cmpObstruction)
|
|
m_BlockMovement = cmpObstruction->GetBlockMovementFlag(false);
|
|
break;
|
|
}
|
|
case MT_ValueModification:
|
|
{
|
|
const CMessageValueModification& msgData = static_cast<const CMessageValueModification&> (msg);
|
|
if (msgData.component != L"UnitMotion")
|
|
break;
|
|
[[fallthrough]];
|
|
}
|
|
case MT_OwnershipChanged:
|
|
{
|
|
OnValueModification();
|
|
break;
|
|
}
|
|
case MT_Deserialized:
|
|
{
|
|
OnValueModification();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UpdateMessageSubscriptions()
|
|
{
|
|
bool needRender = m_DebugOverlayEnabled;
|
|
GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender);
|
|
}
|
|
|
|
bool IsMoveRequested() const override
|
|
{
|
|
return m_MoveRequest.m_Type != MoveRequest::NONE;
|
|
}
|
|
|
|
fixed GetSpeedMultiplier() const override
|
|
{
|
|
return m_SpeedMultiplier;
|
|
}
|
|
|
|
void SetSpeedMultiplier(fixed multiplier) override
|
|
{
|
|
m_SpeedMultiplier = std::min(multiplier, m_RunMultiplier);
|
|
m_Speed = m_SpeedMultiplier.Multiply(GetWalkSpeed());
|
|
}
|
|
|
|
fixed GetSpeed() const override
|
|
{
|
|
return m_Speed;
|
|
}
|
|
|
|
fixed GetWalkSpeed() const override
|
|
{
|
|
return m_WalkSpeed;
|
|
}
|
|
|
|
fixed GetRunMultiplier() const override
|
|
{
|
|
return m_RunMultiplier;
|
|
}
|
|
|
|
CFixedVector2D EstimateFuturePosition(const fixed dt) const override
|
|
{
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
|
if (!cmpPosition || !cmpPosition->IsInWorld())
|
|
return CFixedVector2D();
|
|
|
|
// TODO: formation members should perhaps try to use the controller's position.
|
|
|
|
CFixedVector2D pos = cmpPosition->GetPosition2D();
|
|
entity_angle_t angle = cmpPosition->GetRotation().Y;
|
|
fixed speed = m_CurrentSpeed;
|
|
// Copy the path so we don't change it.
|
|
WaypointPath shortPath = m_ShortPath;
|
|
WaypointPath longPath = m_LongPath;
|
|
|
|
PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, speed, angle, 0);
|
|
return pos;
|
|
}
|
|
|
|
fixed GetAcceleration() const override
|
|
{
|
|
return m_Acceleration;
|
|
}
|
|
|
|
void SetAcceleration(fixed acceleration) override
|
|
{
|
|
m_Acceleration = acceleration;
|
|
}
|
|
|
|
entity_pos_t GetWeight() const
|
|
{
|
|
return m_TemplateWeight;
|
|
}
|
|
|
|
pass_class_t GetPassabilityClass() const override
|
|
{
|
|
return m_PassClass;
|
|
}
|
|
|
|
std::string GetPassabilityClassName() const override
|
|
{
|
|
return m_PassClassName;
|
|
}
|
|
|
|
void SetPassabilityClassName(const std::string& passClassName) override
|
|
{
|
|
if (!m_IsFormationController)
|
|
{
|
|
LOGWARNING("Only formation controllers can change their passability class");
|
|
return;
|
|
}
|
|
SetPassabilityData(passClassName);
|
|
}
|
|
|
|
fixed GetCurrentSpeed() const override
|
|
{
|
|
return m_CurrentSpeed;
|
|
}
|
|
|
|
void SetCurrentSpeed(const fixed& speed) override
|
|
{
|
|
m_CurrentSpeed = speed;
|
|
|
|
if (speed == fixed::Zero())
|
|
m_LastTurnSpeed = fixed::Zero();
|
|
else
|
|
m_LastTurnSpeed = speed;
|
|
}
|
|
|
|
void SetFacePointAfterMove(bool facePointAfterMove) override
|
|
{
|
|
m_FacePointAfterMove = facePointAfterMove;
|
|
}
|
|
|
|
bool GetFacePointAfterMove() const override
|
|
{
|
|
return m_FacePointAfterMove;
|
|
}
|
|
|
|
void SetDebugOverlay(bool enabled) override
|
|
{
|
|
m_DebugOverlayEnabled = enabled;
|
|
UpdateMessageSubscriptions();
|
|
}
|
|
|
|
bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) override
|
|
{
|
|
return MoveTo(MoveRequest(CFixedVector2D(x, z), minRange, maxRange));
|
|
}
|
|
|
|
bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) override
|
|
{
|
|
return MoveTo(MoveRequest(target, minRange, maxRange));
|
|
}
|
|
|
|
void MoveToFormationOffset(entity_id_t controller, entity_pos_t x, entity_pos_t z) override
|
|
{
|
|
// Pass the controller to the move request anyways.
|
|
MoveTo(MoveRequest(controller, CFixedVector2D(x, z)));
|
|
}
|
|
|
|
void SetMemberOfFormation(entity_id_t controller) override
|
|
{
|
|
m_FormationController = controller;
|
|
}
|
|
|
|
bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) override;
|
|
|
|
void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) override;
|
|
|
|
/**
|
|
* Clears the current MoveRequest - the unit will stop and no longer try and move.
|
|
* This should never be called from UnitMotion, since MoveToX orders are given
|
|
* by other components - these components should also decide when to stop.
|
|
*/
|
|
void StopMoving() override
|
|
{
|
|
if (m_FacePointAfterMove)
|
|
{
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
|
if (cmpPosition && cmpPosition->IsInWorld())
|
|
{
|
|
CFixedVector2D targetPos;
|
|
if (ComputeTargetPosition(targetPos))
|
|
FaceTowardsPointFromPos(cmpPosition->GetPosition2D(), targetPos.X, targetPos.Y);
|
|
}
|
|
}
|
|
|
|
m_MoveRequest = MoveRequest();
|
|
m_ExpectedPathTicket.clear();
|
|
m_LongPath.m_Waypoints.clear();
|
|
m_ShortPath.m_Waypoints.clear();
|
|
}
|
|
|
|
entity_pos_t GetUnitClearance() const override
|
|
{
|
|
return m_Clearance;
|
|
}
|
|
|
|
private:
|
|
bool IsFormationMember() const
|
|
{
|
|
return m_FormationController != INVALID_ENTITY;
|
|
}
|
|
|
|
bool IsMovingAsFormation() const
|
|
{
|
|
return IsFormationMember() && m_MoveRequest.m_Type == MoveRequest::OFFSET;
|
|
}
|
|
|
|
bool IsFormationControllerMoving() const
|
|
{
|
|
CmpPtr<ICmpUnitMotion> cmpControllerMotion(GetSimContext(), m_FormationController);
|
|
return cmpControllerMotion && cmpControllerMotion->IsMoveRequested();
|
|
}
|
|
|
|
entity_id_t GetGroup() const
|
|
{
|
|
return IsFormationMember() ? m_FormationController : GetEntityId();
|
|
}
|
|
|
|
void SetParticipateInPushing(bool pushing)
|
|
{
|
|
CmpPtr<ICmpUnitMotionManager> cmpUnitMotionManager(GetSystemEntity());
|
|
m_Pushing = pushing && cmpUnitMotionManager->IsPushingActivated();
|
|
}
|
|
|
|
void SetPassabilityData(const std::string& passClassName)
|
|
{
|
|
m_PassClassName = passClassName;
|
|
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
|
|
if (cmpPathfinder)
|
|
{
|
|
m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName);
|
|
m_Clearance = cmpPathfinder->GetClearance(m_PassClass);
|
|
|
|
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
|
|
if (cmpObstruction)
|
|
cmpObstruction->SetUnitClearance(m_Clearance);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Warns other components that our current movement will likely fail (e.g. we won't be able to reach our target)
|
|
* This should only be called before the actual movement in a given turn, or units might both move and try to do things
|
|
* on the same turn, leading to gliding units.
|
|
*/
|
|
void MoveFailed()
|
|
{
|
|
// Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time
|
|
// if our current offset is unreachable, but we don't want to end up stuck.
|
|
// (If the formation controller has stopped moving however, we can safely message).
|
|
if (IsFormationMember() && IsFormationControllerMoving())
|
|
return;
|
|
|
|
CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_FAILURE);
|
|
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
|
|
}
|
|
|
|
/**
|
|
* Warns other components that our current movement is likely over (i.e. we probably reached our destination)
|
|
* This should only be called before the actual movement in a given turn, or units might both move and try to do things
|
|
* on the same turn, leading to gliding units.
|
|
*/
|
|
void MoveSucceeded()
|
|
{
|
|
// Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time
|
|
// if our current offset is unreachable, but we don't want to end up stuck.
|
|
// (If the formation controller has stopped moving however, we can safely message).
|
|
if (IsFormationMember() && IsFormationControllerMoving())
|
|
return;
|
|
|
|
CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_SUCCESS);
|
|
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
|
|
}
|
|
|
|
/**
|
|
* Warns other components that our current movement was obstructed (i.e. we failed to move this turn).
|
|
* This should only be called before the actual movement in a given turn, or units might both move and try to do things
|
|
* on the same turn, leading to gliding units.
|
|
*/
|
|
void MoveObstructed()
|
|
{
|
|
// Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time
|
|
// if our current offset is unreachable, but we don't want to end up stuck.
|
|
// (If the formation controller has stopped moving however, we can safely message).
|
|
if (IsFormationMember() && IsFormationControllerMoving())
|
|
return;
|
|
|
|
CMessageMotionUpdate msg(m_FailedMovements >= VERY_OBSTRUCTED_THRESHOLD ?
|
|
CMessageMotionUpdate::VERY_OBSTRUCTED : CMessageMotionUpdate::OBSTRUCTED);
|
|
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
|
|
}
|
|
|
|
/**
|
|
* Increment the number of failed movements and notify other components if required.
|
|
* @returns true if the failure was notified, false otherwise.
|
|
*/
|
|
bool IncrementFailedMovementsAndMaybeNotify()
|
|
{
|
|
m_FailedMovements++;
|
|
if (m_FailedMovements >= MAX_FAILED_MOVEMENTS)
|
|
{
|
|
MoveFailed();
|
|
m_FailedMovements = 0;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* If path would take us farther away from the goal than pos currently is, return false, else return true.
|
|
*/
|
|
bool RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const;
|
|
|
|
bool ShouldAlternatePathfinder() const
|
|
{
|
|
return (m_FailedMovements == ALTERNATE_PATH_TYPE_DELAY) || ((MAX_FAILED_MOVEMENTS - ALTERNATE_PATH_TYPE_DELAY) % ALTERNATE_PATH_TYPE_EVERY == 0);
|
|
}
|
|
|
|
bool InShortPathRange(const PathGoal& goal, const CFixedVector2D& pos) const
|
|
{
|
|
return goal.DistanceToPoint(pos) < LONG_PATH_MIN_DIST;
|
|
}
|
|
|
|
entity_pos_t ShortPathSearchRange() const
|
|
{
|
|
u8 multiple = m_FailedMovements < SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY ? 0 : m_FailedMovements - SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY;
|
|
fixed searchRange = SHORT_PATH_MIN_SEARCH_RANGE + SHORT_PATH_SEARCH_RANGE_INCREMENT * multiple;
|
|
if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE)
|
|
searchRange = SHORT_PATH_MAX_SEARCH_RANGE;
|
|
return searchRange;
|
|
}
|
|
|
|
/**
|
|
* Handle the result of an asynchronous path query.
|
|
*/
|
|
void PathResult(u32 ticket, const WaypointPath& path);
|
|
|
|
void OnValueModification()
|
|
{
|
|
CmpPtr<ICmpValueModificationManager> cmpValueModificationManager(GetSystemEntity());
|
|
if (!cmpValueModificationManager)
|
|
return;
|
|
|
|
m_WalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId());
|
|
m_RunMultiplier = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateRunMultiplier, GetEntityId());
|
|
|
|
// For MT_Deserialize compute m_Speed from the serialized m_SpeedMultiplier.
|
|
// For MT_ValueModification and MT_OwnershipChanged, adjust m_SpeedMultiplier if needed
|
|
// (in case then new m_RunMultiplier value is lower than the old).
|
|
SetSpeedMultiplier(m_SpeedMultiplier);
|
|
}
|
|
|
|
/**
|
|
* Check if we are at destination early in the turn, this both lets units react faster
|
|
* and ensure that distance comparisons are done while units are not being moved
|
|
* (otherwise they won't be commutative).
|
|
*/
|
|
void OnTurnStart();
|
|
|
|
void PreMove(CCmpUnitMotionManager::MotionState& state);
|
|
void Move(CCmpUnitMotionManager::MotionState& state, fixed dt);
|
|
void PostMove(CCmpUnitMotionManager::MotionState& state, fixed dt);
|
|
|
|
bool PossiblyAtDestination() const override;
|
|
|
|
/**
|
|
* Process the move the unit will do this turn.
|
|
* This does not send actually change the position.
|
|
* @returns true if the move was obstructed.
|
|
*/
|
|
bool PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle, uint8_t pushingPressure) const;
|
|
|
|
/**
|
|
* Update other components on our speed.
|
|
* (For performance, this should try to avoid sending messages).
|
|
*/
|
|
void UpdateMovementState(entity_pos_t speed, entity_pos_t meanSpeed);
|
|
|
|
/**
|
|
* React if our move was obstructed.
|
|
* @param moved - true if the unit still managed to move.
|
|
* @returns true if the obstruction required handling, false otherwise.
|
|
*/
|
|
bool HandleObstructedMove(bool moved);
|
|
|
|
/**
|
|
* Returns true if the target position is valid. False otherwise.
|
|
* (this may indicate that the target is e.g. out of the world/dead).
|
|
* NB: for code-writing convenience, if we have no target, this returns true.
|
|
*/
|
|
bool TargetHasValidPosition(const MoveRequest& moveRequest) const;
|
|
bool TargetHasValidPosition() const
|
|
{
|
|
return TargetHasValidPosition(m_MoveRequest);
|
|
}
|
|
|
|
/**
|
|
* Computes the current location of our target entity (plus offset).
|
|
* Returns false if no target entity or no valid position.
|
|
*/
|
|
bool ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const;
|
|
bool ComputeTargetPosition(CFixedVector2D& out) const
|
|
{
|
|
return ComputeTargetPosition(out, m_MoveRequest);
|
|
}
|
|
|
|
/**
|
|
* Attempts to replace the current path with a straight line to the target,
|
|
* if it's close enough and the route is not obstructed.
|
|
*/
|
|
bool TryGoingStraightToTarget(const CFixedVector2D& from, bool updatePaths);
|
|
|
|
/**
|
|
* Returns whether our we need to recompute a path to reach our target.
|
|
*/
|
|
bool PathingUpdateNeeded(const CFixedVector2D& from) const;
|
|
|
|
/**
|
|
* Rotate to face towards the target point, given the current pos
|
|
*/
|
|
void FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z);
|
|
|
|
/**
|
|
* Units in 'pushing' mode are marked as 'moving' in the obstruction manager.
|
|
* Units in 'pushing' mode should skip them in checkMovement (to enable pushing).
|
|
* However, units for which pushing is deactivated should collide against everyone.
|
|
* Units that don't block movement never participate in pushing, but they also
|
|
* shouldn't collide with pushing units.
|
|
*/
|
|
bool ShouldCollideWithMovingUnits() const
|
|
{
|
|
return !m_Pushing && m_BlockMovement;
|
|
}
|
|
|
|
/**
|
|
* Returns an appropriate obstruction filter for use with path requests.
|
|
*/
|
|
ControlGroupMovementObstructionFilter GetObstructionFilter() const
|
|
{
|
|
return ControlGroupMovementObstructionFilter(ShouldCollideWithMovingUnits(), GetGroup());
|
|
}
|
|
/**
|
|
* Filter a specific tag on top of the existing control groups.
|
|
*/
|
|
SkipTagAndControlGroupObstructionFilter GetObstructionFilter(const ICmpObstructionManager::tag_t& tag) const
|
|
{
|
|
return SkipTagAndControlGroupObstructionFilter(tag, ShouldCollideWithMovingUnits(), GetGroup());
|
|
}
|
|
|
|
/**
|
|
* Decide whether to approximate the given range from a square target as a circle,
|
|
* rather than as a square.
|
|
*/
|
|
bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const;
|
|
|
|
/**
|
|
* Create a PathGoal from a move request.
|
|
* @returns true if the goal was successfully created.
|
|
*/
|
|
bool ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) const;
|
|
|
|
/**
|
|
* Compute a path to the given goal from the given position.
|
|
* Might go in a straight line immediately, or might start an asynchronous path request.
|
|
*/
|
|
void ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal);
|
|
|
|
/**
|
|
* Start an asynchronous long path query.
|
|
*/
|
|
void RequestLongPath(const CFixedVector2D& from, const PathGoal& goal);
|
|
|
|
/**
|
|
* Start an asynchronous short path query.
|
|
* @param extendRange - if true, extend the search range to at least the distance to the goal.
|
|
*/
|
|
void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool extendRange);
|
|
|
|
/**
|
|
* General handler for MoveTo interface functions.
|
|
*/
|
|
bool MoveTo(MoveRequest request);
|
|
|
|
/**
|
|
* Convert a path into a renderable list of lines
|
|
*/
|
|
void RenderPath(const WaypointPath& path, std::vector<SOverlayLine>& lines, CColor color);
|
|
|
|
void RenderSubmit(SceneCollector& collector);
|
|
};
|
|
|
|
REGISTER_COMPONENT_TYPE(UnitMotion)
|
|
|
|
bool CCmpUnitMotion::RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const
|
|
{
|
|
if (path.m_Waypoints.empty())
|
|
return false;
|
|
|
|
// Reject the new path if it does not lead us closer to the target's position.
|
|
if (goal.DistanceToPoint(pos) <= goal.DistanceToPoint(CFixedVector2D(path.m_Waypoints.front().x, path.m_Waypoints.front().z)))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
|
|
{
|
|
// Ignore obsolete path requests
|
|
if (ticket != m_ExpectedPathTicket.m_Ticket || m_MoveRequest.m_Type == MoveRequest::NONE)
|
|
return;
|
|
|
|
Ticket::Type ticketType = m_ExpectedPathTicket.m_Type;
|
|
m_ExpectedPathTicket.clear();
|
|
|
|
// If we not longer have a position, we won't be able to do much.
|
|
// Fail in the next Move() call.
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
|
if (!cmpPosition || !cmpPosition->IsInWorld())
|
|
return;
|
|
CFixedVector2D pos = cmpPosition->GetPosition2D();
|
|
|
|
// Assume all long paths were towards the goal, and assume short paths were if there are no long waypoints.
|
|
bool pathedTowardsGoal = ticketType == Ticket::LONG_PATH || m_LongPath.m_Waypoints.empty();
|
|
|
|
// Check if we need to run the short-path hack (warning: tricky control flow).
|
|
bool shortPathHack = false;
|
|
if (path.m_Waypoints.empty())
|
|
{
|
|
// No waypoints means pathing failed. If this was a long-path, try the short-path hack.
|
|
if (!pathedTowardsGoal)
|
|
return;
|
|
shortPathHack = ticketType == Ticket::LONG_PATH;
|
|
}
|
|
else if (PathGoal goal; pathedTowardsGoal && ComputeGoal(goal, m_MoveRequest) && RejectFartherPaths(goal, path, pos))
|
|
{
|
|
// Reject paths that would take the unit further away from the goal.
|
|
// This assumes that we prefer being closer 'as the crow flies' to unreachable goals.
|
|
// This is a hack of sorts around units 'dancing' between two positions (see e.g. #3144),
|
|
// but never actually failing to move, ergo never actually informing unitAI that it succeeds/fails.
|
|
// (for short paths, only do so if aiming directly for the goal
|
|
// as sub-goals may be farther than we are).
|
|
|
|
// If this was a long-path and we no longer have waypoints, try the short-path hack.
|
|
if (!m_LongPath.m_Waypoints.empty())
|
|
return;
|
|
shortPathHack = ticketType == Ticket::LONG_PATH;
|
|
}
|
|
|
|
// Short-path hack: if the long-range pathfinder doesn't find an acceptable path, push a fake waypoint at the goal.
|
|
// This means HandleObstructedMove will use the short-pathfinder to try and reach it,
|
|
// and that may find a path as the vertex pathfinder is more precise.
|
|
if (shortPathHack)
|
|
{
|
|
// If we're resorting to the short-path hack, the situation is dire. Most likely, the goal is unreachable.
|
|
// We want to find a path or fail fast. Bump failed movements so the short pathfinder will run at max-range
|
|
// right away. This is safe from a performance PoV because it can only happen if the target is unreachable to
|
|
// the long-range pathfinder, which is rare, and since the entity will fail to move if the goal is actually unreachable,
|
|
// the failed movements will be increased to MAX anyways, so just shortcut.
|
|
m_FailedMovements = MAX_FAILED_MOVEMENTS - 2;
|
|
|
|
CFixedVector2D targetPos;
|
|
if (ComputeTargetPosition(targetPos))
|
|
m_LongPath.m_Waypoints.emplace_back(Waypoint{ targetPos.X, targetPos.Y });
|
|
return;
|
|
}
|
|
|
|
if (ticketType == Ticket::LONG_PATH)
|
|
{
|
|
m_LongPath = path;
|
|
// Long paths don't properly follow diagonals because of JPS/the grid. Since units now take time turning,
|
|
// they can actually slow down substantially if they have to do a one navcell diagonal movement,
|
|
// which is somewhat common at the beginning of a new path.
|
|
// For that reason, if the first waypoint is really close, check if we can't go directly to the second.
|
|
if (m_LongPath.m_Waypoints.size() >= 2)
|
|
{
|
|
const Waypoint& firstWpt = m_LongPath.m_Waypoints.back();
|
|
if (CFixedVector2D(firstWpt.x - pos.X, firstWpt.z - pos.Y).CompareLength(Pathfinding::NAVCELL_SIZE * 4) <= 0)
|
|
{
|
|
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
|
|
ENSURE(cmpPathfinder);
|
|
const Waypoint& secondWpt = m_LongPath.m_Waypoints[m_LongPath.m_Waypoints.size() - 2];
|
|
if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, secondWpt.x, secondWpt.z, m_Clearance, m_PassClass))
|
|
m_LongPath.m_Waypoints.pop_back();
|
|
}
|
|
|
|
}
|
|
}
|
|
else
|
|
m_ShortPath = path;
|
|
|
|
m_FollowKnownImperfectPathCountdown = 0;
|
|
|
|
if (!pathedTowardsGoal)
|
|
return;
|
|
|
|
// Performance hack: If we were pathing towards the goal and this new path won't put us in range,
|
|
// it's highly likely that we are going somewhere unreachable.
|
|
// However, Move() will try to recompute the path every turn, which can be quite slow.
|
|
// To avoid this, act as if our current path leads us to the correct destination.
|
|
// NB: for short-paths, the problem might be that the search space is too small
|
|
// but we'll still follow this path until the en and try again then.
|
|
// Because we reject farther paths, it works out.
|
|
if (PathingUpdateNeeded(pos))
|
|
{
|
|
// Inform other components early, as they might have better behaviour than waiting for the path to carry out.
|
|
// Send OBSTRUCTED at first - moveFailed is likely to trigger path recomputation and we might end up
|
|
// recomputing too often for nothing.
|
|
if (!IncrementFailedMovementsAndMaybeNotify())
|
|
MoveObstructed();
|
|
// We'll automatically recompute a path when this reaches 0, as a way to improve behaviour.
|
|
// (See D665 - this is needed because the target may be moving, and we should adjust to that).
|
|
m_FollowKnownImperfectPathCountdown = KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN;
|
|
}
|
|
}
|
|
|
|
void CCmpUnitMotion::OnTurnStart()
|
|
{
|
|
if (PossiblyAtDestination())
|
|
MoveSucceeded();
|
|
else if (!TargetHasValidPosition())
|
|
{
|
|
// Scrap waypoints - we don't know where to go.
|
|
// If the move request remains unchanged and the target again has a valid position later on,
|
|
// moving will be resumed.
|
|
// Units may want to move to move to the target's last known position,
|
|
// but that should be decided by UnitAI (handling MoveFailed), not UnitMotion.
|
|
m_LongPath.m_Waypoints.clear();
|
|
m_ShortPath.m_Waypoints.clear();
|
|
|
|
MoveFailed();
|
|
}
|
|
}
|
|
|
|
void CCmpUnitMotion::PreMove(CCmpUnitMotionManager::MotionState& state)
|
|
{
|
|
state.ignore = !m_Pushing || !m_BlockMovement;
|
|
|
|
state.wasObstructed = false;
|
|
state.wentStraight = false;
|
|
|
|
// If we were idle and will still be, no need for an update.
|
|
state.needUpdate = state.cmpPosition->IsInWorld() &&
|
|
(m_CurrentSpeed != fixed::Zero() || m_LastTurnSpeed != fixed::Zero() || m_MoveRequest.m_Type != MoveRequest::NONE);
|
|
|
|
if (!m_BlockMovement)
|
|
return;
|
|
|
|
state.controlGroup = IsFormationMember() ? m_FormationController : INVALID_ENTITY;
|
|
|
|
// Update moving flag, this is an internal construct used for pushing,
|
|
// so it does not really reflect whether the unit is actually moving or not.
|
|
state.isMoving = m_Pushing && m_MoveRequest.m_Type != MoveRequest::NONE;
|
|
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
|
|
if (cmpObstruction)
|
|
cmpObstruction->SetMovingFlag(state.isMoving);
|
|
}
|
|
|
|
void CCmpUnitMotion::Move(CCmpUnitMotionManager::MotionState& state, fixed dt)
|
|
{
|
|
PROFILE("Move");
|
|
|
|
// 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.
|
|
state.wentStraight = TryGoingStraightToTarget(state.initialPos, true);
|
|
|
|
state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.speed, state.angle, state.pushingPressure);
|
|
}
|
|
|
|
void CCmpUnitMotion::PostMove(CCmpUnitMotionManager::MotionState& state, fixed dt)
|
|
{
|
|
// Update our speed over this turn so that the visual actor shows the correct animation.
|
|
if (state.pos == state.initialPos)
|
|
{
|
|
if (state.angle != state.initialAngle)
|
|
state.cmpPosition->TurnTo(state.angle);
|
|
UpdateMovementState(fixed::Zero(), fixed::Zero());
|
|
}
|
|
else
|
|
{
|
|
// Update the Position component after our movement (if we actually moved anywhere)
|
|
CFixedVector2D offset = state.pos - state.initialPos;
|
|
state.cmpPosition->MoveAndTurnTo(state.pos.X, state.pos.Y, state.angle);
|
|
|
|
// Calculate the mean speed over this past turn.
|
|
UpdateMovementState(state.speed, offset.Length() / dt);
|
|
}
|
|
|
|
if (state.wasObstructed && HandleObstructedMove(state.pos != state.initialPos))
|
|
return;
|
|
else if (!state.wasObstructed && state.pos != state.initialPos)
|
|
m_FailedMovements = 0;
|
|
|
|
const bool needPathUpdate{PathingUpdateNeeded(state.pos)};
|
|
|
|
// If we're following a long-path, check if we might run into units in advance, to smoothe motion.
|
|
if (!needPathUpdate && !state.wasObstructed && m_LongPath.m_Waypoints.size() >= 1 && m_ShortPath.m_Waypoints.empty())
|
|
{
|
|
ICmpObstructionManager::tag_t specificIgnore;
|
|
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
|
|
{
|
|
CmpPtr<ICmpObstruction> cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
|
|
if (cmpTargetObstruction)
|
|
specificIgnore = cmpTargetObstruction->GetObstruction();
|
|
}
|
|
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
|
|
if (cmpObstructionManager && cmpObstructionManager->TestUnitLine(GetObstructionFilter(specificIgnore),
|
|
state.pos.X, state.pos.Y, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z, m_Clearance, true))
|
|
{
|
|
// We will run into something: request a new short path.
|
|
// If we have several waypoints left, aim for the one after next directly.
|
|
// (This is mostly because the obstruction might be at the waypoint, and the end is kind of treated specially).
|
|
// Else just path to the goal.
|
|
if (m_LongPath.m_Waypoints.size() > 1) {
|
|
fixed radius = Pathfinding::NAVCELL_SIZE * 2;
|
|
m_LongPath.m_Waypoints.pop_back();
|
|
PathGoal subgoal = { PathGoal::CIRCLE, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z, radius };
|
|
RequestShortPath(state.pos, subgoal, false);
|
|
} else {
|
|
// If we only have one waypoint left, request a short path to the waypoint itself.
|
|
PathGoal goal;
|
|
if (ComputeGoal(goal, m_MoveRequest))
|
|
RequestShortPath(state.pos, goal, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we moved straight, and didn't quite finish the path, reset - we'll update it next turn if still OK.
|
|
if (state.wentStraight && !state.wasObstructed)
|
|
m_ShortPath.m_Waypoints.clear();
|
|
|
|
// 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 (!state.wentStraight && needPathUpdate)
|
|
{
|
|
PathGoal goal;
|
|
if (ComputeGoal(goal, m_MoveRequest))
|
|
ComputePathToGoal(state.pos, goal);
|
|
}
|
|
else if (m_FollowKnownImperfectPathCountdown > 0)
|
|
--m_FollowKnownImperfectPathCountdown;
|
|
}
|
|
|
|
bool CCmpUnitMotion::PossiblyAtDestination() const
|
|
{
|
|
if (m_MoveRequest.m_Type == MoveRequest::NONE)
|
|
return false;
|
|
|
|
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
|
|
ENSURE(cmpObstructionManager);
|
|
|
|
switch (m_MoveRequest.m_Type)
|
|
{
|
|
case MoveRequest::POINT:
|
|
return cmpObstructionManager->IsInPointRange(
|
|
GetEntityId(), m_MoveRequest.m_Position.X, m_MoveRequest.m_Position.Y,
|
|
m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false);
|
|
|
|
case MoveRequest::ENTITY:
|
|
return cmpObstructionManager->IsInTargetRange(
|
|
GetEntityId(), m_MoveRequest.m_Entity,
|
|
m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false);
|
|
|
|
case MoveRequest::OFFSET:
|
|
{
|
|
CmpPtr<ICmpUnitMotion> cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity);
|
|
if (cmpControllerMotion && cmpControllerMotion->IsMoveRequested())
|
|
return false;
|
|
|
|
CFixedVector2D targetPos;
|
|
ComputeTargetPosition(targetPos);
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
|
return (targetPos - cmpPosition->GetPosition2D()).CompareLength(Pathfinding::NAVCELL_SIZE) <= 0;
|
|
}
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle, uint8_t pushingPressure) const
|
|
{
|
|
// If there are no waypoint, behave as though we were obstructed and let HandleObstructedMove handle it.
|
|
if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty())
|
|
return true;
|
|
|
|
// Wrap the angle to (-Pi, Pi].
|
|
while (angle > entity_angle_t::Pi())
|
|
angle -= entity_angle_t::Pi() * 2;
|
|
while (angle < -entity_angle_t::Pi())
|
|
angle += entity_angle_t::Pi() * 2;
|
|
|
|
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
|
|
ENSURE(cmpPathfinder);
|
|
|
|
fixed basicSpeed = m_Speed;
|
|
// If in formation, run to keep up; otherwise just walk.
|
|
if (IsMovingAsFormation())
|
|
basicSpeed = m_Speed.Multiply(m_RunMultiplier);
|
|
|
|
// If pushing pressure is applied, slow the unit down.
|
|
if (pushingPressure)
|
|
{
|
|
// Values below this pressure don't slow the unit down (avoids slowing groups down).
|
|
constexpr int pressureMinThreshold = 10;
|
|
|
|
// Lower speed up to a floor to prevent units from getting stopped.
|
|
// This helped pushing particularly for fast units, since they'll end up slowing down.
|
|
constexpr int maxPressure = CCmpUnitMotionManager::MAX_PRESSURE - pressureMinThreshold - 80;
|
|
constexpr entity_pos_t floorSpeed = entity_pos_t::FromFraction(3, 2);
|
|
static_assert(maxPressure > 0);
|
|
|
|
uint8_t slowdown = maxPressure - std::min(maxPressure, std::max(0, pushingPressure - pressureMinThreshold));
|
|
basicSpeed = basicSpeed.Multiply(fixed::FromInt(slowdown) / maxPressure);
|
|
// NB: lowering this too much will make the units behave a lot like viscous fluid
|
|
// when the density becomes extreme. While perhaps realistic (and kind of neat),
|
|
// it's not very helpful for gameplay. Empirically, a value of 1.5 avoids most of the effect
|
|
// while still slowing down movement significantly, and seems like a good balance.
|
|
// Min with the template speed to allow units that are explicitly absurdly slow.
|
|
basicSpeed = std::max(std::min(m_TemplateWalkSpeed, floorSpeed), basicSpeed);
|
|
}
|
|
|
|
// TODO: would be nice to support terrain-dependent speed again.
|
|
fixed maxSpeed = basicSpeed;
|
|
|
|
fixed timeLeft = dt;
|
|
fixed zero = fixed::Zero();
|
|
|
|
ICmpObstructionManager::tag_t specificIgnore;
|
|
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
|
|
{
|
|
CmpPtr<ICmpObstruction> cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
|
|
if (cmpTargetObstruction)
|
|
specificIgnore = cmpTargetObstruction->GetObstruction();
|
|
}
|
|
|
|
while (timeLeft > zero)
|
|
{
|
|
// If we ran out of path, we have to stop.
|
|
if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty())
|
|
break;
|
|
|
|
CFixedVector2D target;
|
|
if (shortPath.m_Waypoints.empty())
|
|
target = CFixedVector2D(longPath.m_Waypoints.back().x, longPath.m_Waypoints.back().z);
|
|
else
|
|
target = CFixedVector2D(shortPath.m_Waypoints.back().x, shortPath.m_Waypoints.back().z);
|
|
|
|
CFixedVector2D offset = target - pos;
|
|
|
|
if (turnRate > zero && !offset.IsZero())
|
|
{
|
|
fixed angleDiff = angle - atan2_approx(offset.X, offset.Y);
|
|
fixed absoluteAngleDiff = angleDiff.Absolute();
|
|
if (absoluteAngleDiff > entity_angle_t::Pi())
|
|
absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff;
|
|
|
|
// We only rotate to the instantTurnAngle angle. The rest we rotate during movement.
|
|
if (absoluteAngleDiff > m_InstantTurnAngle)
|
|
{
|
|
// Stop moving when rotating this far.
|
|
speed = zero;
|
|
|
|
fixed maxRotation = turnRate.Multiply(timeLeft);
|
|
|
|
// Figure out whether rotating will increase or decrease the angle, and how far we need to rotate in that direction.
|
|
int direction = (entity_angle_t::Zero() < angleDiff && angleDiff <= entity_angle_t::Pi()) || angleDiff < -entity_angle_t::Pi() ? -1 : 1;
|
|
|
|
// Can't rotate far enough, just rotate in the correct direction.
|
|
if (absoluteAngleDiff - m_InstantTurnAngle > maxRotation)
|
|
{
|
|
angle += maxRotation * direction;
|
|
if (angle * direction > entity_angle_t::Pi())
|
|
angle -= entity_angle_t::Pi() * 2 * direction;
|
|
break;
|
|
}
|
|
// Rotate towards the next waypoint and continue moving.
|
|
angle = atan2_approx(offset.X, offset.Y);
|
|
timeLeft = std::min(maxRotation, maxRotation - absoluteAngleDiff + m_InstantTurnAngle) / turnRate;
|
|
}
|
|
else
|
|
{
|
|
// Modify the speed depending on the angle difference.
|
|
fixed sin, cos;
|
|
sincos_approx(angleDiff, sin, cos);
|
|
speed = speed.Multiply(cos);
|
|
angle = atan2_approx(offset.X, offset.Y);
|
|
}
|
|
}
|
|
|
|
// Work out how far we can travel in timeLeft.
|
|
fixed accelTime = std::min(timeLeft, (maxSpeed - speed) / m_Acceleration);
|
|
fixed accelDist = speed.Multiply(accelTime) + accelTime.Square().Multiply(m_Acceleration) / 2;
|
|
fixed maxdist = accelDist + maxSpeed.Multiply(timeLeft - accelTime);
|
|
|
|
// If the target is close, we can move there directly.
|
|
fixed offsetLength = offset.Length();
|
|
if (offsetLength <= maxdist)
|
|
{
|
|
if (cmpPathfinder->CheckMovement(GetObstructionFilter(specificIgnore), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass))
|
|
{
|
|
pos = target;
|
|
|
|
// Spend the rest of the time heading towards the next waypoint.
|
|
// Either we still need to accelerate after, or we have reached maxSpeed.
|
|
// The former is much less likely than the latter: usually we can reach
|
|
// maxSpeed within one waypoint. So the Sqrt is not too bad.
|
|
if (offsetLength <= accelDist)
|
|
{
|
|
fixed requiredTime = (-speed + (speed.Square() + offsetLength.Multiply(m_Acceleration).Multiply(fixed::FromInt(2))).Sqrt()) / m_Acceleration;
|
|
timeLeft -= requiredTime;
|
|
speed += m_Acceleration.Multiply(requiredTime);
|
|
}
|
|
else
|
|
{
|
|
timeLeft -= accelTime + (offsetLength - accelDist) / maxSpeed;
|
|
speed = maxSpeed;
|
|
}
|
|
|
|
if (shortPath.m_Waypoints.empty())
|
|
longPath.m_Waypoints.pop_back();
|
|
else
|
|
shortPath.m_Waypoints.pop_back();
|
|
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
// Error - path was obstructed.
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Not close enough, so just move in the right direction.
|
|
offset.Normalize(maxdist);
|
|
target = pos + offset;
|
|
|
|
speed = std::min(maxSpeed, speed + m_Acceleration.Multiply(timeLeft));
|
|
|
|
if (cmpPathfinder->CheckMovement(GetObstructionFilter(specificIgnore), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass))
|
|
pos = target;
|
|
else
|
|
return true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CCmpUnitMotion::UpdateMovementState(entity_pos_t speed, entity_pos_t meanSpeed)
|
|
{
|
|
CmpPtr<ICmpVisual> cmpVisual(GetEntityHandle());
|
|
if (cmpVisual)
|
|
{
|
|
if (meanSpeed == fixed::Zero())
|
|
cmpVisual->SelectMovementAnimation("idle", fixed::FromInt(1));
|
|
else
|
|
cmpVisual->SelectMovementAnimation(meanSpeed > (m_WalkSpeed / 2).Multiply(m_RunMultiplier + fixed::FromInt(1)) ? "run" : "walk", meanSpeed);
|
|
}
|
|
|
|
m_LastTurnSpeed = meanSpeed;
|
|
m_CurrentSpeed = speed;
|
|
}
|
|
|
|
bool CCmpUnitMotion::HandleObstructedMove(bool moved)
|
|
{
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
|
if (!cmpPosition || !cmpPosition->IsInWorld())
|
|
return false;
|
|
|
|
// We failed to move, inform other components as they might handle it.
|
|
// (don't send messages on the first failure, as that would be too noisy).
|
|
// Also don't increment above the initial MoveObstructed message if we actually manage to move a little.
|
|
if (!moved || m_FailedMovements < 2)
|
|
{
|
|
if (!IncrementFailedMovementsAndMaybeNotify() && m_FailedMovements >= 2)
|
|
MoveObstructed();
|
|
}
|
|
|
|
PathGoal goal;
|
|
if (!ComputeGoal(goal, m_MoveRequest))
|
|
return false;
|
|
|
|
// At this point we have a position in the world since ComputeGoal checked for that.
|
|
CFixedVector2D pos = cmpPosition->GetPosition2D();
|
|
|
|
// Assume that we are merely obstructed and the long path is salvageable, so try going around the obstruction.
|
|
// This could be a separate function, but it doesn't really make sense to call it outside of here, and I can't find a name.
|
|
// I use an IIFE to have nice 'return' semantics still.
|
|
if ([&]() -> bool {
|
|
// If the goal is close enough, we should ignore any remaining long waypoint and just
|
|
// short path there directly, as that improves behaviour in general - see D2095).
|
|
if (InShortPathRange(goal, pos))
|
|
return false;
|
|
|
|
// On rare occasions, when following a short path, we can end up in a position where
|
|
// the short pathfinder thinks we are inside an obstruction (and can leave)
|
|
// but the CheckMovement logic doesn't. I believe the cause is a small numerical difference
|
|
// in their calculation, but haven't been able to pinpoint it precisely.
|
|
// In those cases, the solution is to back away to prevent the short-pathfinder from being confused.
|
|
// TODO: this should only be done if we're obstructed by a static entity.
|
|
if (!m_ShortPath.m_Waypoints.empty() && m_FailedMovements == BACKUP_HACK_DELAY)
|
|
{
|
|
Waypoint next = m_ShortPath.m_Waypoints.back();
|
|
CFixedVector2D backUp(pos.X - next.x, pos.Y - next.z);
|
|
backUp.Normalize();
|
|
next.x = pos.X + backUp.X;
|
|
next.z = pos.Y + backUp.Y;
|
|
m_ShortPath.m_Waypoints.push_back(next);
|
|
return true;
|
|
}
|
|
// Delete the next waypoint if it's reasonably close,
|
|
// because it might be blocked by units and thus unreachable.
|
|
// NB: this number is tricky. Make it too high, and units start going down dead ends, which looks odd (#5795)
|
|
// Make it too low, and they might get stuck behind other obstructed entities.
|
|
// It also has performance implications because it calls the short-pathfinder.
|
|
fixed skipbeyond = std::max(ShortPathSearchRange() / 3, Pathfinding::NAVCELL_SIZE * 8);
|
|
if (m_LongPath.m_Waypoints.size() > 1 &&
|
|
(pos - CFixedVector2D(m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z)).CompareLength(skipbeyond) < 0)
|
|
{
|
|
m_LongPath.m_Waypoints.pop_back();
|
|
}
|
|
else if (ShouldAlternatePathfinder())
|
|
{
|
|
// Recompute the whole thing occasionally, in case we got stuck in a dead end from removing long waypoints.
|
|
RequestLongPath(pos, goal);
|
|
return true;
|
|
}
|
|
|
|
if (m_LongPath.m_Waypoints.empty())
|
|
return false;
|
|
|
|
// Compute a short path in the general vicinity of the next waypoint, to help pathfinding in crowds.
|
|
// The goal here is to manage to move in the general direction of our target, not to be super accurate.
|
|
fixed radius = Clamp(skipbeyond/3, Pathfinding::NAVCELL_SIZE * 4, Pathfinding::NAVCELL_SIZE * 12);
|
|
PathGoal subgoal = { PathGoal::CIRCLE, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z, radius };
|
|
RequestShortPath(pos, subgoal, false);
|
|
return true;
|
|
}()) return true;
|
|
|
|
// If we couldn't use a workaround, try recomputing the entire path.
|
|
ComputePathToGoal(pos, goal);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CCmpUnitMotion::TargetHasValidPosition(const MoveRequest& moveRequest) const
|
|
{
|
|
if (moveRequest.m_Type != MoveRequest::ENTITY)
|
|
return true;
|
|
|
|
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), moveRequest.m_Entity);
|
|
return cmpPosition && cmpPosition->IsInWorld();
|
|
}
|
|
|
|
bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const
|
|
{
|
|
if (moveRequest.m_Type == MoveRequest::POINT)
|
|
{
|
|
out = moveRequest.m_Position;
|
|
return true;
|
|
}
|
|
|
|
CmpPtr<ICmpPosition> cmpTargetPosition(GetSimContext(), moveRequest.m_Entity);
|
|
if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
|
|
return false;
|
|
|
|
if (moveRequest.m_Type == MoveRequest::OFFSET)
|
|
{
|
|
// There is an offset, so compute it relative to orientation
|
|
entity_angle_t angle = cmpTargetPosition->GetRotation().Y;
|
|
CFixedVector2D offset = moveRequest.GetOffset().Rotate(angle);
|
|
out = cmpTargetPosition->GetPosition2D() + offset;
|
|
}
|
|
else
|
|
{
|
|
out = cmpTargetPosition->GetPosition2D();
|
|
// Position is only updated after all units have moved & pushed.
|
|
// Therefore, we may need to interpolate the target position, depending on when this call takes place during the turn:
|
|
// - On "Turn Start", we'll check positions directly without interpolation.
|
|
// - During movement, we'll call this for direct-pathing & we need to interpolate
|
|
// (this way, we move where the unit will end up at the end of _this_ turn, making it match on next turn start).
|
|
// - After movement, we'll call this to request paths & we need to interpolate
|
|
// (this way, we'll move where the unit ends up in the end of _next_ turn, making it a match in 2 turns).
|
|
// TODO: This does not really aim many turns in advance, with orthogonal trajectories it probably should.
|
|
CmpPtr<ICmpUnitMotion> cmpUnitMotion(GetSimContext(), moveRequest.m_Entity);
|
|
CmpPtr<ICmpUnitMotionManager> cmpUnitMotionManager(GetSystemEntity());
|
|
bool needInterpolation = cmpUnitMotion && cmpUnitMotion->IsMoveRequested() && cmpUnitMotionManager->ComputingMotion();
|
|
if (needInterpolation)
|
|
{
|
|
// Add predicted movement.
|
|
CFixedVector2D tempPos = out + (out - cmpTargetPosition->GetPreviousPosition2D());
|
|
out = tempPos;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from, bool updatePaths)
|
|
{
|
|
// Assume if we have short paths we want to follow them.
|
|
// Exception: offset movement (formations) generally have very short deltas
|
|
// and to look good we need them to walk-straight most of the time.
|
|
if (!IsFormationMember() && !m_ShortPath.m_Waypoints.empty())
|
|
return false;
|
|
|
|
CFixedVector2D targetPos;
|
|
if (!ComputeTargetPosition(targetPos))
|
|
return false;
|
|
|
|
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
|
|
if (!cmpPathfinder)
|
|
return false;
|
|
|
|
// Move the goal to match the target entity's new position
|
|
PathGoal goal;
|
|
if (!ComputeGoal(goal, m_MoveRequest))
|
|
return false;
|
|
goal.x = targetPos.X;
|
|
goal.z = targetPos.Y;
|
|
// (we ignore changes to the target's rotation, since only buildings are
|
|
// square and buildings don't move)
|
|
|
|
// Find the point on the goal shape that we should head towards
|
|
CFixedVector2D goalPos = goal.NearestPointOnGoal(from);
|
|
|
|
// Fail if the target is too far away
|
|
if ((goalPos - from).CompareLength(DIRECT_PATH_RANGE) > 0)
|
|
return false;
|
|
|
|
// Check if there's any collisions on that route.
|
|
// For entity goals, skip only the specific obstruction tag or with e.g. walls we might ignore too many entities.
|
|
ICmpObstructionManager::tag_t specificIgnore;
|
|
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
|
|
{
|
|
CmpPtr<ICmpObstruction> cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
|
|
if (cmpTargetObstruction)
|
|
specificIgnore = cmpTargetObstruction->GetObstruction();
|
|
}
|
|
|
|
// Check movement against units - we want to use the short pathfinder to walk around those if needed.
|
|
if (specificIgnore.valid())
|
|
{
|
|
if (!cmpPathfinder->CheckMovement(GetObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
|
|
return false;
|
|
}
|
|
else if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
|
|
return false;
|
|
|
|
if (!updatePaths)
|
|
return true;
|
|
|
|
// That route is okay, so update our path
|
|
m_LongPath.m_Waypoints.clear();
|
|
m_ShortPath.m_Waypoints.clear();
|
|
m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y });
|
|
return true;
|
|
}
|
|
|
|
bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const
|
|
{
|
|
if (m_MoveRequest.m_Type == MoveRequest::NONE)
|
|
return false;
|
|
|
|
CFixedVector2D targetPos;
|
|
if (!ComputeTargetPosition(targetPos))
|
|
return false;
|
|
|
|
if (m_FollowKnownImperfectPathCountdown > 0 && (!m_LongPath.m_Waypoints.empty() || !m_ShortPath.m_Waypoints.empty()))
|
|
return false;
|
|
|
|
if (PossiblyAtDestination())
|
|
return false;
|
|
|
|
// Get the obstruction shape and translate it where we estimate the target to be.
|
|
ICmpObstructionManager::ObstructionSquare estimatedTargetShape;
|
|
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
|
|
{
|
|
CmpPtr<ICmpObstruction> cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
|
|
if (cmpTargetObstruction)
|
|
cmpTargetObstruction->GetObstructionSquare(estimatedTargetShape);
|
|
}
|
|
|
|
estimatedTargetShape.x = targetPos.X;
|
|
estimatedTargetShape.z = targetPos.Y;
|
|
|
|
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
|
|
ICmpObstructionManager::ObstructionSquare shape;
|
|
if (cmpObstruction)
|
|
cmpObstruction->GetObstructionSquare(shape);
|
|
|
|
// 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())
|
|
{
|
|
shape.x = from.X;
|
|
shape.z = from.Y;
|
|
}
|
|
else
|
|
{
|
|
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;
|
|
}
|
|
|
|
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
|
|
ENSURE(cmpObstructionManager);
|
|
|
|
// 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();
|
|
|
|
// 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;
|
|
}
|
|
|
|
void CCmpUnitMotion::FaceTowardsPoint(entity_pos_t x, entity_pos_t z)
|
|
{
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
|
if (!cmpPosition || !cmpPosition->IsInWorld())
|
|
return;
|
|
|
|
CFixedVector2D pos = cmpPosition->GetPosition2D();
|
|
FaceTowardsPointFromPos(pos, x, z);
|
|
}
|
|
|
|
void CCmpUnitMotion::FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z)
|
|
{
|
|
CFixedVector2D target(x, z);
|
|
CFixedVector2D offset = target - pos;
|
|
if (!offset.IsZero())
|
|
{
|
|
entity_angle_t angle = atan2_approx(offset.X, offset.Y);
|
|
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
|
if (!cmpPosition)
|
|
return;
|
|
cmpPosition->TurnTo(angle);
|
|
}
|
|
}
|
|
|
|
// The pathfinder cannot go to "rounded rectangles" goals, which are what happens with square targets and a non-null range.
|
|
// Depending on what the best approximation is, we either pretend the target is a circle or a square.
|
|
// One needs to be careful that the approximated geometry will be in the range.
|
|
bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const
|
|
{
|
|
// Given a square, plus a target range we should reach, the shape at that distance
|
|
// is a round-cornered square which we can approximate as either a circle or as a square.
|
|
// Previously, we used the shape that minimized the worst-case error.
|
|
// However that is unsage in some situations. So let's be less clever and
|
|
// just check if our range is at least three times bigger than the circleradius
|
|
return (range > circleRadius*3);
|
|
}
|
|
|
|
bool CCmpUnitMotion::ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) const
|
|
{
|
|
if (moveRequest.m_Type == MoveRequest::NONE)
|
|
return false;
|
|
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
|
if (!cmpPosition || !cmpPosition->IsInWorld())
|
|
return false;
|
|
|
|
CFixedVector2D pos = cmpPosition->GetPosition2D();
|
|
|
|
CFixedVector2D targetPosition;
|
|
if (!ComputeTargetPosition(targetPosition, moveRequest))
|
|
return false;
|
|
|
|
ICmpObstructionManager::ObstructionSquare targetObstruction;
|
|
if (moveRequest.m_Type == MoveRequest::ENTITY)
|
|
{
|
|
CmpPtr<ICmpObstruction> cmpTargetObstruction(GetSimContext(), moveRequest.m_Entity);
|
|
if (cmpTargetObstruction)
|
|
cmpTargetObstruction->GetObstructionSquare(targetObstruction);
|
|
}
|
|
targetObstruction.x = targetPosition.X;
|
|
targetObstruction.z = targetPosition.Y;
|
|
|
|
ICmpObstructionManager::ObstructionSquare obstruction;
|
|
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
|
|
if (cmpObstruction)
|
|
cmpObstruction->GetObstructionSquare(obstruction);
|
|
else
|
|
{
|
|
obstruction.x = pos.X;
|
|
obstruction.z = pos.Y;
|
|
}
|
|
|
|
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
|
|
ENSURE(cmpObstructionManager);
|
|
|
|
out.x = targetObstruction.x;
|
|
out.z = targetObstruction.z;
|
|
out.hw = targetObstruction.hw;
|
|
out.hh = targetObstruction.hh;
|
|
out.u = targetObstruction.u;
|
|
out.v = targetObstruction.v;
|
|
|
|
if (moveRequest.m_MinRange > fixed::Zero() || moveRequest.m_MaxRange > fixed::Zero() ||
|
|
targetObstruction.hw > fixed::Zero())
|
|
out.type = PathGoal::SQUARE;
|
|
else
|
|
{
|
|
out.type = PathGoal::POINT;
|
|
return true;
|
|
}
|
|
|
|
entity_pos_t distance = cmpObstructionManager->DistanceBetweenShapes(obstruction, targetObstruction);
|
|
|
|
entity_pos_t circleRadius = CFixedVector2D(targetObstruction.hw, targetObstruction.hh).Length();
|
|
|
|
// TODO: because we cannot move to rounded rectangles, we have to make conservative approximations.
|
|
// This means we might end up in a situation where cons(max-range) < min range < max range < cons(min-range)
|
|
// When going outside of the min-range or inside the max-range, the unit will still go through the correct range
|
|
// but if it moves fast enough, this might not be picked up by PossiblyAtDestination().
|
|
// Fixing this involves moving to rounded rectangles, or checking more often in PerformMove().
|
|
// In the meantime, one should avoid that 'Speed over a turn' > MaxRange - MinRange, in case where
|
|
// min-range is not 0 and max-range is not infinity.
|
|
if (distance < moveRequest.m_MinRange)
|
|
{
|
|
// Distance checks are nearest edge to nearest edge, so we need to account for our clearance
|
|
// and we must make sure diagonals also fit so multiply by slightly more than sqrt(2)
|
|
entity_pos_t goalDistance = moveRequest.m_MinRange + m_Clearance * 3 / 2;
|
|
|
|
if (ShouldTreatTargetAsCircle(moveRequest.m_MinRange, circleRadius))
|
|
{
|
|
// We are safely away from the obstruction itself if we are away from the circumscribing circle
|
|
out.type = PathGoal::INVERTED_CIRCLE;
|
|
out.hw = circleRadius + goalDistance;
|
|
}
|
|
else
|
|
{
|
|
out.type = PathGoal::INVERTED_SQUARE;
|
|
out.hw = targetObstruction.hw + goalDistance;
|
|
out.hh = targetObstruction.hh + goalDistance;
|
|
}
|
|
}
|
|
else if (moveRequest.m_MaxRange >= fixed::Zero() && distance > moveRequest.m_MaxRange)
|
|
{
|
|
if (ShouldTreatTargetAsCircle(moveRequest.m_MaxRange, circleRadius))
|
|
{
|
|
entity_pos_t goalDistance = moveRequest.m_MaxRange;
|
|
// We must go in-range of the inscribed circle, not the circumscribing circle.
|
|
circleRadius = std::min(targetObstruction.hw, targetObstruction.hh);
|
|
|
|
out.type = PathGoal::CIRCLE;
|
|
out.hw = circleRadius + goalDistance;
|
|
}
|
|
else
|
|
{
|
|
// The target is large relative to our range, so treat it as a square and
|
|
// get close enough that the diagonals come within range
|
|
|
|
entity_pos_t goalDistance = moveRequest.m_MaxRange * 2 / 3; // multiply by slightly less than 1/sqrt(2)
|
|
|
|
out.type = PathGoal::SQUARE;
|
|
entity_pos_t delta = std::max(goalDistance, m_Clearance + entity_pos_t::FromInt(4)/16); // ensure it's far enough to not intersect the building itself
|
|
out.hw = targetObstruction.hw + delta;
|
|
out.hh = targetObstruction.hh + delta;
|
|
}
|
|
}
|
|
// Do nothing in particular in case we are already in range.
|
|
return true;
|
|
}
|
|
|
|
void CCmpUnitMotion::ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal)
|
|
{
|
|
#if DISABLE_PATHFINDER
|
|
{
|
|
CmpPtr<ICmpPathfinder> cmpPathfinder (GetSimContext(), SYSTEM_ENTITY);
|
|
CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from);
|
|
m_LongPath.m_Waypoints.clear();
|
|
m_ShortPath.m_Waypoints.clear();
|
|
m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y });
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
// If the target is close enough, hope that we'll be able to go straight next turn.
|
|
if (!ShouldAlternatePathfinder() && TryGoingStraightToTarget(from, false))
|
|
{
|
|
// NB: since we may fail to move straight next turn, we should edge our bets.
|
|
// Since the 'go straight' logic currently fires only if there's no short path,
|
|
// we'll compute a long path regardless to make sure _that_ stays up to date.
|
|
// (it's also extremely likely to be very fast to compute, so no big deal).
|
|
m_ShortPath.m_Waypoints.clear();
|
|
RequestLongPath(from, goal);
|
|
return;
|
|
}
|
|
|
|
// Otherwise we need to compute a path.
|
|
|
|
// If it's close then just do a short path, not a long path
|
|
// TODO: If it's close on the opposite side of a river then we really
|
|
// need a long path, so we shouldn't simply check linear distance
|
|
// the check is arbitrary but should be a reasonably small distance.
|
|
// We want to occasionally compute a long path if we're computing short-paths, because the short path domain
|
|
// is bounded and thus it can't around very large static obstacles.
|
|
// Likewise, we want to compile a short-path occasionally when the target is far because we might be stuck
|
|
// on a navcell surrounded by impassable navcells, but the short-pathfinder could move us out of there.
|
|
bool shortPath = InShortPathRange(goal, from);
|
|
if (ShouldAlternatePathfinder())
|
|
shortPath = !shortPath;
|
|
if (shortPath)
|
|
{
|
|
m_LongPath.m_Waypoints.clear();
|
|
// Extend the range so that our first path is probably valid.
|
|
RequestShortPath(from, goal, true);
|
|
}
|
|
else
|
|
{
|
|
m_ShortPath.m_Waypoints.clear();
|
|
RequestLongPath(from, goal);
|
|
}
|
|
}
|
|
|
|
void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal)
|
|
{
|
|
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
|
|
if (!cmpPathfinder)
|
|
return;
|
|
|
|
// this is by how much our waypoints will be apart at most.
|
|
// this value here seems sensible enough.
|
|
PathGoal improvedGoal = goal;
|
|
improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1);
|
|
|
|
cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass);
|
|
|
|
m_ExpectedPathTicket.m_Type = Ticket::LONG_PATH;
|
|
m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId());
|
|
}
|
|
|
|
void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool extendRange)
|
|
{
|
|
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
|
|
if (!cmpPathfinder)
|
|
return;
|
|
|
|
entity_pos_t searchRange = ShortPathSearchRange();
|
|
if (extendRange)
|
|
{
|
|
CFixedVector2D dist(from.X - goal.x, from.Y - goal.z);
|
|
if (dist.CompareLength(searchRange - entity_pos_t::FromInt(1)) >= 0)
|
|
{
|
|
searchRange = dist.Length() + fixed::FromInt(1);
|
|
if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE)
|
|
searchRange = SHORT_PATH_MAX_SEARCH_RANGE;
|
|
}
|
|
}
|
|
|
|
m_ExpectedPathTicket.m_Type = Ticket::SHORT_PATH;
|
|
m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, ShouldCollideWithMovingUnits(), GetGroup(), GetEntityId());
|
|
}
|
|
|
|
bool CCmpUnitMotion::MoveTo(MoveRequest request)
|
|
{
|
|
PROFILE("MoveTo");
|
|
|
|
if (request.m_MinRange == request.m_MaxRange && !request.m_MinRange.IsZero())
|
|
LOGWARNING("MaxRange must be larger than MinRange; See CCmpUnitMotion.cpp for more information");
|
|
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
|
if (!cmpPosition || !cmpPosition->IsInWorld())
|
|
return false;
|
|
|
|
PathGoal goal;
|
|
if (!ComputeGoal(goal, request))
|
|
return false;
|
|
|
|
m_MoveRequest = request;
|
|
m_FailedMovements = 0;
|
|
m_FollowKnownImperfectPathCountdown = 0;
|
|
|
|
ComputePathToGoal(cmpPosition->GetPosition2D(), goal);
|
|
return true;
|
|
}
|
|
|
|
bool CCmpUnitMotion::IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
|
|
{
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
|
if (!cmpPosition || !cmpPosition->IsInWorld())
|
|
return false;
|
|
|
|
MoveRequest request(target, minRange, maxRange);
|
|
PathGoal goal;
|
|
if (!ComputeGoal(goal, request))
|
|
return false;
|
|
|
|
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
|
|
CFixedVector2D pos = cmpPosition->GetPosition2D();
|
|
return cmpPathfinder->IsGoalReachable(pos.X, pos.Y, goal, m_PassClass);
|
|
}
|
|
|
|
void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector<SOverlayLine>& lines, CColor color)
|
|
{
|
|
bool floating = false;
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
|
if (cmpPosition)
|
|
floating = cmpPosition->CanFloat();
|
|
|
|
lines.clear();
|
|
std::vector<float> waypointCoords;
|
|
for (size_t i = 0; i < path.m_Waypoints.size(); ++i)
|
|
{
|
|
float x = path.m_Waypoints[i].x.ToFloat();
|
|
float z = path.m_Waypoints[i].z.ToFloat();
|
|
waypointCoords.push_back(x);
|
|
waypointCoords.push_back(z);
|
|
lines.push_back(SOverlayLine());
|
|
lines.back().m_Color = color;
|
|
SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating);
|
|
}
|
|
float x = cmpPosition->GetPosition2D().X.ToFloat();
|
|
float z = cmpPosition->GetPosition2D().Y.ToFloat();
|
|
waypointCoords.push_back(x);
|
|
waypointCoords.push_back(z);
|
|
lines.push_back(SOverlayLine());
|
|
lines.back().m_Color = color;
|
|
SimRender::ConstructLineOnGround(GetSimContext(), waypointCoords, lines.back(), floating);
|
|
|
|
}
|
|
|
|
void CCmpUnitMotion::RenderSubmit(SceneCollector& collector)
|
|
{
|
|
if (!m_DebugOverlayEnabled)
|
|
return;
|
|
|
|
const auto& palette{m_IsFormationController ?
|
|
FORMATION_CONTROLLER_PALETTE : REGULAR_UNIT_PALETTE};
|
|
|
|
RenderPath(m_LongPath, m_DebugOverlayLongPathLines, palette.longPath);
|
|
RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, palette.shortPath);
|
|
|
|
for (SOverlayLine& line : m_DebugOverlayLongPathLines)
|
|
collector.Submit(&line);
|
|
|
|
for (SOverlayLine& line : m_DebugOverlayShortPathLines)
|
|
collector.Submit(&line);
|
|
}
|
|
|
|
#endif // INCLUDED_CCMPUNITMOTION
|