Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.h
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotion.h
+++ ps/trunk/source/simulation2/components/CCmpUnitMotion.h
@@ -0,0 +1,1726 @@
+/* Copyright (C) 2021 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 .
+ */
+
+#ifndef INCLUDED_CCMPUNITMOTION
+#define INCLUDED_CCMPUNITMOTION
+
+#include "simulation2/system/Component.h"
+#include "ICmpUnitMotion.h"
+
+#include "simulation2/components/CCmpUnitMotionManager.h"
+#include "simulation2/components/ICmpObstruction.h"
+#include "simulation2/components/ICmpObstructionManager.h"
+#include "simulation2/components/ICmpOwnership.h"
+#include "simulation2/components/ICmpPosition.h"
+#include "simulation2/components/ICmpPathfinder.h"
+#include "simulation2/components/ICmpRangeManager.h"
+#include "simulation2/components/ICmpValueModificationManager.h"
+#include "simulation2/components/ICmpVisual.h"
+#include "simulation2/helpers/Geometry.h"
+#include "simulation2/helpers/Render.h"
+#include "simulation2/MessageTypes.h"
+#include "simulation2/serialization/SerializedPathfinder.h"
+#include "simulation2/serialization/SerializedTypes.h"
+
+#include "graphics/Overlay.h"
+#include "graphics/Terrain.h"
+#include "maths/FixedVector2D.h"
+#include "ps/CLogger.h"
+#include "ps/Profile.h"
+#include "renderer/Scene.h"
+
+// 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
+
+/**
+ * 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.
+ */
+static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3)/2;
+static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*14);
+static const entity_pos_t SHORT_PATH_SEARCH_RANGE_INCREMENT = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1);
+static const u8 SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY = 2;
+
+/**
+ * When using the short-pathfinder to rejoin a long-path waypoint, aim for a circle of this radius around the waypoint.
+ */
+static const entity_pos_t SHORT_PATH_LONG_WAYPOINT_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1);
+
+/**
+ * Minimum distance to goal for a long path request
+ */
+static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4);
+
+/**
+ * If we are this close to our target entity/point, then think about heading
+ * for it in a straight line instead of pathfinding.
+ */
+static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*6);
+
+/**
+ * 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 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).
+ */
+static const 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.
+ * TODO: when unit pushing is implemented, this number can probably be lowered.
+ */
+static const u8 MAX_FAILED_MOVEMENTS = 40;
+
+/**
+ * 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).
+ */
+static const u8 ALTERNATE_PATH_TYPE_DELAY = 3;
+static const u8 ALTERNATE_PATH_TYPE_EVERY = 6;
+
+/**
+ * After this many failed computations, start sending "VERY_OBSTRUCTED" messages instead.
+ * Should probably be larger than ALTERNATE_PATH_TYPE_DELAY.
+ */
+static const u8 VERY_OBSTRUCTED_THRESHOLD = 10;
+
+static const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1);
+static const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1);
+
+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_Deserialized);
+ }
+
+ DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
+
+ bool m_DebugOverlayEnabled;
+ std::vector m_DebugOverlayLongPathLines;
+ std::vector m_DebugOverlayShortPathLines;
+
+ // Template state:
+
+ bool m_FormationController;
+
+ fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier;
+ 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;
+
+ // Number of turns since we last managed to move successfully.
+ // 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 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;
+
+ // Current mean speed (over the last turn).
+ fixed m_CurSpeed;
+
+ // 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
+ "Provides the unit with the ability to move around the world by itself."
+ ""
+ "7.0"
+ "default"
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ "";
+ }
+
+ virtual void Init(const CParamNode& paramNode)
+ {
+ m_FormationController = paramNode.GetChild("FormationController").ToBool();
+
+ m_FacePointAfterMove = true;
+
+ m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed();
+ m_SpeedMultiplier = fixed::FromInt(1);
+ m_CurSpeed = fixed::Zero();
+
+ m_RunMultiplier = m_TemplateRunMultiplier = fixed::FromInt(1);
+ if (paramNode.GetChild("RunMultiplier").IsOk())
+ m_RunMultiplier = m_TemplateRunMultiplier = paramNode.GetChild("RunMultiplier").ToFixed();
+
+ CmpPtr cmpPathfinder(GetSystemEntity());
+ if (cmpPathfinder)
+ {
+ m_PassClassName = paramNode.GetChild("PassabilityClass").ToUTF8();
+ m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName);
+ m_Clearance = cmpPathfinder->GetClearance(m_PassClass);
+
+ CmpPtr cmpObstruction(GetEntityHandle());
+ if (cmpObstruction)
+ cmpObstruction->SetUnitClearance(m_Clearance);
+ }
+
+ m_DebugOverlayEnabled = false;
+ }
+
+ virtual void Deinit()
+ {
+ }
+
+ template
+ void SerializeCommon(S& serialize)
+ {
+ 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.NumberFixed_Unbounded("speed multiplier", m_SpeedMultiplier);
+
+ serialize.NumberFixed_Unbounded("current speed", m_CurSpeed);
+
+ serialize.Bool("facePointAfterMove", m_FacePointAfterMove);
+
+ Serializer(serialize, "long path", m_LongPath.m_Waypoints);
+ Serializer(serialize, "short path", m_ShortPath.m_Waypoints);
+ }
+
+ virtual void Serialize(ISerializer& serialize)
+ {
+ SerializeCommon(serialize);
+ }
+
+ virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
+ {
+ Init(paramNode);
+
+ SerializeCommon(deserialize);
+
+ CmpPtr cmpPathfinder(GetSystemEntity());
+ if (cmpPathfinder)
+ m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName);
+ }
+
+ virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
+ {
+ switch (msg.GetType())
+ {
+ case MT_RenderSubmit:
+ {
+ PROFILE("UnitMotion::RenderSubmit");
+ const CMessageRenderSubmit& msgData = static_cast (msg);
+ RenderSubmit(msgData.collector);
+ break;
+ }
+ case MT_PathResult:
+ {
+ const CMessagePathResult& msgData = static_cast (msg);
+ PathResult(msgData.ticket, msgData.path);
+ break;
+ }
+ case MT_Create:
+ {
+ if (!ENTITY_IS_LOCAL(GetEntityId()))
+ CmpPtr(GetSystemEntity())->Register(this, GetEntityId(), m_FormationController);
+ break;
+ }
+ case MT_Destroy:
+ {
+ if (!ENTITY_IS_LOCAL(GetEntityId()))
+ CmpPtr(GetSystemEntity())->Unregister(GetEntityId());
+ break;
+ }
+ case MT_ValueModification:
+ {
+ const CMessageValueModification& msgData = static_cast (msg);
+ if (msgData.component != L"UnitMotion")
+ break;
+ FALLTHROUGH;
+ }
+ case MT_OwnershipChanged:
+ {
+ OnValueModification();
+ break;
+ }
+ case MT_Deserialized:
+ {
+ OnValueModification();
+ if (!ENTITY_IS_LOCAL(GetEntityId()))
+ CmpPtr(GetSystemEntity())->Register(this, GetEntityId(), m_FormationController);
+ break;
+ }
+ }
+ }
+
+ void UpdateMessageSubscriptions()
+ {
+ bool needRender = m_DebugOverlayEnabled;
+ GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender);
+ }
+
+ virtual bool IsMoveRequested() const
+ {
+ return m_MoveRequest.m_Type != MoveRequest::NONE;
+ }
+
+ virtual fixed GetSpeedMultiplier() const
+ {
+ return m_SpeedMultiplier;
+ }
+
+ virtual void SetSpeedMultiplier(fixed multiplier)
+ {
+ m_SpeedMultiplier = std::min(multiplier, m_RunMultiplier);
+ m_Speed = m_SpeedMultiplier.Multiply(GetWalkSpeed());
+ }
+
+ virtual fixed GetSpeed() const
+ {
+ return m_Speed;
+ }
+
+ virtual fixed GetWalkSpeed() const
+ {
+ return m_WalkSpeed;
+ }
+
+ virtual fixed GetRunMultiplier() const
+ {
+ return m_RunMultiplier;
+ }
+
+ virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const
+ {
+ CmpPtr 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;
+
+ // Copy the path so we don't change it.
+ WaypointPath shortPath = m_ShortPath;
+ WaypointPath longPath = m_LongPath;
+
+ PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, angle);
+ return pos;
+ }
+
+ virtual pass_class_t GetPassabilityClass() const
+ {
+ return m_PassClass;
+ }
+
+ virtual std::string GetPassabilityClassName() const
+ {
+ return m_PassClassName;
+ }
+
+ virtual void SetPassabilityClassName(const std::string& passClassName)
+ {
+ m_PassClassName = passClassName;
+ CmpPtr cmpPathfinder(GetSystemEntity());
+ if (cmpPathfinder)
+ m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName);
+ }
+
+ virtual fixed GetCurrentSpeed() const
+ {
+ return m_CurSpeed;
+ }
+
+ virtual void SetFacePointAfterMove(bool facePointAfterMove)
+ {
+ m_FacePointAfterMove = facePointAfterMove;
+ }
+
+ virtual bool GetFacePointAfterMove() const
+ {
+ return m_FacePointAfterMove;
+ }
+
+ virtual void SetDebugOverlay(bool enabled)
+ {
+ m_DebugOverlayEnabled = enabled;
+ UpdateMessageSubscriptions();
+ }
+
+ virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
+ {
+ return MoveTo(MoveRequest(CFixedVector2D(x, z), minRange, maxRange));
+ }
+
+ virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
+ {
+ return MoveTo(MoveRequest(target, minRange, maxRange));
+ }
+
+ virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z)
+ {
+ MoveTo(MoveRequest(target, CFixedVector2D(x, z)));
+ }
+
+ virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
+
+ virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z);
+
+ /**
+ * 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.
+ */
+ virtual void StopMoving()
+ {
+ if (m_FacePointAfterMove)
+ {
+ CmpPtr 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();
+ }
+
+ virtual entity_pos_t GetUnitClearance() const
+ {
+ return m_Clearance;
+ }
+
+private:
+ bool ShouldAvoidMovingUnits() const
+ {
+ return !m_FormationController;
+ }
+
+ bool IsFormationMember() const
+ {
+ // TODO: this really shouldn't be what we are checking for.
+ return m_MoveRequest.m_Type == MoveRequest::OFFSET;
+ }
+
+ bool IsFormationControllerMoving() const
+ {
+ CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity);
+ return cmpControllerMotion && cmpControllerMotion->IsMoveRequested();
+ }
+
+ entity_id_t GetGroup() const
+ {
+ return IsFormationMember() ? m_MoveRequest.m_Entity : GetEntityId();
+ }
+
+ /**
+ * 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 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);
+
+ /**
+ * Returns true if we are possibly at our destination.
+ * Since the concept of being at destination is dependent on why the move was requested,
+ * UnitMotion can only ever hint about this, hence the conditional tone.
+ */
+ bool PossiblyAtDestination() const;
+
+ /**
+ * 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, entity_angle_t& angle) const;
+
+ /**
+ * Update other components on our speed.
+ * (For performance, this should try to avoid sending messages).
+ */
+ void UpdateMovementState(entity_pos_t speed);
+
+ /**
+ * 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);
+
+ /**
+ * 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);
+
+ /**
+ * Returns an appropriate obstruction filter for use with path requests.
+ */
+ ControlGroupMovementObstructionFilter GetObstructionFilter() const
+ {
+ return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), GetGroup());
+ }
+ /**
+ * Filter a specific tag on top of the existing control groups.
+ */
+ SkipMovingTagAndControlGroupObstructionFilter GetObstructionFilter(const ICmpObstructionManager::tag_t& tag) const
+ {
+ return SkipMovingTagAndControlGroupObstructionFilter(tag, 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& 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 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(fixed::FromInt(TERRAIN_TILE_SIZE)) <= 0)
+ {
+ CmpPtr 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)
+{
+ // If we were idle and will still be, no need for an update.
+ state.needUpdate = state.cmpPosition->IsInWorld() &&
+ (m_CurSpeed != fixed::Zero() || m_MoveRequest.m_Type != MoveRequest::NONE);
+}
+
+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);
+
+ state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.angle);
+}
+
+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());
+ }
+ else
+ {
+ // Update the Position component after our movement (if we actually moved anywhere)
+ // When moving always set the angle in the direction of the movement.
+ CFixedVector2D offset = state.pos - state.initialPos;
+ state.angle = atan2_approx(offset.X, offset.Y);
+ state.cmpPosition->MoveAndTurnTo(state.pos.X, state.pos.Y, state.angle);
+
+ // Calculate the mean speed over this past turn.
+ UpdateMovementState(offset.Length() / dt);
+ }
+
+ if (state.wasObstructed && HandleObstructedMove(state.pos != state.initialPos))
+ return;
+ else if (!state.wasObstructed && state.pos != state.initialPos)
+ m_FailedMovements = 0;
+
+ // 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 && PathingUpdateNeeded(state.pos))
+ {
+ 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 cmpObstructionManager(GetSystemEntity());
+ ENSURE(cmpObstructionManager);
+
+ if (m_MoveRequest.m_Type == 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);
+ if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
+ return cmpObstructionManager->IsInTargetRange(GetEntityId(), m_MoveRequest.m_Entity, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false);
+ if (m_MoveRequest.m_Type == MoveRequest::OFFSET)
+ {
+ CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity);
+ if (cmpControllerMotion && cmpControllerMotion->IsMoveRequested())
+ return false;
+
+ // In formation, return a match only if we are exactly at the target position.
+ // Otherwise, units can go in an infinite "walzting" loop when the Idle formation timer
+ // reforms them.
+ CFixedVector2D targetPos;
+ ComputeTargetPosition(targetPos);
+ CmpPtr cmpPosition(GetEntityHandle());
+ return (targetPos-cmpPosition->GetPosition2D()).CompareLength(fixed::Zero()) <= 0;
+ }
+ return false;
+}
+
+bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle) 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;
+
+ // TODO: there's some asymmetry here when units look at other
+ // units' positions - the result will depend on the order of execution.
+ // Maybe we should split the updates into multiple phases to minimise
+ // that problem.
+
+ CmpPtr cmpPathfinder(GetSystemEntity());
+ ENSURE(cmpPathfinder);
+
+ fixed basicSpeed = m_Speed;
+ // If in formation, run to keep up; otherwise just walk.
+ if (IsFormationMember())
+ basicSpeed = m_Speed.Multiply(m_RunMultiplier);
+
+ // Find the speed factor of the underlying terrain.
+ // (We only care about the tile we start on - it doesn't matter if we're moving
+ // partially onto a much slower/faster tile).
+ // TODO: Terrain-dependent speeds are not currently supported.
+ fixed terrainSpeed = fixed::FromInt(1);
+
+ fixed maxSpeed = basicSpeed.Multiply(terrainSpeed);
+
+ fixed timeLeft = dt;
+ fixed zero = fixed::Zero();
+
+ ICmpObstructionManager::tag_t specificIgnore;
+ if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
+ {
+ CmpPtr 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 maxRotation = turnRate.Multiply(timeLeft);
+ fixed angleDiff = angle - atan2_approx(offset.X, offset.Y);
+ if (angleDiff != zero)
+ {
+ fixed absoluteAngleDiff = angleDiff.Absolute();
+ if (absoluteAngleDiff > entity_angle_t::Pi())
+ absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff;
+
+ // 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 > 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);
+ // Give some 'free' rotation for angles below 0.5 radians.
+ timeLeft = (std::min(maxRotation, maxRotation - absoluteAngleDiff + fixed::FromInt(1)/2)) / turnRate;
+ }
+ }
+
+ // Work out how far we can travel in timeLeft.
+ fixed maxdist = maxSpeed.Multiply(timeLeft);
+
+ // 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.
+ timeLeft = (maxdist - offsetLength) / 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;
+
+ 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)
+{
+ CmpPtr cmpObstruction(GetEntityHandle());
+ CmpPtr cmpVisual(GetEntityHandle());
+ // Idle this turn.
+ if (speed == fixed::Zero())
+ {
+ // Update moving flag if we moved last turn.
+ if (m_CurSpeed > fixed::Zero() && cmpObstruction)
+ cmpObstruction->SetMovingFlag(false);
+ if (cmpVisual)
+ cmpVisual->SelectMovementAnimation("idle", fixed::FromInt(1));
+ }
+ // Moved this turn
+ else
+ {
+ // Update moving flag if we didn't move last turn.
+ if (m_CurSpeed == fixed::Zero() && cmpObstruction)
+ cmpObstruction->SetMovingFlag(true);
+ if (cmpVisual)
+ cmpVisual->SelectMovementAnimation(speed > (m_WalkSpeed / 2).Multiply(m_RunMultiplier + fixed::FromInt(1)) ? "run" : "walk", speed);
+ }
+
+ m_CurSpeed = speed;
+}
+
+bool CCmpUnitMotion::HandleObstructedMove(bool moved)
+{
+ CmpPtr 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;
+
+ // 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, fixed::FromInt(TERRAIN_TILE_SIZE*2));
+ 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, fixed::FromInt(TERRAIN_TILE_SIZE), fixed::FromInt(TERRAIN_TILE_SIZE*3));
+ 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 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 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();
+ // Because units move one-at-a-time and pathing is asynchronous, we need to account for target movement,
+ // if we are computing this during the MT_Motion* part of the turn.
+ // If our entity ID is lower, we move first, and so we need to add a predicted movement to compute a path for next turn.
+ // If our entity ID is higher, the target has already moved, so we can just use the position directly.
+ // TODO: This does not really aim many turns in advance, with orthogonal trajectories it probably should.
+ CmpPtr cmpUnitMotion(GetSimContext(), moveRequest.m_Entity);
+ CmpPtr cmpUnitMotionManager(GetSystemEntity());
+ bool needInterpolation = cmpUnitMotion && cmpUnitMotion->IsMoveRequested() && cmpUnitMotionManager->ComputingMotion();
+ if (needInterpolation && GetEntityId() < moveRequest.m_Entity)
+ {
+ // Add predicted movement.
+ CFixedVector2D tempPos = out + (out - cmpTargetPosition->GetPreviousPosition2D());
+
+ CmpPtr cmpPosition(GetEntityHandle());
+ if (!cmpPosition || !cmpPosition->IsInWorld())
+ return true; // Still return true since we don't need a position for the target to have one.
+
+ // Fleeing fix: if we anticipate the target to go through us, we'll suddenly turn around, which is bad.
+ // Pretend that the target is still behind us in those cases.
+ if (m_MoveRequest.m_MinRange > fixed::Zero())
+ {
+ if ((out - cmpPosition->GetPosition2D()).RelativeOrientation(tempPos - cmpPosition->GetPosition2D()) >= 0)
+ out = tempPos;
+ }
+ else
+ out = tempPos;
+ }
+ else if (needInterpolation && GetEntityId() > moveRequest.m_Entity)
+ {
+ CmpPtr cmpPosition(GetEntityHandle());
+ if (!cmpPosition || !cmpPosition->IsInWorld())
+ return true; // Still return true since we don't need a position for the target to have one.
+
+ // Fleeing fix: opposite to above, check if our target has travelled through us already this turn.
+ CFixedVector2D tempPos = out - (out - cmpTargetPosition->GetPreviousPosition2D());
+ if (m_MoveRequest.m_MinRange > fixed::Zero() &&
+ (out - cmpPosition->GetPosition2D()).RelativeOrientation(tempPos - cmpPosition->GetPosition2D()) < 0)
+ out = tempPos;
+ }
+ }
+ return true;
+}
+
+bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from)
+{
+ CFixedVector2D targetPos;
+ if (!ComputeTargetPosition(targetPos))
+ return false;
+
+ CmpPtr 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 cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
+ if (cmpTargetObstruction)
+ specificIgnore = cmpTargetObstruction->GetObstruction();
+ }
+
+ if (specificIgnore.valid())
+ {
+ if (!cmpPathfinder->CheckMovement(SkipTagObstructionFilter(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;
+
+
+ // 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 cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
+ if (cmpTargetObstruction)
+ cmpTargetObstruction->GetObstructionSquare(estimatedTargetShape);
+ }
+
+ estimatedTargetShape.x = targetPos.X;
+ estimatedTargetShape.z = targetPos.Y;
+
+ CmpPtr 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 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 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 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 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 cmpTargetObstruction(GetSimContext(), moveRequest.m_Entity);
+ if (cmpTargetObstruction)
+ cmpTargetObstruction->GetObstructionSquare(targetObstruction);
+ }
+ targetObstruction.x = targetPosition.X;
+ targetObstruction.z = targetPosition.Y;
+
+ ICmpObstructionManager::ObstructionSquare obstruction;
+ CmpPtr cmpObstruction(GetEntityHandle());
+ if (cmpObstruction)
+ cmpObstruction->GetObstructionSquare(obstruction);
+ else
+ {
+ obstruction.x = pos.X;
+ obstruction.z = pos.Y;
+ }
+
+ CmpPtr cmpObstructionManager(GetSystemEntity());
+ ENSURE(cmpObstructionManager);
+
+ entity_pos_t distance = cmpObstructionManager->DistanceBetweenShapes(obstruction, targetObstruction);
+
+ 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 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(TERRAIN_TILE_SIZE)/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 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 and we can reach it in a straight line,
+ // then we'll just go along the straight line instead of computing a path.
+ if (!ShouldAlternatePathfinder() && TryGoingStraightToTarget(from))
+ 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 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 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);
+ }
+
+ m_ExpectedPathTicket.m_Type = Ticket::SHORT_PATH;
+ m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, true, 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 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 cmpPosition(GetEntityHandle());
+ if (!cmpPosition || !cmpPosition->IsInWorld())
+ return false;
+
+ MoveRequest request(target, minRange, maxRange);
+ PathGoal goal;
+ if (!ComputeGoal(goal, request))
+ return false;
+
+ CmpPtr 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& lines, CColor color)
+{
+ bool floating = false;
+ CmpPtr cmpPosition(GetEntityHandle());
+ if (cmpPosition)
+ floating = cmpPosition->CanFloat();
+
+ lines.clear();
+ std::vector 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;
+
+ RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH);
+ RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH);
+
+ for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i)
+ collector.Submit(&m_DebugOverlayLongPathLines[i]);
+
+ for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i)
+ collector.Submit(&m_DebugOverlayShortPathLines[i]);
+}
+
+#endif // INCLUDED_CCMPUNITMOTION
Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp
+++ ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp
@@ -1,1720 +0,0 @@
-/* Copyright (C) 2021 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 .
- */
-
-#include "precompiled.h"
-
-#include "simulation2/system/Component.h"
-#include "ICmpUnitMotion.h"
-
-#include "simulation2/components/ICmpObstruction.h"
-#include "simulation2/components/ICmpObstructionManager.h"
-#include "simulation2/components/ICmpOwnership.h"
-#include "simulation2/components/ICmpPosition.h"
-#include "simulation2/components/ICmpPathfinder.h"
-#include "simulation2/components/ICmpRangeManager.h"
-#include "simulation2/components/ICmpValueModificationManager.h"
-#include "simulation2/components/ICmpVisual.h"
-#include "simulation2/helpers/Geometry.h"
-#include "simulation2/helpers/Render.h"
-#include "simulation2/MessageTypes.h"
-#include "simulation2/serialization/SerializedPathfinder.h"
-#include "simulation2/serialization/SerializedTypes.h"
-
-#include "graphics/Overlay.h"
-#include "graphics/Terrain.h"
-#include "maths/FixedVector2D.h"
-#include "ps/CLogger.h"
-#include "ps/Profile.h"
-#include "renderer/Scene.h"
-
-// For debugging; units will start going straight to the target
-// instead of calling the pathfinder
-#define DISABLE_PATHFINDER 0
-
-/**
- * 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.
- */
-static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3)/2;
-static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*14);
-static const entity_pos_t SHORT_PATH_SEARCH_RANGE_INCREMENT = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1);
-static const u8 SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY = 2;
-
-/**
- * When using the short-pathfinder to rejoin a long-path waypoint, aim for a circle of this radius around the waypoint.
- */
-static const entity_pos_t SHORT_PATH_LONG_WAYPOINT_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1);
-
-/**
- * Minimum distance to goal for a long path request
- */
-static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4);
-
-/**
- * If we are this close to our target entity/point, then think about heading
- * for it in a straight line instead of pathfinding.
- */
-static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*6);
-
-/**
- * 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 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).
- */
-static const 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.
- * TODO: when unit pushing is implemented, this number can probably be lowered.
- */
-static const u8 MAX_FAILED_MOVEMENTS = 40;
-
-/**
- * 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).
- */
-static const u8 ALTERNATE_PATH_TYPE_DELAY = 3;
-static const u8 ALTERNATE_PATH_TYPE_EVERY = 6;
-
-/**
- * After this many failed computations, start sending "VERY_OBSTRUCTED" messages instead.
- * Should probably be larger than ALTERNATE_PATH_TYPE_DELAY.
- */
-static const u8 VERY_OBSTRUCTED_THRESHOLD = 10;
-
-static const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1);
-static const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1);
-
-class CCmpUnitMotion : public ICmpUnitMotion
-{
-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_Deserialized);
- }
-
- DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
-
- bool m_DebugOverlayEnabled;
- std::vector m_DebugOverlayLongPathLines;
- std::vector m_DebugOverlayShortPathLines;
-
- // Template state:
-
- bool m_FormationController;
-
- fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier;
- 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;
-
- // Number of turns since we last managed to move successfully.
- // 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 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;
-
- // Current mean speed (over the last turn).
- fixed m_CurSpeed;
-
- // 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
- "Provides the unit with the ability to move around the world by itself."
- ""
- "7.0"
- "default"
- ""
- ""
- ""
- ""
- ""
- ""
- ""
- ""
- ""
- ""
- ""
- ""
- ""
- ""
- "";
- }
-
- virtual void Init(const CParamNode& paramNode)
- {
- m_FormationController = paramNode.GetChild("FormationController").ToBool();
-
- m_FacePointAfterMove = true;
-
- m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed();
- m_SpeedMultiplier = fixed::FromInt(1);
- m_CurSpeed = fixed::Zero();
-
- m_RunMultiplier = m_TemplateRunMultiplier = fixed::FromInt(1);
- if (paramNode.GetChild("RunMultiplier").IsOk())
- m_RunMultiplier = m_TemplateRunMultiplier = paramNode.GetChild("RunMultiplier").ToFixed();
-
- CmpPtr cmpPathfinder(GetSystemEntity());
- if (cmpPathfinder)
- {
- m_PassClassName = paramNode.GetChild("PassabilityClass").ToUTF8();
- m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName);
- m_Clearance = cmpPathfinder->GetClearance(m_PassClass);
-
- CmpPtr cmpObstruction(GetEntityHandle());
- if (cmpObstruction)
- cmpObstruction->SetUnitClearance(m_Clearance);
- }
-
- m_DebugOverlayEnabled = false;
- }
-
- virtual void Deinit()
- {
- }
-
- template
- void SerializeCommon(S& serialize)
- {
- 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.NumberFixed_Unbounded("speed multiplier", m_SpeedMultiplier);
-
- serialize.NumberFixed_Unbounded("current speed", m_CurSpeed);
-
- serialize.Bool("facePointAfterMove", m_FacePointAfterMove);
-
- Serializer(serialize, "long path", m_LongPath.m_Waypoints);
- Serializer(serialize, "short path", m_ShortPath.m_Waypoints);
- }
-
- virtual void Serialize(ISerializer& serialize)
- {
- SerializeCommon(serialize);
- }
-
- virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
- {
- Init(paramNode);
-
- SerializeCommon(deserialize);
-
- CmpPtr cmpPathfinder(GetSystemEntity());
- if (cmpPathfinder)
- m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName);
- }
-
- virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
- {
- switch (msg.GetType())
- {
- case MT_RenderSubmit:
- {
- PROFILE("UnitMotion::RenderSubmit");
- const CMessageRenderSubmit& msgData = static_cast (msg);
- RenderSubmit(msgData.collector);
- break;
- }
- case MT_PathResult:
- {
- const CMessagePathResult& msgData = static_cast (msg);
- PathResult(msgData.ticket, msgData.path);
- break;
- }
- case MT_Create:
- {
- if (!ENTITY_IS_LOCAL(GetEntityId()))
- CmpPtr(GetSystemEntity())->Register(GetEntityId(), m_FormationController);
- break;
- }
- case MT_Destroy:
- {
- if (!ENTITY_IS_LOCAL(GetEntityId()))
- CmpPtr(GetSystemEntity())->Unregister(GetEntityId());
- break;
- }
- case MT_ValueModification:
- {
- const CMessageValueModification& msgData = static_cast (msg);
- if (msgData.component != L"UnitMotion")
- break;
- FALLTHROUGH;
- }
- case MT_OwnershipChanged:
- {
- OnValueModification();
- break;
- }
- case MT_Deserialized:
- {
- OnValueModification();
- if (!ENTITY_IS_LOCAL(GetEntityId()))
- CmpPtr(GetSystemEntity())->Register(GetEntityId(), m_FormationController);
- break;
- }
- }
- }
-
- void UpdateMessageSubscriptions()
- {
- bool needRender = m_DebugOverlayEnabled;
- GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender);
- }
-
- virtual bool IsMoveRequested() const
- {
- return m_MoveRequest.m_Type != MoveRequest::NONE;
- }
-
- virtual fixed GetSpeedMultiplier() const
- {
- return m_SpeedMultiplier;
- }
-
- virtual void SetSpeedMultiplier(fixed multiplier)
- {
- m_SpeedMultiplier = std::min(multiplier, m_RunMultiplier);
- m_Speed = m_SpeedMultiplier.Multiply(GetWalkSpeed());
- }
-
- virtual fixed GetSpeed() const
- {
- return m_Speed;
- }
-
- virtual fixed GetWalkSpeed() const
- {
- return m_WalkSpeed;
- }
-
- virtual fixed GetRunMultiplier() const
- {
- return m_RunMultiplier;
- }
-
- virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const
- {
- CmpPtr 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;
-
- // Copy the path so we don't change it.
- WaypointPath shortPath = m_ShortPath;
- WaypointPath longPath = m_LongPath;
-
- PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, angle);
- return pos;
- }
-
- virtual pass_class_t GetPassabilityClass() const
- {
- return m_PassClass;
- }
-
- virtual std::string GetPassabilityClassName() const
- {
- return m_PassClassName;
- }
-
- virtual void SetPassabilityClassName(const std::string& passClassName)
- {
- m_PassClassName = passClassName;
- CmpPtr cmpPathfinder(GetSystemEntity());
- if (cmpPathfinder)
- m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName);
- }
-
- virtual fixed GetCurrentSpeed() const
- {
- return m_CurSpeed;
- }
-
- virtual void SetFacePointAfterMove(bool facePointAfterMove)
- {
- m_FacePointAfterMove = facePointAfterMove;
- }
-
- virtual bool GetFacePointAfterMove() const
- {
- return m_FacePointAfterMove;
- }
-
- virtual void SetDebugOverlay(bool enabled)
- {
- m_DebugOverlayEnabled = enabled;
- UpdateMessageSubscriptions();
- }
-
- virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
- {
- return MoveTo(MoveRequest(CFixedVector2D(x, z), minRange, maxRange));
- }
-
- virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
- {
- return MoveTo(MoveRequest(target, minRange, maxRange));
- }
-
- virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z)
- {
- MoveTo(MoveRequest(target, CFixedVector2D(x, z)));
- }
-
- virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
-
- virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z);
-
- /**
- * 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.
- */
- virtual void StopMoving()
- {
- if (m_FacePointAfterMove)
- {
- CmpPtr 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();
- }
-
- virtual entity_pos_t GetUnitClearance() const
- {
- return m_Clearance;
- }
-
-private:
- bool ShouldAvoidMovingUnits() const
- {
- return !m_FormationController;
- }
-
- bool IsFormationMember() const
- {
- // TODO: this really shouldn't be what we are checking for.
- return m_MoveRequest.m_Type == MoveRequest::OFFSET;
- }
-
- bool IsFormationControllerMoving() const
- {
- CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity);
- return cmpControllerMotion && cmpControllerMotion->IsMoveRequested();
- }
-
- entity_id_t GetGroup() const
- {
- return IsFormationMember() ? m_MoveRequest.m_Entity : GetEntityId();
- }
-
- /**
- * 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 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).
- */
- virtual void OnTurnStart();
-
- virtual void PreMove(ICmpUnitMotionManager::MotionState& state);
-
- virtual void Move(ICmpUnitMotionManager::MotionState& state, fixed dt);
-
- virtual void PostMove(ICmpUnitMotionManager::MotionState& state, fixed dt);
-
- /**
- * Returns true if we are possibly at our destination.
- * Since the concept of being at destination is dependent on why the move was requested,
- * UnitMotion can only ever hint about this, hence the conditional tone.
- */
- bool PossiblyAtDestination() const;
-
- /**
- * 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, entity_angle_t& angle) const;
-
- /**
- * Update other components on our speed.
- * (For performance, this should try to avoid sending messages).
- */
- void UpdateMovementState(entity_pos_t speed);
-
- /**
- * 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);
-
- /**
- * 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);
-
- /**
- * Returns an appropriate obstruction filter for use with path requests.
- */
- ControlGroupMovementObstructionFilter GetObstructionFilter() const
- {
- return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), GetGroup());
- }
- /**
- * Filter a specific tag on top of the existing control groups.
- */
- SkipMovingTagAndControlGroupObstructionFilter GetObstructionFilter(const ICmpObstructionManager::tag_t& tag) const
- {
- return SkipMovingTagAndControlGroupObstructionFilter(tag, 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& 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 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(fixed::FromInt(TERRAIN_TILE_SIZE)) <= 0)
- {
- CmpPtr 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(ICmpUnitMotionManager::MotionState& state)
-{
- // If we were idle and will still be, no need for an update.
- state.needUpdate = state.cmpPosition->IsInWorld() &&
- (m_CurSpeed != fixed::Zero() || m_MoveRequest.m_Type != MoveRequest::NONE);
-}
-
-void CCmpUnitMotion::Move(ICmpUnitMotionManager::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);
-
- state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.angle);
-}
-
-void CCmpUnitMotion::PostMove(ICmpUnitMotionManager::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());
- }
- else
- {
- // Update the Position component after our movement (if we actually moved anywhere)
- // When moving always set the angle in the direction of the movement.
- CFixedVector2D offset = state.pos - state.initialPos;
- state.angle = atan2_approx(offset.X, offset.Y);
- state.cmpPosition->MoveAndTurnTo(state.pos.X, state.pos.Y, state.angle);
-
- // Calculate the mean speed over this past turn.
- UpdateMovementState(offset.Length() / dt);
- }
-
- if (state.wasObstructed && HandleObstructedMove(state.pos != state.initialPos))
- return;
- else if (!state.wasObstructed && state.pos != state.initialPos)
- m_FailedMovements = 0;
-
- // 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 && PathingUpdateNeeded(state.pos))
- {
- 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 cmpObstructionManager(GetSystemEntity());
- ENSURE(cmpObstructionManager);
-
- if (m_MoveRequest.m_Type == 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);
- if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
- return cmpObstructionManager->IsInTargetRange(GetEntityId(), m_MoveRequest.m_Entity, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false);
- if (m_MoveRequest.m_Type == MoveRequest::OFFSET)
- {
- CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity);
- if (cmpControllerMotion && cmpControllerMotion->IsMoveRequested())
- return false;
-
- // In formation, return a match only if we are exactly at the target position.
- // Otherwise, units can go in an infinite "walzting" loop when the Idle formation timer
- // reforms them.
- CFixedVector2D targetPos;
- ComputeTargetPosition(targetPos);
- CmpPtr cmpPosition(GetEntityHandle());
- return (targetPos-cmpPosition->GetPosition2D()).CompareLength(fixed::Zero()) <= 0;
- }
- return false;
-}
-
-bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle) 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;
-
- // TODO: there's some asymmetry here when units look at other
- // units' positions - the result will depend on the order of execution.
- // Maybe we should split the updates into multiple phases to minimise
- // that problem.
-
- CmpPtr cmpPathfinder(GetSystemEntity());
- ENSURE(cmpPathfinder);
-
- fixed basicSpeed = m_Speed;
- // If in formation, run to keep up; otherwise just walk.
- if (IsFormationMember())
- basicSpeed = m_Speed.Multiply(m_RunMultiplier);
-
- // Find the speed factor of the underlying terrain.
- // (We only care about the tile we start on - it doesn't matter if we're moving
- // partially onto a much slower/faster tile).
- // TODO: Terrain-dependent speeds are not currently supported.
- fixed terrainSpeed = fixed::FromInt(1);
-
- fixed maxSpeed = basicSpeed.Multiply(terrainSpeed);
-
- fixed timeLeft = dt;
- fixed zero = fixed::Zero();
-
- ICmpObstructionManager::tag_t specificIgnore;
- if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
- {
- CmpPtr 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 maxRotation = turnRate.Multiply(timeLeft);
- fixed angleDiff = angle - atan2_approx(offset.X, offset.Y);
- if (angleDiff != zero)
- {
- fixed absoluteAngleDiff = angleDiff.Absolute();
- if (absoluteAngleDiff > entity_angle_t::Pi())
- absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff;
-
- // 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 > 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);
- // Give some 'free' rotation for angles below 0.5 radians.
- timeLeft = (std::min(maxRotation, maxRotation - absoluteAngleDiff + fixed::FromInt(1)/2)) / turnRate;
- }
- }
-
- // Work out how far we can travel in timeLeft.
- fixed maxdist = maxSpeed.Multiply(timeLeft);
-
- // 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.
- timeLeft = (maxdist - offsetLength) / 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;
-
- 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)
-{
- CmpPtr cmpObstruction(GetEntityHandle());
- CmpPtr cmpVisual(GetEntityHandle());
- // Idle this turn.
- if (speed == fixed::Zero())
- {
- // Update moving flag if we moved last turn.
- if (m_CurSpeed > fixed::Zero() && cmpObstruction)
- cmpObstruction->SetMovingFlag(false);
- if (cmpVisual)
- cmpVisual->SelectMovementAnimation("idle", fixed::FromInt(1));
- }
- // Moved this turn
- else
- {
- // Update moving flag if we didn't move last turn.
- if (m_CurSpeed == fixed::Zero() && cmpObstruction)
- cmpObstruction->SetMovingFlag(true);
- if (cmpVisual)
- cmpVisual->SelectMovementAnimation(speed > (m_WalkSpeed / 2).Multiply(m_RunMultiplier + fixed::FromInt(1)) ? "run" : "walk", speed);
- }
-
- m_CurSpeed = speed;
-}
-
-bool CCmpUnitMotion::HandleObstructedMove(bool moved)
-{
- CmpPtr 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;
-
- // 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, fixed::FromInt(TERRAIN_TILE_SIZE*2));
- 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, fixed::FromInt(TERRAIN_TILE_SIZE), fixed::FromInt(TERRAIN_TILE_SIZE*3));
- 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 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 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();
- // Because units move one-at-a-time and pathing is asynchronous, we need to account for target movement,
- // if we are computing this during the MT_Motion* part of the turn.
- // If our entity ID is lower, we move first, and so we need to add a predicted movement to compute a path for next turn.
- // If our entity ID is higher, the target has already moved, so we can just use the position directly.
- // TODO: This does not really aim many turns in advance, with orthogonal trajectories it probably should.
- CmpPtr cmpUnitMotion(GetSimContext(), moveRequest.m_Entity);
- CmpPtr cmpUnitMotionManager(GetSystemEntity());
- bool needInterpolation = cmpUnitMotion && cmpUnitMotion->IsMoveRequested() && cmpUnitMotionManager->ComputingMotion();
- if (needInterpolation && GetEntityId() < moveRequest.m_Entity)
- {
- // Add predicted movement.
- CFixedVector2D tempPos = out + (out - cmpTargetPosition->GetPreviousPosition2D());
-
- CmpPtr cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return true; // Still return true since we don't need a position for the target to have one.
-
- // Fleeing fix: if we anticipate the target to go through us, we'll suddenly turn around, which is bad.
- // Pretend that the target is still behind us in those cases.
- if (m_MoveRequest.m_MinRange > fixed::Zero())
- {
- if ((out - cmpPosition->GetPosition2D()).RelativeOrientation(tempPos - cmpPosition->GetPosition2D()) >= 0)
- out = tempPos;
- }
- else
- out = tempPos;
- }
- else if (needInterpolation && GetEntityId() > moveRequest.m_Entity)
- {
- CmpPtr cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return true; // Still return true since we don't need a position for the target to have one.
-
- // Fleeing fix: opposite to above, check if our target has travelled through us already this turn.
- CFixedVector2D tempPos = out - (out - cmpTargetPosition->GetPreviousPosition2D());
- if (m_MoveRequest.m_MinRange > fixed::Zero() &&
- (out - cmpPosition->GetPosition2D()).RelativeOrientation(tempPos - cmpPosition->GetPosition2D()) < 0)
- out = tempPos;
- }
- }
- return true;
-}
-
-bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from)
-{
- CFixedVector2D targetPos;
- if (!ComputeTargetPosition(targetPos))
- return false;
-
- CmpPtr 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 cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
- if (cmpTargetObstruction)
- specificIgnore = cmpTargetObstruction->GetObstruction();
- }
-
- if (specificIgnore.valid())
- {
- if (!cmpPathfinder->CheckMovement(SkipTagObstructionFilter(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;
-
-
- // 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 cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
- if (cmpTargetObstruction)
- cmpTargetObstruction->GetObstructionSquare(estimatedTargetShape);
- }
-
- estimatedTargetShape.x = targetPos.X;
- estimatedTargetShape.z = targetPos.Y;
-
- CmpPtr 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 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 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 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 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 cmpTargetObstruction(GetSimContext(), moveRequest.m_Entity);
- if (cmpTargetObstruction)
- cmpTargetObstruction->GetObstructionSquare(targetObstruction);
- }
- targetObstruction.x = targetPosition.X;
- targetObstruction.z = targetPosition.Y;
-
- ICmpObstructionManager::ObstructionSquare obstruction;
- CmpPtr cmpObstruction(GetEntityHandle());
- if (cmpObstruction)
- cmpObstruction->GetObstructionSquare(obstruction);
- else
- {
- obstruction.x = pos.X;
- obstruction.z = pos.Y;
- }
-
- CmpPtr cmpObstructionManager(GetSystemEntity());
- ENSURE(cmpObstructionManager);
-
- entity_pos_t distance = cmpObstructionManager->DistanceBetweenShapes(obstruction, targetObstruction);
-
- 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 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(TERRAIN_TILE_SIZE)/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 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 and we can reach it in a straight line,
- // then we'll just go along the straight line instead of computing a path.
- if (!ShouldAlternatePathfinder() && TryGoingStraightToTarget(from))
- 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 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 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);
- }
-
- m_ExpectedPathTicket.m_Type = Ticket::SHORT_PATH;
- m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, true, 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 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 cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return false;
-
- MoveRequest request(target, minRange, maxRange);
- PathGoal goal;
- if (!ComputeGoal(goal, request))
- return false;
-
- CmpPtr 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& lines, CColor color)
-{
- bool floating = false;
- CmpPtr cmpPosition(GetEntityHandle());
- if (cmpPosition)
- floating = cmpPosition->CanFloat();
-
- lines.clear();
- std::vector 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;
-
- RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH);
- RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH);
-
- for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i)
- collector.Submit(&m_DebugOverlayLongPathLines[i]);
-
- for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i)
- collector.Submit(&m_DebugOverlayShortPathLines[i]);
-}
Index: ps/trunk/source/simulation2/components/CCmpUnitMotionManager.h
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotionManager.h
+++ ps/trunk/source/simulation2/components/CCmpUnitMotionManager.h
@@ -0,0 +1,142 @@
+/* Copyright (C) 2021 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 .
+ */
+
+#ifndef INCLUDED_CCMPUNITMOTIONMANAGER
+#define INCLUDED_CCMPUNITMOTIONMANAGER
+
+#include "simulation2/system/Component.h"
+#include "ICmpUnitMotionManager.h"
+
+#include "simulation2/MessageTypes.h"
+#include "simulation2/system/EntityMap.h"
+
+class CCmpUnitMotion;
+
+class CCmpUnitMotionManager : public ICmpUnitMotionManager
+{
+public:
+ static void ClassInit(CComponentManager& componentManager)
+ {
+ componentManager.SubscribeToMessageType(MT_TurnStart);
+ componentManager.SubscribeToMessageType(MT_Update_Final);
+ componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
+ componentManager.SubscribeToMessageType(MT_Update_MotionFormation);
+ }
+
+ DEFAULT_COMPONENT_ALLOCATOR(UnitMotionManager)
+
+ // Persisted state for each unit.
+ struct MotionState
+ {
+ // Component references - these must be kept alive for the duration of motion.
+ // NB: this is generally not something one should do, but because of the tight coupling here it's doable.
+ CmpPtr cmpPosition;
+ CCmpUnitMotion* cmpUnitMotion;
+
+ // Position before units start moving
+ CFixedVector2D initialPos;
+ // Transient position during the movement.
+ CFixedVector2D pos;
+
+ fixed initialAngle;
+ fixed angle;
+
+ // If true, the entity needs to be handled during movement.
+ bool needUpdate;
+
+ // 'Leak' from UnitMotion.
+ bool wentStraight;
+ bool wasObstructed;
+ };
+
+ EntityMap m_Units;
+ EntityMap m_FormationControllers;
+
+ // Temporary vector, reconstructed each turn (stored here to avoid memory reallocations).
+ std::vector::iterator> m_MovingUnits;
+
+ bool m_ComputingMotion;
+
+ static std::string GetSchema()
+ {
+ return "";
+ }
+
+ virtual void Init(const CParamNode& UNUSED(paramNode))
+ {
+ m_MovingUnits.reserve(40);
+ }
+
+ virtual void Deinit()
+ {
+ }
+
+ virtual void Serialize(ISerializer& UNUSED(serialize))
+ {
+ }
+
+ virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize))
+ {
+ Init(paramNode);
+ }
+
+ virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
+ {
+ switch (msg.GetType())
+ {
+ case MT_TurnStart:
+ {
+ OnTurnStart();
+ break;
+ }
+ case MT_Update_MotionFormation:
+ {
+ fixed dt = static_cast(msg).turnLength;
+ m_ComputingMotion = true;
+ MoveFormations(dt);
+ m_ComputingMotion = false;
+ break;
+ }
+ case MT_Update_MotionUnit:
+ {
+ fixed dt = static_cast(msg).turnLength;
+ m_ComputingMotion = true;
+ MoveUnits(dt);
+ m_ComputingMotion = false;
+ break;
+ }
+ }
+ }
+
+ virtual void Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController);
+ virtual void Unregister(entity_id_t ent);
+
+ virtual bool ComputingMotion() const
+ {
+ return m_ComputingMotion;
+ }
+
+ void OnTurnStart();
+
+ void MoveUnits(fixed dt);
+ void MoveFormations(fixed dt);
+ void Move(EntityMap& ents, fixed dt);
+};
+
+REGISTER_COMPONENT_TYPE(UnitMotionManager)
+
+#endif // INCLUDED_CCMPUNITMOTIONMANAGER
Index: ps/trunk/source/simulation2/components/CCmpUnitMotionManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotionManager.cpp
+++ ps/trunk/source/simulation2/components/CCmpUnitMotionManager.cpp
@@ -1,190 +0,0 @@
-/* Copyright (C) 2021 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 .
- */
-
-#include "precompiled.h"
-
-#include "simulation2/system/Component.h"
-#include "ICmpUnitMotionManager.h"
-
-#include "simulation2/MessageTypes.h"
-#include "simulation2/components/ICmpUnitMotion.h"
-#include "simulation2/system/EntityMap.h"
-
-#include "ps/CLogger.h"
-#include "ps/Profile.h"
-
-class CCmpUnitMotionManager : public ICmpUnitMotionManager
-{
-public:
- static void ClassInit(CComponentManager& componentManager)
- {
- componentManager.SubscribeToMessageType(MT_TurnStart);
- componentManager.SubscribeToMessageType(MT_Update_Final);
- componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
- componentManager.SubscribeToMessageType(MT_Update_MotionFormation);
- }
-
- DEFAULT_COMPONENT_ALLOCATOR(UnitMotionManager)
-
- EntityMap m_Units;
- EntityMap m_FormationControllers;
-
- // Temporary vector, reconstructed each turn (stored here to avoid memory reallocations).
- std::vector::iterator> m_MovingUnits;
-
- bool m_ComputingMotion;
-
- static std::string GetSchema()
- {
- return "";
- }
-
- virtual void Init(const CParamNode& UNUSED(paramNode))
- {
- m_MovingUnits.reserve(40);
- }
-
- virtual void Deinit()
- {
- }
-
- virtual void Serialize(ISerializer& UNUSED(serialize))
- {
- }
-
- virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize))
- {
- Init(paramNode);
- }
-
- virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
- {
- switch (msg.GetType())
- {
- case MT_TurnStart:
- {
- OnTurnStart();
- break;
- }
- case MT_Update_MotionFormation:
- {
- fixed dt = static_cast(msg).turnLength;
- m_ComputingMotion = true;
- MoveFormations(dt);
- m_ComputingMotion = false;
- break;
- }
- case MT_Update_MotionUnit:
- {
- fixed dt = static_cast(msg).turnLength;
- m_ComputingMotion = true;
- MoveUnits(dt);
- m_ComputingMotion = false;
- break;
- }
- }
- }
-
- virtual void Register(entity_id_t ent, bool formationController);
- virtual void Unregister(entity_id_t ent);
-
- virtual bool ComputingMotion() const
- {
- return m_ComputingMotion;
- }
-
- void OnTurnStart();
-
- void MoveUnits(fixed dt);
- void MoveFormations(fixed dt);
- void Move(EntityMap& ents, fixed dt);
-};
-
-void CCmpUnitMotionManager::Register(entity_id_t ent, bool formationController)
-{
- MotionState state = {
- CmpPtr(GetSimContext(), ent),
- CmpPtr(GetSimContext(), ent),
- CFixedVector2D(),
- CFixedVector2D(),
- fixed::Zero(),
- fixed::Zero(),
- false,
- false
- };
- if (!formationController)
- m_Units.insert(ent, state);
- else
- m_FormationControllers.insert(ent, state);
-}
-
-void CCmpUnitMotionManager::Unregister(entity_id_t ent)
-{
- EntityMap::iterator it = m_Units.find(ent);
- if (it != m_Units.end())
- {
- m_Units.erase(it);
- return;
- }
- it = m_FormationControllers.find(ent);
- if (it != m_FormationControllers.end())
- m_FormationControllers.erase(it);
-}
-
-void CCmpUnitMotionManager::OnTurnStart()
-{
- for (EntityMap::value_type& data : m_FormationControllers)
- data.second.cmpUnitMotion->OnTurnStart();
-
- for (EntityMap::value_type& data : m_Units)
- data.second.cmpUnitMotion->OnTurnStart();
-}
-
-void CCmpUnitMotionManager::MoveUnits(fixed dt)
-{
- Move(m_Units, dt);
-}
-
-void CCmpUnitMotionManager::MoveFormations(fixed dt)
-{
- Move(m_FormationControllers, dt);
-}
-
-void CCmpUnitMotionManager::Move(EntityMap& ents, fixed dt)
-{
- m_MovingUnits.clear();
- for (EntityMap::iterator it = ents.begin(); it != ents.end(); ++it)
- {
- it->second.cmpUnitMotion->PreMove(it->second);
- if (!it->second.needUpdate)
- continue;
- m_MovingUnits.push_back(it);
- it->second.initialPos = it->second.cmpPosition->GetPosition2D();
- it->second.initialAngle = it->second.cmpPosition->GetRotation().Y;
- it->second.pos = it->second.initialPos;
- it->second.angle = it->second.initialAngle;
- }
-
- for (EntityMap::iterator& it : m_MovingUnits)
- it->second.cmpUnitMotion->Move(it->second, dt);
-
- for (EntityMap::iterator& it : m_MovingUnits)
- it->second.cmpUnitMotion->PostMove(it->second, dt);
-}
-
-
-REGISTER_COMPONENT_TYPE(UnitMotionManager)
Index: ps/trunk/source/simulation2/components/CCmpUnitMotion_System.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotion_System.cpp
+++ ps/trunk/source/simulation2/components/CCmpUnitMotion_System.cpp
@@ -0,0 +1,101 @@
+/* Copyright (C) 2021 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 .
+ */
+
+#include "precompiled.h"
+
+#include "CCmpUnitMotion.h"
+#include "CCmpUnitMotionManager.h"
+
+#include "ps/CLogger.h"
+#include "ps/Profile.h"
+
+// NB: this TU contains the CCmpUnitMotion/CCmpUnitMotionManager couple.
+// In practice, UnitMotionManager functions need access to the full implementation of UnitMotion,
+// but UnitMotion needs access to MotionState (defined in UnitMotionManager).
+// To avoid inclusion issues, implementation of UnitMotionManager that uses UnitMotion is here.
+
+void CCmpUnitMotionManager::Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController)
+{
+ MotionState state = {
+ CmpPtr(GetSimContext(), ent),
+ component,
+ CFixedVector2D(),
+ CFixedVector2D(),
+ fixed::Zero(),
+ fixed::Zero(),
+ false,
+ false
+ };
+ if (!formationController)
+ m_Units.insert(ent, state);
+ else
+ m_FormationControllers.insert(ent, state);
+}
+
+void CCmpUnitMotionManager::Unregister(entity_id_t ent)
+{
+ EntityMap::iterator it = m_Units.find(ent);
+ if (it != m_Units.end())
+ {
+ m_Units.erase(it);
+ return;
+ }
+ it = m_FormationControllers.find(ent);
+ if (it != m_FormationControllers.end())
+ m_FormationControllers.erase(it);
+}
+
+void CCmpUnitMotionManager::OnTurnStart()
+{
+ for (EntityMap::value_type& data : m_FormationControllers)
+ data.second.cmpUnitMotion->OnTurnStart();
+
+ for (EntityMap::value_type& data : m_Units)
+ data.second.cmpUnitMotion->OnTurnStart();
+}
+
+void CCmpUnitMotionManager::MoveUnits(fixed dt)
+{
+ Move(m_Units, dt);
+}
+
+void CCmpUnitMotionManager::MoveFormations(fixed dt)
+{
+ Move(m_FormationControllers, dt);
+}
+
+void CCmpUnitMotionManager::Move(EntityMap& ents, fixed dt)
+{
+ m_MovingUnits.clear();
+ for (EntityMap::iterator it = ents.begin(); it != ents.end(); ++it)
+ {
+ it->second.cmpUnitMotion->PreMove(it->second);
+ if (!it->second.needUpdate)
+ continue;
+ m_MovingUnits.push_back(it);
+ it->second.initialPos = it->second.cmpPosition->GetPosition2D();
+ it->second.initialAngle = it->second.cmpPosition->GetRotation().Y;
+ it->second.pos = it->second.initialPos;
+ it->second.angle = it->second.initialAngle;
+ }
+
+ for (EntityMap::iterator& it : m_MovingUnits)
+ it->second.cmpUnitMotion->Move(it->second, dt);
+
+ for (EntityMap::iterator& it : m_MovingUnits)
+ it->second.cmpUnitMotion->PostMove(it->second, dt);
+}
Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpUnitMotion.h
+++ ps/trunk/source/simulation2/components/ICmpUnitMotion.h
@@ -22,9 +22,6 @@
#include "simulation2/components/ICmpPathfinder.h" // for pass_class_t
#include "simulation2/components/ICmpPosition.h" // for entity_pos_t
-#include "simulation2/components/ICmpUnitMotionManager.h"
-
-class CCmpUnitMotionManager;
/**
* Motion interface for entities with complex movement capabilities.
@@ -36,18 +33,6 @@
*/
class ICmpUnitMotion : public IComponent
{
-protected:
- friend class CCmpUnitMotionManager;
-
- /**
- * This external interface is used by the Unit Motion Manager.
- * Components that do not register there do not need to implement these.
- */
- virtual void OnTurnStart() = 0;
- virtual void PreMove(ICmpUnitMotionManager::MotionState& state) = 0;
- virtual void Move(ICmpUnitMotionManager::MotionState& state, fixed dt) = 0;
- virtual void PostMove(ICmpUnitMotionManager::MotionState& state, fixed dt) = 0;
-
public:
/**
Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp
+++ ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp
@@ -48,14 +48,6 @@
public:
DEFAULT_SCRIPT_WRAPPER(UnitMotionScripted)
-private:
- virtual void OnTurnStart() {};
- virtual void PreMove(ICmpUnitMotionManager::MotionState&) {};
- virtual void Move(ICmpUnitMotionManager::MotionState&, fixed) {};
- virtual void PostMove(ICmpUnitMotionManager::MotionState&, fixed) {};
-
-public:
-
virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
{
return m_Script.Call("MoveToPointRange", x, z, minRange, maxRange);
Index: ps/trunk/source/simulation2/components/ICmpUnitMotionManager.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpUnitMotionManager.h
+++ ps/trunk/source/simulation2/components/ICmpUnitMotionManager.h
@@ -20,40 +20,25 @@
#include "simulation2/system/Interface.h"
-#include "maths/Fixed.h"
-#include "maths/FixedVector2D.h"
-#include "simulation2/system/CmpPtr.h"
-
-class ICmpPosition;
-class ICmpUnitMotion;
+class CCmpUnitMotion;
+/**
+ * UnitMotionManager - handles motion for CCmpUnitMotion.
+ * This allows units to push past each other instead of requiring pathfinder computations,
+ * making movement much smoother overall.
+ */
class ICmpUnitMotionManager : public IComponent
{
public:
- // Persisted state for each unit.
- struct MotionState
- {
- // Component references - these must be kept alive for the duration of motion.
- CmpPtr cmpPosition;
- CmpPtr cmpUnitMotion;
-
- // Position before units start moving
- CFixedVector2D initialPos;
- // Transient position during the movement.
- CFixedVector2D pos;
-
- fixed initialAngle;
- fixed angle;
-
- // If true, the entity needs to be handled during movement.
- bool needUpdate;
-
- // 'Leak' from UnitMotion.
- bool wentStraight;
- bool wasObstructed;
- };
+ DECLARE_INTERFACE_TYPE(UnitMotionManager)
- virtual void Register(entity_id_t ent, bool formationController) = 0;
+private:
+ /**
+ * This class makes no sense outside of CCmpUnitMotion. This enforces that tight coupling.
+ */
+ friend class CCmpUnitMotion;
+
+ virtual void Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController) = 0;
virtual void Unregister(entity_id_t ent) = 0;
/**
@@ -61,7 +46,6 @@
*/
virtual bool ComputingMotion() const = 0;
- DECLARE_INTERFACE_TYPE(UnitMotionManager)
};
#endif // INCLUDED_ICMPUNITMOTIONMANAGER