Index: binaries/data/mods/public/simulation/components/Formation.js =================================================================== --- binaries/data/mods/public/simulation/components/Formation.js +++ binaries/data/mods/public/simulation/components/Formation.js @@ -865,7 +865,7 @@ { var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) - minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed()); + minSpeed = Math.min(minSpeed, cmpUnitMotion.GetSpeed()); } minSpeed *= this.GetSpeedMultiplier(); Index: binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/GuiInterface.js +++ binaries/data/mods/public/simulation/components/GuiInterface.js @@ -596,8 +596,8 @@ let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { - "walk": cmpUnitMotion.GetWalkSpeed(), - "run": cmpUnitMotion.GetRunSpeed() + "walk": cmpUnitMotion.GetSpeed(), + "run": cmpUnitMotion.GetSpeed() * cmpUnitMotion.GetTopSpeedRatio() }; return ret; Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -1,3 +1,5 @@ +const WALKING_SPEED = 1.0 + function UnitAI() {} UnitAI.prototype.Schema = @@ -1391,11 +1393,12 @@ "enter": function () { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); - if (cmpFormation && cmpVisual) +/* TOREPLACE if (cmpFormation && cmpVisual) { cmpVisual.ReplaceMoveAnimation("walk", cmpFormation.GetFormationAnimation(this.entity, "walk")); cmpVisual.ReplaceMoveAnimation("run", cmpFormation.GetFormationAnimation(this.entity, "run")); } + */ this.SelectAnimation("move"); }, @@ -1405,13 +1408,13 @@ // We can only finish this order if the move was really completed. if (!msg.data.error && this.FinishOrder()) return; - var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); +/* TOREPLACE var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) { cmpVisual.ResetMoveAnimation("walk"); cmpVisual.ResetMoveAnimation("run"); } - +*/ var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.SetInPosition(this.entity); @@ -1699,27 +1702,29 @@ }, "leave": function(msg) { - this.SetMoveSpeed(this.GetWalkSpeed()); + this.SetMoveSpeed(WALKING_SPEED); this.StopTimer(); }, "MoveStarted": function(msg) { // Adapt the speed to the one of the target if needed - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion.IsInTargetRange(this.isGuardOf, 0, 3*this.guardRange)) { - var cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI); - if (cmpUnitAI) + var cmpOtherMotion = Engine.QueryInterface(this.isGuardOf, IID_UnitMotion); + if (cmpOtherMotion) { - var speed = cmpUnitAI.GetWalkSpeed(); - if (speed < this.GetWalkSpeed()) + let otherSpeed = cmpOtherMotion.GetSpeed(); + let mySpeed = cmpUnitMotion.GetSpeed(); + let speed = otherSpeed / mySpeed; + if (speed < WALKING_SPEED) this.SetMoveSpeed(speed); } } }, "MoveCompleted": function() { - this.SetMoveSpeed(this.GetWalkSpeed()); + this.SetMoveSpeed(WALKING_SPEED); if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("GUARDING"); }, @@ -1774,19 +1779,12 @@ this.PlaySound("panic"); // Run quickly - var speed = this.GetRunSpeed(); - this.SelectAnimation("move"); - this.SetMoveSpeed(speed); - }, - - "HealthChanged": function() { - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); + this.SetMoveSpeed(this.GetRunSpeed()); }, "leave": function() { // Reset normal speed - this.SetMoveSpeed(this.GetWalkSpeed()); + this.SetMoveSpeed(WALKING_SPEED); }, "MoveCompleted": function() { @@ -2100,12 +2098,9 @@ this.SelectAnimation("move"); var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); + // Run after a fleeing target if (cmpUnitAI && cmpUnitAI.IsFleeing()) - { - // Run after a fleeing target - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); - } + this.SetMoveSpeed(this.GetRunSpeed()); this.StartTimer(1000, 1000); }, @@ -2113,13 +2108,14 @@ var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsFleeing()) return; - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); + // TODO: figure out what to do with fleeing + //var speed = this.GetRunSpeed(); + //this.SetMoveSpeed(speed); }, "leave": function() { // Reset normal speed in case it was changed - this.SetMoveSpeed(this.GetWalkSpeed()); + this.SetMoveSpeed(WALKING_SPEED); // Show carried resources when walking. this.SetGathererAnimationOverride(); @@ -3227,7 +3223,6 @@ "ROAMING": { "enter": function() { // Walk in a random direction - this.SelectAnimation("walk", false, this.GetWalkSpeed()); this.MoveRandomly(+this.template.RoamDistance); // Set a random timer to switch to feeding state this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); @@ -4010,12 +4005,14 @@ //// Message handlers ///// -UnitAI.prototype.OnMotionChanged = function(msg) +UnitAI.prototype.OnBeginMove = function(msg) +{ + this.UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg}); +}; + +UnitAI.prototype.OnFinishedMove = function(msg) { - if (msg.starting && !msg.error) - this.UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg}); - else if (!msg.starting || msg.error) - this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg}); + this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg}); }; UnitAI.prototype.OnGlobalConstructionFinished = function(msg) @@ -4076,22 +4073,10 @@ //// Helper functions to be called by the FSM //// -UnitAI.prototype.GetWalkSpeed = function() -{ - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.GetWalkSpeed(); -}; - UnitAI.prototype.GetRunSpeed = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - var runSpeed = cmpUnitMotion.GetRunSpeed(); - var walkSpeed = cmpUnitMotion.GetWalkSpeed(); - if (runSpeed <= walkSpeed) - return runSpeed; - var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); - var health = cmpHealth.GetHitpoints()/cmpHealth.GetMaxHitpoints(); - return (health*runSpeed + (1-health)*walkSpeed); + return cmpUnitMotion.GetTopSpeedRatio(); }; /** @@ -4290,11 +4275,12 @@ // Remove the animation override, so that weapons are shown again. if (disable) { - cmpVisual.ResetMoveAnimation("walk"); +//TOREPLACE cmpVisual.ResetMoveAnimation("walk"); return; } // Work out what we're carrying, in order to select an appropriate animation + /* var type = cmpResourceGatherer.GetLastCarriedType(); if (type) { @@ -4304,10 +4290,11 @@ if (type.specific == "meat") typename = "carry_" + type.specific; - cmpVisual.ReplaceMoveAnimation("walk", typename); +// TOREPLACE cmpVisual.ReplaceMoveAnimation("walk", typename); } else - cmpVisual.ResetMoveAnimation("walk"); + cmpVisual.ResetMoveAnimation("idle"); + */ }; UnitAI.prototype.SelectAnimation = function(name, once, speed, sound) @@ -4316,17 +4303,6 @@ if (!cmpVisual) return; - // Special case: the "move" animation gets turned into a special - // movement mode that deals with speeds and walk/run automatically - if (name == "move") - { - // Speed to switch from walking to running animations - var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2; - - cmpVisual.SelectMovementAnimation(runThreshold); - return; - } - var soundgroup; if (sound) { @@ -4359,18 +4335,20 @@ UnitAI.prototype.StopMoving = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpUnitMotion.StopMoving(); + cmpUnitMotion.DiscardMove(); }; UnitAI.prototype.MoveToPoint = function(x, z) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(3); return cmpUnitMotion.MoveToPointRange(x, z, 0, 0); }; UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(3); return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); }; @@ -4380,6 +4358,7 @@ return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(5); return cmpUnitMotion.MoveToTargetRange(target, 0, 0); }; @@ -4394,6 +4373,7 @@ var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(5); return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; @@ -4450,6 +4430,7 @@ var guessedMaxRange = (range.max + parabolicMaxRange)/2; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(9); if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange)) return true; @@ -4463,6 +4444,7 @@ return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(5); return cmpUnitMotion.MoveToTargetRange(target, min, max); }; @@ -4477,6 +4459,7 @@ var range = cmpGarrisonHolder.GetLoadingRange(); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(5); return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; @@ -5694,7 +5677,7 @@ UnitAI.prototype.SetMoveSpeed = function(speed) { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpMotion.SetSpeed(speed); + cmpMotion.SetSpeed(1.0); }; UnitAI.prototype.SetHeldPosition = function(x, z) Index: binaries/data/mods/public/simulation/components/UnitMotionFlying.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitMotionFlying.js +++ binaries/data/mods/public/simulation/components/UnitMotionFlying.js @@ -295,7 +295,7 @@ return this.IsInPointRange(targetPos.x, targetPos.y, minRange, maxRange); }; -UnitMotionFlying.prototype.GetWalkSpeed = function() +UnitMotionFlying.prototype.GetSpeed = function() { return +this.template.MaxSpeed; }; Index: binaries/data/mods/public/simulation/data/pathfinder.xml =================================================================== --- binaries/data/mods/public/simulation/data/pathfinder.xml +++ binaries/data/mods/public/simulation/data/pathfinder.xml @@ -11,7 +11,7 @@ pathfinding 2 1.0 - 0.8 + 0.7 pathfinding Index: binaries/data/mods/public/simulation/templates/gaia/fauna_hawk.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_hawk.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_hawk.xml @@ -6,11 +6,9 @@ false 1.0 - - 1000.0 - + - + fauna/hawk.xml Index: source/simulation2/MessageTypes.h =================================================================== --- source/simulation2/MessageTypes.h +++ source/simulation2/MessageTypes.h @@ -317,21 +317,55 @@ }; /** - * Sent by CCmpUnitMotion during Update, whenever the motion status has changed - * since the previous update. + * Sent by CCmpUnitMotion during Update, + * whenever we have started actually moving and were not moving before. + * We may or may not already have been trying to move */ -class CMessageMotionChanged : public CMessage +class CMessageBeginMove : public CMessage { public: - DEFAULT_MESSAGE_IMPL(MotionChanged) + DEFAULT_MESSAGE_IMPL(BeginMove) - CMessageMotionChanged(bool starting, bool error) : - starting(starting), error(error) + CMessageBeginMove() + { + } +}; + +/** + * Sent by CCmpUnitMotion during Update, + * whenever we were actually moving before, and cannot continue + * this can be because we've arrived (failed=false) or we failed moving (failed=true) + * After this message is sent, the unit won't remove/repath without orders. + * Will never be sent on the same turn as MT_BeginMove. + */ +class CMessageFinishedMove : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(FinishedMove) + + CMessageFinishedMove(bool fail) : failed(fail) { } - bool starting; // whether this is a start or end of movement - bool error; // whether we failed to start moving (couldn't find any path) + bool failed; // move failed +}; + +/** + * Sent by CCmpUnitMotion during Update, + * whenever we were actually moving before, and now stopped + * In this case, we will retry moving/pathing in the future on our own + * Unless ordered otherwise. + * We are just possibly stuck short-term, or must repath. + * Will never be sent on the same turn as MT_BeginMove. + */ +class CMessagePausedMove : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(PausedMove) + + CMessagePausedMove() + { + } }; /** Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -45,7 +45,9 @@ MESSAGE(PositionChanged) MESSAGE(InterpolatedPositionChanged) MESSAGE(TerritoryPositionChanged) -MESSAGE(MotionChanged) +MESSAGE(BeginMove) +MESSAGE(FinishedMove) +MESSAGE(PausedMove) MESSAGE(RangeUpdate) MESSAGE(TerrainChanged) MESSAGE(VisibilityChanged) Index: source/simulation2/components/CCmpObstructionManager.cpp =================================================================== --- source/simulation2/components/CCmpObstructionManager.cpp +++ source/simulation2/components/CCmpObstructionManager.cpp @@ -20,6 +20,7 @@ #include "simulation2/system/Component.h" #include "ICmpObstructionManager.h" +#include "ICmpPosition.h" #include "ICmpTerrain.h" #include "simulation2/MessageTypes.h" @@ -465,6 +466,9 @@ } } + virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange); + virtual bool IsInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); + virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits = false); virtual bool TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, std::vector* out); virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, std::vector* out); @@ -658,6 +662,83 @@ REGISTER_COMPONENT_TYPE(ObstructionManager) +bool CCmpObstructionManager::IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) +{ + CFixedVector2D pos(x, z); + + entity_pos_t distance = (pos - CFixedVector2D(px, pz)).Length(); + + if (distance < minRange) + return false; + else if (maxRange >= entity_pos_t::Zero() && distance > maxRange) + return false; + else + return true; +} + +bool CCmpObstructionManager::IsInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) +{ + CFixedVector2D pos(x, z); + + CmpPtr cmpObstructionManager(GetSystemEntity()); + if (!cmpObstructionManager) + return false; + + bool hasObstruction = false; + ICmpObstructionManager::ObstructionSquare obstruction; + CmpPtr cmpObstruction(GetSimContext(), target); + if (cmpObstruction) + hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); + + if (hasObstruction) + { + CFixedVector2D halfSize(obstruction.hw, obstruction.hh); + entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize, true); + + // Compare with previous obstruction + ICmpObstructionManager::ObstructionSquare previousObstruction; + cmpObstruction->GetPreviousObstructionSquare(previousObstruction); + entity_pos_t previousDistance = Geometry::DistanceToSquare(pos - CFixedVector2D(previousObstruction.x, previousObstruction.z), obstruction.u, obstruction.v, halfSize, true); + + // See if we're too close to the target square + bool inside = distance.IsZero() && !Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize).IsZero(); + if ((distance < minRange && previousDistance < minRange) || inside) + return false; + + // See if we're close enough to the target square + if (maxRange < entity_pos_t::Zero() || distance <= maxRange || previousDistance <= maxRange) + return true; + + entity_pos_t circleRadius = halfSize.Length(); + + if (Geometry::ShouldTreatTargetAsCircle(maxRange, circleRadius)) + { + // The target is small relative to our range, so pretend it's a circle + // and see if we're close enough to that. + // Also check circle around previous position. + entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius; + entity_pos_t previousCircleDistance = (pos - CFixedVector2D(previousObstruction.x, previousObstruction.z)).Length() - circleRadius; + + return circleDistance <= maxRange || previousCircleDistance <= maxRange; + } + + // take minimal clearance required in MoveToTargetRange into account, multiplying by 3/2 for diagonals + return distance <= maxRange || previousDistance <= maxRange; + } + else + { + CmpPtr cmpTargetPosition(GetSimContext(), target); + if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) + return false; + + CFixedVector2D targetPos = cmpTargetPosition->GetPreviousPosition2D(); + entity_pos_t distance = (pos - targetPos).Length(); + + return minRange <= distance && (maxRange < entity_pos_t::Zero() || distance <= maxRange); + } +} + + bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits) { PROFILE("TestLine"); @@ -1084,7 +1165,7 @@ m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = defaultColor; float a = atan2f(it->second.v.X.ToFloat(), it->second.v.Y.ToFloat()); - SimRender::ConstructSquareOnGround(GetSimContext(), it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.hw.ToFloat()*2, it->second.hh.ToFloat()*2, a, m_DebugOverlayLines.back(), true); + SimRender::ConstructSquareOnGround(GetSimContext(), it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.hw.ToFloat()*2 + 1, it->second.hh.ToFloat()*2 + 1, a, m_DebugOverlayLines.back(), true); } m_DebugOverlayDirty = false; Index: source/simulation2/components/CCmpPathfinder_Vertex.cpp =================================================================== --- source/simulation2/components/CCmpPathfinder_Vertex.cpp +++ source/simulation2/components/CCmpPathfinder_Vertex.cpp @@ -654,8 +654,6 @@ edges.emplace_back(Edge{ ev3, ev0 }); } - // TODO: should clip out vertexes and edges that are outside the range, - // to reduce the search space } // Add terrain obstructions Index: source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion.cpp +++ source/simulation2/components/CCmpUnitMotion.cpp @@ -27,6 +27,7 @@ #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" @@ -106,15 +107,104 @@ */ static const fixed CHECK_TARGET_MOVEMENT_MIN_COS = fixed::FromInt(866)/1000; +/** + * See unitmotion logic for details. Higher means units will retry more often before potentially failing. + */ +static const size_t MAX_PATH_REATTEMPS = 8; + 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 { +private: + struct SMotionGoal + { + private: + bool m_Valid = false; + + entity_pos_t m_TargetMinRange; + entity_pos_t m_TargetMaxRange; + + entity_id_t m_TargetEntity; + // pathfinder-compliant goal. + PathGoal m_Goal; + public: + SMotionGoal() : m_Valid(false) {}; + + SMotionGoal(PathGoal& goal, entity_pos_t minRange, entity_pos_t maxRange) + { + m_TargetEntity = INVALID_ENTITY; + + m_TargetMinRange = minRange; + m_TargetMaxRange = maxRange; + + m_Goal = goal; + m_Valid = true; + } + + SMotionGoal(const CSimContext& context, entity_id_t target, PathGoal& goal, entity_pos_t minRange, entity_pos_t maxRange) + { + m_TargetEntity = target; + m_TargetMinRange = minRange; + m_TargetMaxRange = maxRange; + + m_Goal = goal; + m_Valid = true; + + UpdateTargetPosition(context); + } + + template + void SerializeCommon(S& serialize) + { + serialize.Bool("valid", m_Valid); + + serialize.NumberFixed_Unbounded("target min range", m_TargetMinRange); + serialize.NumberFixed_Unbounded("target max range", m_TargetMaxRange); + + serialize.NumberU32_Unbounded("target entity", m_TargetEntity); + + SerializeGoal()(serialize, "goal", m_Goal); + } + + const PathGoal& Goal() const { return m_Goal; }; + + bool TargetIsEntity() const { return m_TargetEntity != INVALID_ENTITY; } + entity_id_t GetEntity() const { return m_TargetEntity; } + + bool Valid() const { return m_Valid; } + void Clear() { m_Valid = false; } + + entity_pos_t MinRange() const { return m_TargetMinRange; }; + entity_pos_t MaxRange() const { return m_TargetMaxRange; }; + + CFixedVector2D Pos() const { return CFixedVector2D(m_Goal.x, m_Goal.z); } + entity_pos_t X() const { return m_Goal.x; } + entity_pos_t Z() const { return m_Goal.z; } + + void UpdateTargetPosition(const CSimContext& context) + { + if (!TargetIsEntity()) + return; + + CmpPtr cmpPosition(context, m_TargetEntity); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; + + m_Goal.x = cmpPosition->GetPosition2D().X; + m_Goal.z = cmpPosition->GetPosition2D().Y; + } + + bool IsNotAPoint() const + { + return m_TargetMaxRange > fixed::Zero() || m_Goal.type != PathGoal::POINT; + } + }; + public: static void ClassInit(CComponentManager& componentManager) { - componentManager.SubscribeToMessageType(MT_Update_MotionFormation); componentManager.SubscribeToMessageType(MT_Update_MotionUnit); componentManager.SubscribeToMessageType(MT_PathResult); componentManager.SubscribeToMessageType(MT_OwnershipChanged); @@ -128,121 +218,43 @@ std::vector m_DebugOverlayLongPathLines; std::vector m_DebugOverlayShortPathLines; - // Template state: - - bool m_FormationController; - fixed m_WalkSpeed, m_OriginalWalkSpeed; // in metres per second - fixed m_RunSpeed, m_OriginalRunSpeed; + // Template state, never changed after init. + fixed m_TemplateSpeed, m_TopSpeedRatio; pass_class_t m_PassClass; std::string m_PassClassName; - - // Dynamic state: - entity_pos_t m_Clearance; - bool m_Moving; - bool m_FacePointAfterMove; - - enum State - { - /* - * Not moving at all. - */ - STATE_IDLE, - - /* - * Not moving at all. Will go to IDLE next turn. - * (This one-turn delay is a hack to fix animation timings.) - */ - STATE_STOPPING, - - /* - * Member of a formation. - * Pathing to the target (depending on m_PathState). - * Target is m_TargetEntity plus m_TargetOffset. - */ - STATE_FORMATIONMEMBER_PATH, - - /* - * Individual unit or formation controller. - * Pathing to the target (depending on m_PathState). - * Target is m_TargetPos, m_TargetMinRange, m_TargetMaxRange; - * if m_TargetEntity is not INVALID_ENTITY then m_TargetPos is tracking it. - */ - STATE_INDIVIDUAL_PATH, - - STATE_MAX - }; - u8 m_State; - - enum PathState - { - /* - * There is no path. - * (This should only happen in IDLE and STOPPING.) - */ - PATHSTATE_NONE, - - /* - * We have an outstanding long path request. - * No paths are usable yet, so we can't move anywhere. - */ - PATHSTATE_WAITING_REQUESTING_LONG, - - /* - * We have an outstanding short path request. - * m_LongPath is valid. - * m_ShortPath is not yet valid, so we can't move anywhere. - */ - PATHSTATE_WAITING_REQUESTING_SHORT, - - /* - * We are following our path, and have no path requests. - * m_LongPath and m_ShortPath are valid. - */ - PATHSTATE_FOLLOWING, - - /* - * We are following our path, and have an outstanding long path request. - * (This is because our target moved a long way and we need to recompute - * the whole path). - * m_LongPath and m_ShortPath are valid. - */ - PATHSTATE_FOLLOWING_REQUESTING_LONG, - - /* - * We are following our path, and have an outstanding short path request. - * (This is because our target moved and we've got a new long path - * which we need to follow). - * m_LongPath is valid; m_ShortPath is valid but obsolete. - */ - PATHSTATE_FOLLOWING_REQUESTING_SHORT, - - PATHSTATE_MAX - }; - u8 m_PathState; - - u32 m_ExpectedPathTicket; // asynchronous request ID we're waiting for, or 0 if none - - entity_id_t m_TargetEntity; - CFixedVector2D m_TargetPos; - CFixedVector2D m_TargetOffset; - entity_pos_t m_TargetMinRange; - entity_pos_t m_TargetMaxRange; + // TARGET + // As long as we have a valid target, the unit is considered "on the move". + // It may not be actually moving for a variety of reasons (no path, blocked path)… but it will shortly. + SMotionGoal m_FinalGoal; + + // MOTION PLANNING + // We will abort if we are stuck after X tries. + u8 m_AbortIfStuck; + // turn towards our target at the end + bool m_FacePointAfterMove; + // actual unit speed, after technology and ratio fixed m_Speed; + // cached for convenience + fixed m_SpeedRatio; - // Current mean speed (over the last turn). - fixed m_CurSpeed; + // asynchronous request ID we're waiting for, or 0 if none + u32 m_ExpectedPathTicket; // 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; - - // Motion planning - u8 m_Tries; // how many tries we've done to get to our current Final Goal. - - PathGoal m_FinalGoal; + WaypointPath m_Path; + // used for the short pathfinder, incremented on each unsuccessful try. + u8 m_Tries; + // Turns to wait before a certain action. + u8 m_WaitingTurns; + // if we actually started moving at some point. + bool m_StartedMoving; + + // Speed over the last turn + // cached so we can tell the visual actor when it changes + fixed m_ActualSpeed; static std::string GetSchema() { @@ -259,6 +271,10 @@ "" "" "" + "" + "" + "" + "" "" "" "" @@ -276,19 +292,15 @@ virtual void Init(const CParamNode& paramNode) { - m_FormationController = paramNode.GetChild("FormationController").ToBool(); - - m_Moving = false; m_FacePointAfterMove = true; - m_WalkSpeed = m_OriginalWalkSpeed = paramNode.GetChild("WalkSpeed").ToFixed(); - m_Speed = m_WalkSpeed; - m_CurSpeed = fixed::Zero(); - - if (paramNode.GetChild("Run").IsOk()) - m_RunSpeed = m_OriginalRunSpeed = paramNode.GetChild("Run").GetChild("Speed").ToFixed(); - else - m_RunSpeed = m_OriginalRunSpeed = m_WalkSpeed; + m_TemplateSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); + m_ActualSpeed = fixed::Zero(); + m_SpeedRatio = fixed::FromInt(1); + + m_TopSpeedRatio = fixed::FromInt(1); + if (paramNode.GetChild("RunMultiplier").IsOk()) + m_TopSpeedRatio = paramNode.GetChild("WalkSpeed").ToFixed(); CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) @@ -302,18 +314,13 @@ cmpObstruction->SetUnitClearance(m_Clearance); } - m_State = STATE_IDLE; - m_PathState = PATHSTATE_NONE; - m_ExpectedPathTicket = 0; m_Tries = 0; - - m_TargetEntity = INVALID_ENTITY; - - m_FinalGoal.type = PathGoal::POINT; + m_WaitingTurns = 0; m_DebugOverlayEnabled = false; + m_AbortIfStuck = 0; } virtual void Deinit() @@ -323,33 +330,24 @@ template void SerializeCommon(S& serialize) { - serialize.NumberU8("state", m_State, 0, STATE_MAX-1); - serialize.NumberU8("path state", m_PathState, 0, PATHSTATE_MAX-1); - - serialize.StringASCII("pass class", m_PassClassName, 0, 64); + serialize.NumberU8("abort if stuck", m_AbortIfStuck, 0, 255); + serialize.Bool("face point after move", m_FacePointAfterMove); + serialize.NumberFixed_Unbounded("speed", m_Speed); + serialize.NumberFixed_Unbounded("speed ratio", m_SpeedRatio); serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket); - serialize.NumberU32_Unbounded("target entity", m_TargetEntity); - serialize.NumberFixed_Unbounded("target pos x", m_TargetPos.X); - serialize.NumberFixed_Unbounded("target pos y", m_TargetPos.Y); - serialize.NumberFixed_Unbounded("target offset x", m_TargetOffset.X); - serialize.NumberFixed_Unbounded("target offset y", m_TargetOffset.Y); - serialize.NumberFixed_Unbounded("target min range", m_TargetMinRange); - serialize.NumberFixed_Unbounded("target max range", m_TargetMaxRange); - - serialize.NumberFixed_Unbounded("speed", m_Speed); - serialize.NumberFixed_Unbounded("current speed", m_CurSpeed); - - serialize.Bool("moving", m_Moving); - serialize.Bool("facePointAfterMove", m_FacePointAfterMove); + SerializeVector()(serialize, "path", m_Path.m_Waypoints); serialize.NumberU8("tries", m_Tries, 0, 255); + serialize.NumberU8("waiting turns", m_WaitingTurns, 0, 255); + + serialize.Bool("started moving", m_StartedMoving); - SerializeVector()(serialize, "long path", m_LongPath.m_Waypoints); - SerializeVector()(serialize, "short path", m_ShortPath.m_Waypoints); + // strictly speaking this doesn't need to be serialized since it's graphics-only, but it's nicer to. + serialize.NumberFixed_Unbounded("actual speed", m_ActualSpeed); - SerializeGoal()(serialize, "goal", m_FinalGoal); + m_FinalGoal.SerializeCommon(serialize); } virtual void Serialize(ISerializer& serialize) @@ -372,22 +370,10 @@ { switch (msg.GetType()) { - case MT_Update_MotionFormation: - { - if (m_FormationController) - { - fixed dt = static_cast (msg).turnLength; - Move(dt); - } - break; - } case MT_Update_MotionUnit: { - if (!m_FormationController) - { - fixed dt = static_cast (msg).turnLength; - Move(dt); - } + fixed dt = static_cast (msg).turnLength; + Move(dt); break; } case MT_RenderSubmit: @@ -410,24 +396,22 @@ break; } // fall-through - case MT_OwnershipChanged: case MT_Deserialized: { + // tell the visual actor our speed. + // don't call setactualspeed since the if check will return immediately. + CmpPtr cmpVisualActor(GetEntityHandle()); + if (cmpVisualActor) + cmpVisualActor->SetMovingSpeed(m_ActualSpeed); + } + case MT_OwnershipChanged: + { CmpPtr cmpValueModificationManager(GetSystemEntity()); if (!cmpValueModificationManager) break; - fixed newWalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_OriginalWalkSpeed, GetEntityId()); - fixed newRunSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/Run/Speed", m_OriginalRunSpeed, GetEntityId()); + m_Speed = m_SpeedRatio.Multiply(cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateSpeed, GetEntityId())); - // update m_Speed (the actual speed) if set to one of the variables - if (m_Speed == m_WalkSpeed) - m_Speed = newWalkSpeed; - else if (m_Speed == m_RunSpeed) - m_Speed = newRunSpeed; - - m_WalkSpeed = newWalkSpeed; - m_RunSpeed = newRunSpeed; break; } } @@ -439,19 +423,62 @@ GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender); } - virtual bool IsMoving() + virtual bool IsActuallyMoving() + { + return m_StartedMoving; + } + + virtual bool IsTryingToMove() + { + // speed check as sanity check to avoid infinite loops. + return m_FinalGoal.Valid() && m_Speed > fixed::Zero(); + } + + virtual fixed GetTemplateSpeed() { - return m_Moving; + return m_TemplateSpeed; } - virtual fixed GetWalkSpeed() + virtual fixed GetSpeed() { - return m_WalkSpeed; + return m_Speed; } - virtual fixed GetRunSpeed() + virtual fixed GetSpeedRatio() { - return m_RunSpeed; + return m_SpeedRatio; + } + + virtual fixed GetTopSpeedRatio() + { + return m_TopSpeedRatio; + } + + // don't call this all the time + // it's voluntarily too slow, because you shouldn't be doing this. + virtual void SetSpeed(fixed ratio) + { + m_SpeedRatio = ratio; + CmpPtr cmpValueModificationManager(GetSystemEntity()); + if (cmpValueModificationManager) + { + m_Speed = m_SpeedRatio.Multiply(cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateSpeed, GetEntityId())); + return; + } + + m_Speed = m_SpeedRatio.Multiply(m_TemplateSpeed); + } + + // convenience wrapper + void SetActualSpeed(fixed newRealSpeed) + { + if (m_ActualSpeed == newRealSpeed) + return; + + m_ActualSpeed = newRealSpeed; + CmpPtr cmpVisualActor(GetEntityHandle()); + if (cmpVisualActor) + cmpVisualActor->SetMovingSpeed(m_ActualSpeed); } virtual pass_class_t GetPassabilityClass() @@ -472,16 +499,6 @@ m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName); } - virtual fixed GetCurrentSpeed() - { - return m_CurSpeed; - } - - virtual void SetSpeed(fixed speed) - { - m_Speed = speed; - } - virtual void SetFacePointAfterMove(bool facePointAfterMove) { m_FacePointAfterMove = facePointAfterMove; @@ -493,101 +510,108 @@ UpdateMessageSubscriptions(); } + virtual entity_pos_t GetUnitClearance() + { + return m_Clearance; + } + virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange); virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange); virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); - virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z); virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z); - virtual void StopMoving() + virtual void SetAbortIfStuck(u8 shouldAbort) { - m_Moving = false; - m_ExpectedPathTicket = 0; - m_State = STATE_STOPPING; - m_PathState = PATHSTATE_NONE; - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); + m_AbortIfStuck = shouldAbort; } - virtual entity_pos_t GetUnitClearance() + virtual void DiscardMove() { - return m_Clearance; + StopMovingQuietly(); } -private: - bool ShouldAvoidMovingUnits() const + // stop moving and send message + virtual void CompleteMove() { - return !m_FormationController; + // highlight bugs. + if (!IsTryingToMove()) + { + LOGERROR("Entity %i trying to stop moving but has not actually started", GetEntityId()); + return; + } + + StopMovingQuietly(); + + if (m_FacePointAfterMove) + { + CmpPtr cmpPosition(GetEntityHandle()); + if (cmpPosition && cmpPosition->IsInWorld()) + FaceTowardsPointFromPos(cmpPosition->GetPosition2D(), m_FinalGoal.X(), m_FinalGoal.Z()); + } + + CMessageFinishedMove msg(false); + GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } +private: +/* + TODO: reimplement bool IsFormationMember() const { return m_State == STATE_FORMATIONMEMBER_PATH; } - +*/ entity_id_t GetGroup() const { - return IsFormationMember() ? m_TargetEntity : GetEntityId(); + //return IsFormationMember() ? m_TargetEntity : GetEntityId(); + return GetEntityId(); } bool HasValidPath() const { - return m_PathState == PATHSTATE_FOLLOWING - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT; + return !m_Path.m_Waypoints.empty(); } - void StartFailed() + void StopMovingQuietly() { - StopMoving(); - m_State = STATE_IDLE; // don't go through the STOPPING state since we never even started + // sanity + m_Tries = 0; + m_WaitingTurns = 0; + m_StartedMoving = false; + + // reset state. + m_ExpectedPathTicket = 0; + m_FinalGoal.Clear(); + m_Path.m_Waypoints.clear(); CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->SetMovingFlag(false); - - CMessageMotionChanged msg(true, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } void MoveFailed() { - StopMoving(); + StopMovingQuietly(); - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - CMessageMotionChanged msg(false, true); + CMessageFinishedMove msg(true); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } - void StartSucceeded() + void MovePaused() { - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); + m_StartedMoving = false; - m_Moving = true; - - CMessageMotionChanged msg(true, false); + CMessagePausedMove msg; GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } - void MoveSucceeded() + void MoveStarted() { - m_Moving = false; + m_StartedMoving = true; - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - // No longer moving, so speed is 0. - m_CurSpeed = fixed::Zero(); - - CMessageMotionChanged msg(false, false); + CMessageBeginMove msg; GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } @@ -604,45 +628,16 @@ void Move(fixed dt); /** - * 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; - - /** - * Computes the current location of our target entity (plus offset). - * Returns false if no target entity or no valid position. - */ - bool ComputeTargetPosition(CFixedVector2D& out); - - /** - * Attempts to replace the current path with a straight line to the goal, - * if this goal is a point, is close enough and the route is not obstructed. - */ - bool TryGoingStraightToGoalPoint(const CFixedVector2D& from); - - /** - * Attempts to replace the current path with a straight line to the target - * entity, if it's close enough and the route is not obstructed. - */ - bool TryGoingStraightToTargetEntity(const CFixedVector2D& from); - - /** * Returns whether the target entity has moved more than minDelta since our * last path computations, and we're close enough to it to care. */ bool CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta); /** - * Update goal position if moving target - */ - void UpdateFinalGoal(); - - /** * Returns whether we are close enough to the target to assume it's a good enough * position to stop. */ - bool ShouldConsiderOurselvesAtDestination(const CFixedVector2D& from); + bool ShouldConsiderOurselvesAtDestination(); /** * Returns whether the length of the given path, plus the distance from @@ -663,11 +658,11 @@ ControlGroupMovementObstructionFilter GetObstructionFilter(bool noTarget = false) const; /** - * Start moving to the given goal, from our current position 'from'. + * Dump current paths and request a new one. * Might go in a straight line immediately, or might start an asynchronous * path request. */ - void BeginPathing(const CFixedVector2D& from, const PathGoal& goal); + void RequestNewPath(); /** * Start an asynchronous long path query. @@ -691,489 +686,306 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path) { - // reset our state for sanity. - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - m_Moving = false; - // Ignore obsolete path requests if (ticket != m_ExpectedPathTicket) return; m_ExpectedPathTicket = 0; // we don't expect to get this result again - // Check that we are still able to do something with that path - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - { - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG || m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT) - StartFailed(); - else if (m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT) - StopMoving(); + if (!m_FinalGoal.Valid()) return; - } - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG) + if (path.m_Waypoints.empty()) { - m_LongPath = path; - - // If we are following a path, leave the old m_ShortPath so we can carry on following it - // until a new short path has been computed - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG) - m_ShortPath.m_Waypoints.clear(); - - // If there's no waypoints then we couldn't get near the target. - // Sort of hack: Just try going directly to the goal point instead - // (via the short pathfinder), so if we're stuck and the user clicks - // close enough to the unit then we can probably get unstuck - if (m_LongPath.m_Waypoints.empty()) - m_LongPath.m_Waypoints.emplace_back(Waypoint{ m_FinalGoal.x, m_FinalGoal.z }); - - if (!HasValidPath()) - StartSucceeded(); - - m_PathState = PATHSTATE_FOLLOWING; - - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); + // no waypoints, path failed. + // if we have some room, pop waypoint + // TODO: this isn't particularly bright. + if (!m_Path.m_Waypoints.empty()) + m_Path.m_Waypoints.pop_back(); - m_Moving = true; + // we will then deal with this on the next Move() call. + return; } - else if (m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT) - { - m_ShortPath = path; - - // If there's no waypoints then we couldn't get near the target - if (m_ShortPath.m_Waypoints.empty()) - { - // If we're globally following a long path, try to remove the next waypoint, it might be obstructed - // If not, and we are not in a formation, retry - // unless we are close to our target and we don't have a target entity. - // This makes sure that units don't clump too much when they are not in a formation and tasked to move. - if (m_LongPath.m_Waypoints.size() > 1) - m_LongPath.m_Waypoints.pop_back(); - else if (IsFormationMember()) - { - m_Moving = false; - CMessageMotionChanged msg(true, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); - return; - } - - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - - CFixedVector2D pos = cmpPosition->GetPosition2D(); - if (ShouldConsiderOurselvesAtDestination(pos)) - return; - - UpdateFinalGoal(); - RequestLongPath(pos, m_FinalGoal); - m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; - return; - } - - // else we could, so reset our number of tries. - m_Tries = 0; - - // Now we've got a short path that we can follow - if (!HasValidPath()) - StartSucceeded(); - - m_PathState = PATHSTATE_FOLLOWING; - - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); - - m_Moving = true; - } - else - LOGWARNING("unexpected PathResult (%u %d %d)", GetEntityId(), m_State, m_PathState); + // add to the top of our current waypoints + m_Path.m_Waypoints.insert(m_Path.m_Waypoints.end(), path.m_Waypoints.begin(), path.m_Waypoints.end()); } void CCmpUnitMotion::Move(fixed dt) { PROFILE("Move"); - if (m_State == STATE_STOPPING) + /** + * TODO: the visual actor doesn't interpolate, it merely changes things on update + * This means if a unit wants to change animation between turns (because it stops…) + * It will look slightly glitchy for a very short while + */ + + if (!IsTryingToMove()) { - m_State = STATE_IDLE; - MoveSucceeded(); + SetActualSpeed(fixed::Zero()); return; } - if (m_State == STATE_IDLE) + m_FinalGoal.UpdateTargetPosition(GetSimContext()); + + // TODO: units will look at each other's position in an arbitrary order that must be the same for any simulation + // In particular this means no threading. Maybe we should update this someday if it's possible. + + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) return; - switch (m_PathState) - { - case PATHSTATE_NONE: - { - // If we're not pathing, do nothing + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) return; - } - case PATHSTATE_WAITING_REQUESTING_LONG: - case PATHSTATE_WAITING_REQUESTING_SHORT: + CFixedVector2D initialPos = cmpPosition->GetPosition2D(); + + // Preliminary check: our target may be an entity and may have moved before us + if (ShouldConsiderOurselvesAtDestination()) { - // If we're waiting for a path and don't have one yet, do nothing + CompleteMove(); return; } - case PATHSTATE_FOLLOWING: - case PATHSTATE_FOLLOWING_REQUESTING_SHORT: - case PATHSTATE_FOLLOWING_REQUESTING_LONG: - { - // 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. + // TODO: here should go things such as: + // - has our target moved enough that we should re-path? + // end TODO - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return; + // Keep track of the current unit's position during the update + CFixedVector2D pos = initialPos; - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; + // 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 + // TODO: note that this is also linked to pathfinding so maybe never supported + // fixed terrainSpeed = fixed::FromInt(1); - CFixedVector2D initialPos = cmpPosition->GetPosition2D(); + bool wasObstructed = false; - // 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 - if (m_PathState == PATHSTATE_FOLLOWING) - TryGoingStraightToTargetEntity(initialPos); - - // Keep track of the current unit's position during the update - CFixedVector2D pos = initialPos; - - // If in formation, run to keep up; otherwise just walk - fixed basicSpeed; - if (IsFormationMember()) - basicSpeed = GetRunSpeed(); - else - basicSpeed = m_Speed; // (typically but not always WalkSpeed) - - // 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); + // We want to move (at most) m_Speed*dt units from pos towards the next waypoint - fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); + fixed timeLeft = dt; - bool wasObstructed = false; + // TODO: I think this may be a little buggy if we want to compute it several times per turn. + while (timeLeft > fixed::Zero()) + { + // If we ran out of path, we have to stop + if (!HasValidPath()) + break; - // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint + CFixedVector2D target; + target = CFixedVector2D(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z); - fixed timeLeft = dt; - fixed zero = fixed::Zero(); + CFixedVector2D offset = target - pos; + fixed offsetLength = offset.Length(); + // Work out how far we can travel in timeLeft + fixed maxdist = m_Speed.Multiply(timeLeft); + + CFixedVector2D destination; + if (offsetLength <= maxdist) + destination = target; + else + { + offset.Normalize(maxdist); + destination = pos + offset; + } - while (timeLeft > zero) + // TODO: try moving as much as we can still? + // TODO: get more information about what blocked us. + if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, destination.X, destination.Y, m_Clearance, m_PassClass)) { - // If we ran out of path, we have to stop - if (m_ShortPath.m_Waypoints.empty() && m_LongPath.m_Waypoints.empty()) - break; + pos = destination; - CFixedVector2D target; - if (m_ShortPath.m_Waypoints.empty()) - target = CFixedVector2D(m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z); - else - target = CFixedVector2D(m_ShortPath.m_Waypoints.back().x, m_ShortPath.m_Waypoints.back().z); + timeLeft = (timeLeft.Multiply(m_Speed) - offsetLength) / m_Speed; - CFixedVector2D offset = target - pos; + if (destination == target) + m_Path.m_Waypoints.pop_back(); + continue; + } + else + { + // Error - path was obstructed + wasObstructed = true; + break; + } + } - // Work out how far we can travel in timeLeft - fixed maxdist = maxSpeed.Multiply(timeLeft); + if (!m_StartedMoving && wasObstructed) + // If this is the turn we start moving, and we're already obstructed, + // fail the move entirely to avoid weirdness. + // (we would need to send a "move started" and a "move failed" message in the same turn) + pos = initialPos; - // If the target is close, we can move there directly - fixed offsetLength = offset.Length(); - if (offsetLength <= maxdist) - { - if (cmpPathfinder->CheckMovement(GetObstructionFilter(), 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 = timeLeft - (offsetLength / maxSpeed); - - if (m_ShortPath.m_Waypoints.empty()) - m_LongPath.m_Waypoints.pop_back(); - else - m_ShortPath.m_Waypoints.pop_back(); - - continue; - } - else - { - // Error - path was obstructed - wasObstructed = true; - break; - } - } - else - { - // Not close enough, so just move in the right direction - offset.Normalize(maxdist); - target = pos + offset; - - if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) - pos = target; - else - wasObstructed = true; // Error - path was obstructed + // Update the Position component after our movement (if we actually moved anywhere) + if (pos != initialPos) + { + CFixedVector2D offset = pos - initialPos; - break; - } - } + // Face towards the target + entity_angle_t angle = atan2_approx(offset.X, offset.Y); + cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle); - // Update the Position component after our movement (if we actually moved anywhere) - if (pos != initialPos) - { - CFixedVector2D offset = pos - initialPos; + // Calculate the mean speed over this past turn. + SetActualSpeed(cmpPosition->GetDistanceTravelled() / dt); - // Face towards the target - entity_angle_t angle = atan2_approx(offset.X, offset.Y); - cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle); + // tell other components and visual actor we are moving. + if (!m_StartedMoving) + MoveStarted(); - // Calculate the mean speed over this past turn. - m_CurSpeed = cmpPosition->GetDistanceTravelled() / dt; + // Check if we are at our destination + // since we're already checking in the general case at the beginning of this function, + // no need to do this outside this if block. + if (ShouldConsiderOurselvesAtDestination()) + { + CompleteMove(); + return; } - if (wasObstructed) + if (!wasObstructed) { - // Oops, we hit something (very likely another unit). - // This is when we might easily get stuck wrongly. + // everything is going smoothly, return. + m_Tries = 0; + m_WaitingTurns = 0; + return; + } + } + else + SetActualSpeed(fixed::Zero()); - // check if we've arrived. - if (ShouldConsiderOurselvesAtDestination(pos)) - return; - // If we still have long waypoints, try and compute a short path - // This will get us around units, amongst others. - // However in some cases a long waypoint will be in located in the obstruction of - // an idle unit. In that case, we need to scrap that waypoint or we might never be able to reach it. - // I am not sure why this happens but the following code seems to work. - if (!m_LongPath.m_Waypoints.empty()) - { - CmpPtr cmpObstructionManager(GetSystemEntity()); - if (cmpObstructionManager) - { - // create a fake obstruction to represent our waypoint. - ICmpObstructionManager::ObstructionSquare square; - square.hh = m_Clearance; - square.hw = m_Clearance; - square.u = CFixedVector2D(entity_pos_t::FromInt(1),entity_pos_t::FromInt(0)); - square.v = CFixedVector2D(entity_pos_t::FromInt(0),entity_pos_t::FromInt(1)); - square.x = m_LongPath.m_Waypoints.back().x; - square.z = m_LongPath.m_Waypoints.back().z; - std::vector unitOnGoal; - // don't ignore moving units as those might be units like us, ie not really moving. - cmpObstructionManager->GetUnitsOnObstruction(square, unitOnGoal, GetObstructionFilter(), true); - if (!unitOnGoal.empty()) - m_LongPath.m_Waypoints.pop_back(); - } - if (!m_LongPath.m_Waypoints.empty()) - { - PathGoal goal; - if (m_LongPath.m_Waypoints.size() > 1 || m_FinalGoal.DistanceToPoint(pos) > LONG_PATH_MIN_DIST) - goal = { PathGoal::POINT, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z }; - else - { - UpdateFinalGoal(); - goal = m_FinalGoal; - m_LongPath.m_Waypoints.clear(); - CFixedVector2D target = goal.NearestPointOnGoal(pos); - m_LongPath.m_Waypoints.emplace_back(Waypoint{ target.X, target.Y }); - } - RequestShortPath(pos, goal, true); - m_PathState = PATHSTATE_WAITING_REQUESTING_SHORT; - return; - } - } - // Else, just entirely recompute - UpdateFinalGoal(); - BeginPathing(pos, m_FinalGoal); + // tell relevant components we have paused if necessary + if (m_StartedMoving) + MovePaused(); - // potential TODO: We could switch the short-range pathfinder for something else entirely. - return; - } + // Oops, we've had a problem. Either we were obstructed, or we ran out of path (but still have a goal). + // Handle it. + // Failure to handle it will result in stuckness and players complaining. - // We successfully moved along our path, until running out of - // waypoints or time. + if (m_ExpectedPathTicket != 0) + // wait until we get our path to see where that leads us. + return; - if (m_PathState == PATHSTATE_FOLLOWING) + // if our next waypoint is close enough to our goal and our goal isn't a point, drop our path and recompute directly. + if (m_FinalGoal.IsNotAPoint() && !m_Path.m_Waypoints.empty()) + { + + CmpPtr cmpObstructionManager(GetSystemEntity()); + if (cmpObstructionManager) { - // If we're not currently computing any new paths: - if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty()) + bool inRange = false; + if (m_FinalGoal.TargetIsEntity()) + inRange = cmpObstructionManager->IsInTargetRange(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z, + m_FinalGoal.GetEntity(), m_FinalGoal.MinRange(), m_FinalGoal.MaxRange()); + else + inRange = cmpObstructionManager->IsInPointRange(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z, + m_FinalGoal.X(), m_FinalGoal.Z(), m_FinalGoal.MinRange(), m_FinalGoal.MaxRange()); + if (inRange) { - if (IsFormationMember()) - { - // We've reached our assigned position. If the controller - // is idle, send a notification in case it should disband, - // otherwise continue following the formation next turn. - CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity); - if (cmpUnitMotion && !cmpUnitMotion->IsMoving()) - { - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - m_Moving = false; - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); - } - } - else - { - // check if target was reached in case of a moving target - CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity); - if (cmpUnitMotion && cmpUnitMotion->IsMoving() && - MoveToTargetRange(m_TargetEntity, m_TargetMinRange, m_TargetMaxRange)) - return; - - // Not in formation, so just finish moving - StopMoving(); - m_State = STATE_IDLE; - MoveSucceeded(); - - if (m_FacePointAfterMove) - FaceTowardsPointFromPos(pos, m_FinalGoal.x, m_FinalGoal.z); - // TODO: if the goal was a square building, we ought to point towards the - // nearest point on the square, not towards its center - } + m_Path.m_Waypoints.clear(); + m_WaitingTurns = MAX_PATH_REATTEMPS; // short path } - - // If we have a target entity, and we're not miles away from the end of - // our current path, and the target moved enough, then recompute our - // whole path - if (IsFormationMember()) - CheckTargetMovement(pos, CHECK_TARGET_MOVEMENT_MIN_DELTA_FORMATION); - else - CheckTargetMovement(pos, CHECK_TARGET_MOVEMENT_MIN_DELTA); } } + + // give us some turns to recover. + // TODO: only do this if we ran into a moving unit and not something else, because something else won't move + // specifically: if we ran into a moving unit, we should wait a turn and see what happens + // if we ran into a static unit, recompute a short-path directly + // if we ran into a static obstruction, recompute long-path directly + // And then maybe we could add some finetuning based on target. + if (m_WaitingTurns == 0) + { + if (HasValidPath()) + m_WaitingTurns = MAX_PATH_REATTEMPS; + else + m_WaitingTurns = 3; } -} -bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out) -{ - if (m_TargetEntity == INVALID_ENTITY) - return false; + --m_WaitingTurns; - CmpPtr cmpPosition(GetSimContext(), m_TargetEntity); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + // Try again next turn, no changes + if (m_WaitingTurns >= MAX_PATH_REATTEMPS) + return; - if (m_TargetOffset.IsZero()) + // already waited one turn, no changes, so try computing a short path. + if (m_WaitingTurns >= 3) { - // No offset, just return the position directly - out = cmpPosition->GetPosition2D(); + PathGoal goal; + if (m_Path.m_Waypoints.empty()) + goal = { PathGoal::POINT, m_FinalGoal.X(), m_FinalGoal.Z() }; + else + { + goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z }; + m_Path.m_Waypoints.pop_back(); + } + RequestShortPath(pos, goal, true); + return; } - else + + // Last resort, compute a long path + if (m_WaitingTurns == 2) { - // There is an offset, so compute it relative to orientation - entity_angle_t angle = cmpPosition->GetRotation().Y; - CFixedVector2D offset = m_TargetOffset.Rotate(angle); - out = cmpPosition->GetPosition2D() + offset; + PathGoal goal; + if (m_Path.m_Waypoints.empty()) + goal = { PathGoal::POINT, m_FinalGoal.X(), m_FinalGoal.Z() }; + else + { + goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z }; + m_Path.m_Waypoints.pop_back(); + } + RequestLongPath(pos, goal); + return; } - return true; -} -bool CCmpUnitMotion::TryGoingStraightToGoalPoint(const CFixedVector2D& from) -{ - // Make sure the goal is a point (and not a point-like target like a formation controller) - if (m_FinalGoal.type != PathGoal::POINT || m_TargetEntity != INVALID_ENTITY) - return false; - // Fail if the goal is too far away - CFixedVector2D goalPos(m_FinalGoal.x, m_FinalGoal.z); - if ((goalPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) - return false; + // m_waitingTurns == 1 here - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; + // we tried getting a renewed path and still got stuck + if (m_AbortIfStuck == 0) + { + MoveFailed(); + return; + } - // Check if there's any collisions on that route - if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) - return false; + --m_AbortIfStuck; - // 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 }); + // Recompute a new path, but wait a few turns first + m_WaitingTurns = 4 + MAX_PATH_REATTEMPS; - return true; + return; } -bool CCmpUnitMotion::TryGoingStraightToTargetEntity(const CFixedVector2D& from) +// TODO: this should care about target movement +bool CCmpUnitMotion::CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta) { - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return false; - - // Fail if the target is too far away - if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) + if (!m_FinalGoal.TargetIsEntity()) return false; - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; - - // Move the goal to match the target entity's new position - PathGoal goal = m_FinalGoal; - 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) + if (!HasValidPath()) + return true; - // Find the point on the goal shape that we should head towards - CFixedVector2D goalPos = goal.NearestPointOnGoal(from); + // Fail unless the target has moved enough + CFixedVector2D oldTargetPos = CFixedVector2D(m_Path.m_Waypoints[0].x,m_Path.m_Waypoints[0].z); - // Check if there's any collisions on that route - if (!cmpPathfinder->CheckMovement(GetObstructionFilter(true), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) + if ((m_FinalGoal.Pos() - oldTargetPos).CompareLength(minDelta) < 0) return false; - // That route is okay, so update our path - m_FinalGoal = goal; - 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::CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta) -{ - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return false; - - // Fail unless the target has moved enough - CFixedVector2D oldTargetPos(m_FinalGoal.x, m_FinalGoal.z); - if ((targetPos - oldTargetPos).CompareLength(minDelta) < 0) - return false; CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; CFixedVector2D pos = cmpPosition->GetPosition2D(); CFixedVector2D oldDir = (oldTargetPos - pos); - CFixedVector2D newDir = (targetPos - pos); + CFixedVector2D newDir = (m_FinalGoal.Pos() - pos); oldDir.Normalize(); newDir.Normalize(); // Fail unless we're close enough to the target to care about its movement // and the angle between the (straight-line) directions of the previous and new target positions is small - if (oldDir.Dot(newDir) > CHECK_TARGET_MOVEMENT_MIN_COS && !PathIsShort(m_LongPath, from, CHECK_TARGET_MOVEMENT_AT_MAX_DIST)) + if (oldDir.Dot(newDir) > CHECK_TARGET_MOVEMENT_MIN_COS && !PathIsShort(m_Path, from, CHECK_TARGET_MOVEMENT_AT_MAX_DIST)) return false; // Fail if the target is no longer visible to this entity's owner @@ -1183,47 +995,26 @@ if (cmpOwnership) { CmpPtr cmpRangeManager(GetSystemEntity()); - if (cmpRangeManager && cmpRangeManager->GetLosVisibility(m_TargetEntity, cmpOwnership->GetOwner()) == ICmpRangeManager::VIS_HIDDEN) + if (cmpRangeManager && cmpRangeManager->GetLosVisibility(m_FinalGoal.GetEntity(), cmpOwnership->GetOwner()) == ICmpRangeManager::VIS_HIDDEN) return false; } // The target moved and we need to update our current path; - // change the goal here and expect our caller to start the path request - m_FinalGoal.x = targetPos.X; - m_FinalGoal.z = targetPos.Y; - RequestLongPath(from, m_FinalGoal); - m_PathState = PATHSTATE_FOLLOWING_REQUESTING_LONG; + // Expect our caller to recompute + // Dump our current path. + m_Path.m_Waypoints.clear(); return true; } -void CCmpUnitMotion::UpdateFinalGoal() +// TODO: ought to be cleverer here. +// In particular maybe we should support some "margin" for error. +bool CCmpUnitMotion::ShouldConsiderOurselvesAtDestination() { - if (m_TargetEntity == INVALID_ENTITY) - return; - CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity); - if (!cmpUnitMotion) - return; - if (IsFormationMember()) - return; - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return; - m_FinalGoal.x = targetPos.X; - m_FinalGoal.z = targetPos.Y; -} - -bool CCmpUnitMotion::ShouldConsiderOurselvesAtDestination(const CFixedVector2D& from) -{ - if (m_TargetEntity != INVALID_ENTITY || m_FinalGoal.DistanceToPoint(from) > SHORT_PATH_GOAL_RADIUS) - return false; - - StopMoving(); - MoveSucceeded(); - - if (m_FacePointAfterMove) - FaceTowardsPointFromPos(from, m_FinalGoal.x, m_FinalGoal.z); - return true; + if (m_FinalGoal.TargetIsEntity()) + return IsInTargetRange(m_FinalGoal.GetEntity(), m_FinalGoal.MinRange(), m_FinalGoal.MaxRange()); + else + return IsInPointRange(m_FinalGoal.X(),m_FinalGoal.Z(), m_FinalGoal.MinRange(), m_FinalGoal.MaxRange()); } bool CCmpUnitMotion::PathIsShort(const WaypointPath& path, const CFixedVector2D& from, entity_pos_t minDistance) const @@ -1275,79 +1066,47 @@ ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter(bool noTarget) const { - entity_id_t group = noTarget ? m_TargetEntity : GetGroup(); - return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), group); + entity_id_t group = noTarget ? m_FinalGoal.GetEntity() : GetGroup(); + // TODO: if we sometimes want to consider moving units, change here. + return ControlGroupMovementObstructionFilter(false, group); } - - -void CCmpUnitMotion::BeginPathing(const CFixedVector2D& from, const PathGoal& goal) +// TODO: this should be improved, it's a little limited +// EG use of hierarchical pathfinder,… +// also it should probably make the goal passable directly, to avoid conflict with the paths returned. +void CCmpUnitMotion::RequestNewPath() { - // reset our state for sanity. - m_ExpectedPathTicket = 0; + ENSURE(m_ExpectedPathTicket == 0); - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition) + return; - m_Moving = false; + // dump current path + m_Path.m_Waypoints.clear(); - m_PathState = PATHSTATE_NONE; + CFixedVector2D position = cmpPosition->GetPosition2D(); #if DISABLE_PATHFINDER { CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY); - CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from); + CFixedVector2D goalPos = m_FinalGoal.Goal().NearestPointOnGoal(position); m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); - m_PathState = PATHSTATE_FOLLOWING; return; } #endif - // If we're aiming at a target entity and it's 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 (TryGoingStraightToTargetEntity(from)) - { - if (!HasValidPath()) - StartSucceeded(); - m_PathState = PATHSTATE_FOLLOWING; - return; - } - - // Same thing applies to non-entity points - if (TryGoingStraightToGoalPoint(from)) - { - if (!HasValidPath()) - StartSucceeded(); - m_PathState = PATHSTATE_FOLLOWING; - 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. - if (goal.DistanceToPoint(from) < LONG_PATH_MIN_DIST) - { - // add our final goal as a long range waypoint so we don't forget - // where we are going if the short-range pathfinder returns - // an aborted path. - m_LongPath.m_Waypoints.clear(); - CFixedVector2D target = m_FinalGoal.NearestPointOnGoal(from); - m_LongPath.m_Waypoints.emplace_back(Waypoint{ target.X, target.Y }); - m_PathState = PATHSTATE_WAITING_REQUESTING_SHORT; - RequestShortPath(from, goal, true); - } + // Maybe use PathIsShort? + if (m_FinalGoal.Goal().DistanceToPoint(position) < LONG_PATH_MIN_DIST) + RequestShortPath(position, m_FinalGoal.Goal(), true); else - { - m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; - RequestLongPath(from, goal); - } + RequestLongPath(position, m_FinalGoal.Goal()); } void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal) @@ -1373,7 +1132,7 @@ return; // wrapping around on m_Tries isn't really a problem so don't check for overflow. - fixed searchRange = std::max(SHORT_PATH_MIN_SEARCH_RANGE * ++m_Tries, goal.DistanceToPoint(from)); + fixed searchRange = std::max(SHORT_PATH_MIN_SEARCH_RANGE * (++m_Tries + 1), goal.DistanceToPoint(from)); if (goal.type != PathGoal::POINT && searchRange < goal.hw && searchRange < SHORT_PATH_MIN_SEARCH_RANGE * 2) searchRange = std::min(goal.hw, SHORT_PATH_MIN_SEARCH_RANGE * 2); if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE) @@ -1389,8 +1148,11 @@ bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange, entity_id_t target) { + // Must closely mirror CmpObstructionManager::IsInPointRange PROFILE("MoveToPointRange"); + DiscardMove(); + CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; @@ -1442,76 +1204,23 @@ } } - m_State = STATE_INDIVIDUAL_PATH; - m_TargetEntity = target; - m_TargetOffset = CFixedVector2D(); - m_TargetMinRange = minRange; - m_TargetMaxRange = maxRange; - m_FinalGoal = goal; - m_Tries = 0; - - BeginPathing(pos, goal); - - return true; -} - -bool CCmpUnitMotion::IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) -{ - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; - - CFixedVector2D pos = cmpPosition->GetPosition2D(); - - bool hasObstruction = false; - CmpPtr cmpObstructionManager(GetSystemEntity()); - ICmpObstructionManager::ObstructionSquare obstruction; -//TODO if (cmpObstructionManager) -// hasObstruction = cmpObstructionManager->FindMostImportantObstruction(GetObstructionFilter(), x, z, m_Radius, obstruction); - - if (minRange.IsZero() && maxRange.IsZero() && hasObstruction) - { - // Handle the non-ranged mode: - CFixedVector2D halfSize(obstruction.hw, obstruction.hh); - entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize); - - // See if we're too close to the target square - if (distance < minRange) - return false; - - // See if we're close enough to the target square - if (maxRange < entity_pos_t::Zero() || distance <= maxRange) - return true; - - return false; - } + if (target == INVALID_ENTITY) + m_FinalGoal = SMotionGoal(goal, minRange, maxRange); else - { - entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length(); + m_FinalGoal = SMotionGoal(GetSimContext(), target, goal, minRange, maxRange); - if (distance < minRange) - return false; - else if (maxRange >= entity_pos_t::Zero() && distance > maxRange) - return false; - else - return true; - } -} + RequestNewPath(); -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); + return true; } bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { + // Must closely mirror CmpObstructionManager::IsInTargetRange PROFILE("MoveToTargetRange"); + DiscardMove(); + CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; @@ -1588,7 +1297,7 @@ entity_pos_t goalDistance = minRange + Pathfinding::GOAL_DELTA; - if (ShouldTreatTargetAsCircle(minRange, circleRadius)) + if (Geometry::ShouldTreatTargetAsCircle(minRange, circleRadius)) { // The target is small relative to our range, so pretend it's a circle goal.type = PathGoal::INVERTED_CIRCLE; @@ -1616,7 +1325,7 @@ // Circumscribe the square entity_pos_t circleRadius = halfSize.Length(); - if (ShouldTreatTargetAsCircle(maxRange, circleRadius)) + if (Geometry::ShouldTreatTargetAsCircle(maxRange, circleRadius)) { // The target is small relative to our range, so pretend it's a circle @@ -1655,117 +1364,47 @@ } } - m_State = STATE_INDIVIDUAL_PATH; - m_TargetEntity = target; - m_TargetOffset = CFixedVector2D(); - m_TargetMinRange = minRange; - m_TargetMaxRange = maxRange; - m_FinalGoal = goal; - m_Tries = 0; + if (target == INVALID_ENTITY) + m_FinalGoal = SMotionGoal(goal, minRange, maxRange); + else + m_FinalGoal = SMotionGoal(GetSimContext(), target, goal, minRange, maxRange); - BeginPathing(pos, goal); + RequestNewPath(); return true; } -bool CCmpUnitMotion::IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) +bool CCmpUnitMotion::IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) { - // This function closely mirrors MoveToTargetRange - it needs to return true - // after that Move has completed - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) + if (!cmpPosition) return false; - CFixedVector2D pos = cmpPosition->GetPosition2D(); - CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) - return false; - - bool hasObstruction = false; - ICmpObstructionManager::ObstructionSquare obstruction; - CmpPtr cmpObstruction(GetSimContext(), target); - if (cmpObstruction) - hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); - - if (hasObstruction) - { - CFixedVector2D halfSize(obstruction.hw, obstruction.hh); - entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize, true); - - // Compare with previous obstruction - ICmpObstructionManager::ObstructionSquare previousObstruction; - cmpObstruction->GetPreviousObstructionSquare(previousObstruction); - entity_pos_t previousDistance = Geometry::DistanceToSquare(pos - CFixedVector2D(previousObstruction.x, previousObstruction.z), obstruction.u, obstruction.v, halfSize, true); - - // See if we're too close to the target square - bool inside = distance.IsZero() && !Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize).IsZero(); - if ((distance < minRange && previousDistance < minRange) || inside) - return false; - - // See if we're close enough to the target square - if (maxRange < entity_pos_t::Zero() || distance <= maxRange || previousDistance <= maxRange) - return true; - - entity_pos_t circleRadius = halfSize.Length(); - - if (ShouldTreatTargetAsCircle(maxRange, circleRadius)) - { - // The target is small relative to our range, so pretend it's a circle - // and see if we're close enough to that. - // Also check circle around previous position. - entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius; - entity_pos_t previousCircleDistance = (pos - CFixedVector2D(previousObstruction.x, previousObstruction.z)).Length() - circleRadius; - - return circleDistance <= maxRange || previousCircleDistance <= maxRange; - } - - // take minimal clearance required in MoveToTargetRange into account, multiplying by 3/2 for diagonals - entity_pos_t maxDist = std::max(maxRange, (m_Clearance + entity_pos_t::FromInt(TERRAIN_TILE_SIZE)/16)*3/2); - return distance <= maxDist || previousDistance <= maxDist; - } - else - { - CmpPtr cmpTargetPosition(GetSimContext(), target); - if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) - return false; + return true; // what's a sane default here? - CFixedVector2D targetPos = cmpTargetPosition->GetPreviousPosition2D(); - entity_pos_t distance = (pos - targetPos).Length(); + CFixedVector2D pos = cmpPosition->GetPosition2D(); - return minRange <= distance && (maxRange < entity_pos_t::Zero() || distance <= maxRange); - } + return cmpObstructionManager->IsInPointRange(pos.X, pos.Y, x, z, minRange, maxRange + m_Clearance); } -void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) +bool CCmpUnitMotion::IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { - CmpPtr cmpPosition(GetSimContext(), target); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition) + return false; - CFixedVector2D pos = cmpPosition->GetPosition2D(); + CmpPtr cmpObstructionManager(GetSystemEntity()); + if (!cmpObstructionManager) + return true; // what's a sane default here? - PathGoal goal; - goal.type = PathGoal::POINT; - goal.x = pos.X; - goal.z = pos.Y; - - m_State = STATE_FORMATIONMEMBER_PATH; - m_TargetEntity = target; - m_TargetOffset = CFixedVector2D(x, z); - m_TargetMinRange = entity_pos_t::Zero(); - m_TargetMaxRange = entity_pos_t::Zero(); - m_FinalGoal = goal; - m_Tries = 0; + CFixedVector2D pos = cmpPosition->GetPosition2D(); - BeginPathing(pos, goal); + return cmpObstructionManager->IsInTargetRange(pos.X, pos.Y, target, minRange, maxRange + m_Clearance); } - - - void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color) { bool floating = false; @@ -1800,8 +1439,7 @@ if (!m_DebugOverlayEnabled) return; - RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH); - RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH); + RenderPath(m_Path, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH); for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i) collector.Submit(&m_DebugOverlayLongPathLines[i]); Index: source/simulation2/components/CCmpVisualActor.cpp =================================================================== --- source/simulation2/components/CCmpVisualActor.cpp +++ source/simulation2/components/CCmpVisualActor.cpp @@ -54,7 +54,6 @@ public: static void ClassInit(CComponentManager& componentManager) { - componentManager.SubscribeToMessageType(MT_Update_Final); componentManager.SubscribeToMessageType(MT_InterpolatedPositionChanged); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_ValueModification); @@ -71,10 +70,7 @@ fixed m_R, m_G, m_B; // shading color - std::map m_AnimOverride; - // Current animation state - fixed m_AnimRunThreshold; // if non-zero this is the special walk/run mode std::string m_AnimName; bool m_AnimOnce; fixed m_AnimSpeed; @@ -83,6 +79,9 @@ fixed m_AnimSyncRepeatTime; // 0.0 if not synced fixed m_AnimSyncOffsetTime; + std::string m_MovingPrefix; + fixed m_MovingSpeed; + std::map m_VariantSelections; u32 m_Seed; // seed used for random variations @@ -225,9 +224,6 @@ serialize.NumberFixed_Unbounded("g", m_G); serialize.NumberFixed_Unbounded("b", m_B); - SerializeMap()(serialize, "anim overrides", m_AnimOverride); - - serialize.NumberFixed_Unbounded("anim run threshold", m_AnimRunThreshold); serialize.StringASCII("anim name", m_AnimName, 0, 256); serialize.Bool("anim once", m_AnimOnce); serialize.NumberFixed_Unbounded("anim speed", m_AnimSpeed); @@ -282,12 +278,6 @@ { switch (msg.GetType()) { - case MT_Update_Final: - { - const CMessageUpdate_Final& msgData = static_cast (msg); - Update(msgData.turnLength); - break; - } case MT_OwnershipChanged: { if (!m_Unit) @@ -423,7 +413,6 @@ virtual void SelectAnimation(const std::string& name, bool once, fixed speed, const std::wstring& soundgroup) { - m_AnimRunThreshold = fixed::Zero(); m_AnimName = name; m_AnimOnce = once; m_AnimSpeed = speed; @@ -432,28 +421,36 @@ m_AnimSyncRepeatTime = fixed::Zero(); m_AnimSyncOffsetTime = fixed::Zero(); - SetVariant("animation", m_AnimName); + // TODO: change this once we support walk/run-anims + std::string animName = name; + /*if (!m_MovingPrefix.empty() && m_AnimName != "idle") + animName = m_MovingPrefix + "-" + m_AnimName; + else */if (!m_MovingPrefix.empty()) + animName = m_MovingPrefix; - if (m_Unit && m_Unit->GetAnimation()) - m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str()); - } + SetVariant("animation", animName); - virtual void ReplaceMoveAnimation(const std::string& name, const std::string& replace) - { - m_AnimOverride[name] = replace; + if (m_Unit && m_Unit->GetAnimation()) + m_Unit->GetAnimation()->SetAnimationState(animName, m_AnimOnce, m_MovingSpeed.Multiply(m_AnimSpeed).ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str()); } - virtual void ResetMoveAnimation(const std::string& name) + virtual void SetMovingSpeed(fixed movingSpeed) { - std::map::const_iterator it = m_AnimOverride.find(name); - if (it != m_AnimOverride.end()) - m_AnimOverride.erase(name); - } + // TODO: don't copy strings for fun. + std::string prefix; + if (movingSpeed.IsZero()) + prefix = ""; + else + { + CmpPtr cmpUnitMotion(GetEntityHandle()); + if (!cmpUnitMotion) + return; + prefix = cmpUnitMotion->GetSpeedRatio() <= fixed::FromInt(1) ? "walk" : "run"; + } + m_MovingPrefix = prefix; + m_MovingSpeed = movingSpeed.IsZero() ? fixed::FromInt(1) : movingSpeed; - virtual void SelectMovementAnimation(fixed runThreshold) - { - SelectAnimation("walk", false, fixed::FromFloat(1.f), L""); - m_AnimRunThreshold = runThreshold; + SelectAnimation(m_AnimName, m_AnimOnce, m_AnimSpeed, m_SoundGroup); } virtual void SetAnimationSyncRepeat(fixed repeattime) @@ -534,8 +531,6 @@ // ReloadUnitAnimation is used for a minimal reloading upon deserialization, when the actor and seed are identical. // It is also used by ReloadActor. void ReloadUnitAnimation(); - - void Update(fixed turnLength); }; REGISTER_COMPONENT_TYPE(VisualActor) @@ -741,45 +736,3 @@ m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat()); } -void CCmpVisualActor::Update(fixed UNUSED(turnLength)) -{ - // This function is currently only used to update the animation if the speed in - // CCmpUnitMotion changes. This also only happens in the "special movement mode" - // triggered by SelectMovementAnimation. - - // TODO: This should become event based, in order to save performance and to make the code - // far less hacky. We should also take into account the speed when the animation is different - // from the "special movement mode" walking animation. - - // If we're not in the special movement mode, nothing to do. - if (m_AnimRunThreshold.IsZero()) - return; - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - - CmpPtr cmpUnitMotion(GetEntityHandle()); - if (!cmpUnitMotion) - return; - - fixed speed = cmpUnitMotion->GetCurrentSpeed(); - std::string name; - - if (speed.IsZero()) - { - speed = fixed::FromFloat(1.f); - name = "idle"; - } - else - name = speed < m_AnimRunThreshold ? "walk" : "run"; - - std::map::const_iterator it = m_AnimOverride.find(name); - if (it != m_AnimOverride.end()) - name = it->second; - - // Selecting the animation is going to reset the anim run threshold, so save it - fixed runThreshold = m_AnimRunThreshold; - SelectAnimation(name, false, speed, L""); - m_AnimRunThreshold = runThreshold; -} Index: source/simulation2/components/ICmpObstructionManager.h =================================================================== --- source/simulation2/components/ICmpObstructionManager.h +++ source/simulation2/components/ICmpObstructionManager.h @@ -159,6 +159,16 @@ virtual void RemoveShape(tag_t tag) = 0; /** + * Check if the given point is in range of the other point given those parameters + */ + virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) = 0; + + /** + * Check if the given point is in range of the target given those parameters + */ + virtual bool IsInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; + + /** * Collision test a flat-ended thick line against the current set of shapes. * The line caps extend by @p r beyond the end points. * Only intersections going from outside to inside a shape are counted. Index: source/simulation2/components/ICmpUnitMotion.h =================================================================== --- source/simulation2/components/ICmpUnitMotion.h +++ source/simulation2/components/ICmpUnitMotion.h @@ -47,14 +47,12 @@ virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0; /** - * Determine wether the givven point is within the given range, using the same measurement - * as MoveToPointRange. + * Wrapper around ObstructionManager::IsInPointRange with unit position */ virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0; /** - * Determine whether the target is within the given range, using the same measurement - * as MoveToTargetRange. + * Wrapper around ObstructionManager::IsInTargetRange with unit position */ virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; @@ -71,45 +69,70 @@ virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; /** - * Join a formation, and move towards a given offset relative to the formation controller entity. - * Continues following the formation until given a different command. + * Turn to look towards the given point. */ - virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0; + virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; /** - * Turn to look towards the given point. + * Determine whether to abort or retry X times if pathing fails. + * Generally safer to let it abort and inform us. */ - virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; + virtual void SetAbortIfStuck(u8 shouldAbort) = 0; + + /** + * Stop moving immediately, don't send messages. + * This should be used if you are going to ask for a new path, + * in the same function, for example. + * In doubt, UnitAI should probably call this. + * Use with caution. + */ + virtual void DiscardMove() = 0; /** - * Stop moving immediately. + * Stop moving immediately, send messages. + * In doubt, components that are not UnitIA should probably call this. */ - virtual void StopMoving() = 0; + virtual void CompleteMove() = 0; /** - * Get the current movement speed. + * Get how much faster/slower we are at than normal. */ - virtual fixed GetCurrentSpeed() = 0; + virtual fixed GetSpeedRatio() = 0; + + /** + * Get how much faster than our regular speed we can go. + */ + virtual fixed GetTopSpeedRatio() = 0; /** * Set the current movement speed. + * 'speed' in % of top speed (ie 3.0 will be 3 times top speed). */ virtual void SetSpeed(fixed speed) = 0; /** - * Get whether the unit is moving. + * Get whether the unit is actually moving on the map this turn. + */ + virtual bool IsActuallyMoving() = 0; + + /** + * Get whether a unit is trying to go somewhere + * NB: this does not mean its position is actually changing right now. */ - virtual bool IsMoving() = 0; + virtual bool IsTryingToMove() = 0; /** - * Get the default speed that this unit will have when walking, in metres per second. + * Get the unit theoretical speed in metres per second. + * GetActualSpeed will return historical speed + * This is affected by SetSpeed. */ - virtual fixed GetWalkSpeed() = 0; + virtual fixed GetSpeed() = 0; /** - * Get the default speed that this unit will have when running, in metres per second. + * Get the unit template speed in metres per second. + * This is NOT affected by SetSpeed. */ - virtual fixed GetRunSpeed() = 0; + virtual fixed GetTemplateSpeed() = 0; /** * Set whether the unit will turn to face the target point after finishing moving. Index: source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- source/simulation2/components/ICmpUnitMotion.cpp +++ source/simulation2/components/ICmpUnitMotion.cpp @@ -27,14 +27,16 @@ DEFINE_INTERFACE_METHOD_4("IsInPointRange", bool, ICmpUnitMotion, IsInPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_3("IsInTargetRange", bool, ICmpUnitMotion, IsInTargetRange, entity_id_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_3("MoveToTargetRange", bool, ICmpUnitMotion, MoveToTargetRange, entity_id_t, entity_pos_t, entity_pos_t) -DEFINE_INTERFACE_METHOD_3("MoveToFormationOffset", void, ICmpUnitMotion, MoveToFormationOffset, entity_id_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_2("FaceTowardsPoint", void, ICmpUnitMotion, FaceTowardsPoint, entity_pos_t, entity_pos_t) -DEFINE_INTERFACE_METHOD_0("StopMoving", void, ICmpUnitMotion, StopMoving) -DEFINE_INTERFACE_METHOD_0("GetCurrentSpeed", fixed, ICmpUnitMotion, GetCurrentSpeed) +DEFINE_INTERFACE_METHOD_1("SetAbortIfStuck", void, ICmpUnitMotion, SetAbortIfStuck, u8) +DEFINE_INTERFACE_METHOD_0("DiscardMove", void, ICmpUnitMotion, DiscardMove) +DEFINE_INTERFACE_METHOD_0("CompleteMove", void, ICmpUnitMotion, CompleteMove) +DEFINE_INTERFACE_METHOD_0("GetTopSpeedRatio", fixed, ICmpUnitMotion, GetTopSpeedRatio) DEFINE_INTERFACE_METHOD_1("SetSpeed", void, ICmpUnitMotion, SetSpeed, fixed) -DEFINE_INTERFACE_METHOD_0("IsMoving", bool, ICmpUnitMotion, IsMoving) -DEFINE_INTERFACE_METHOD_0("GetWalkSpeed", fixed, ICmpUnitMotion, GetWalkSpeed) -DEFINE_INTERFACE_METHOD_0("GetRunSpeed", fixed, ICmpUnitMotion, GetRunSpeed) +DEFINE_INTERFACE_METHOD_0("IsActuallyMoving", bool, ICmpUnitMotion, IsActuallyMoving) +DEFINE_INTERFACE_METHOD_0("IsTryingToMove", bool, ICmpUnitMotion, IsTryingToMove) +DEFINE_INTERFACE_METHOD_0("GetSpeed", fixed, ICmpUnitMotion, GetSpeed) +DEFINE_INTERFACE_METHOD_0("GetTemplateSpeed", fixed, ICmpUnitMotion, GetTemplateSpeed) DEFINE_INTERFACE_METHOD_0("GetPassabilityClassName", std::string, ICmpUnitMotion, GetPassabilityClassName) DEFINE_INTERFACE_METHOD_0("GetUnitClearance", entity_pos_t, ICmpUnitMotion, GetUnitClearance) DEFINE_INTERFACE_METHOD_1("SetFacePointAfterMove", void, ICmpUnitMotion, SetFacePointAfterMove, bool) @@ -66,24 +68,29 @@ return m_Script.Call("MoveToTargetRange", target, minRange, maxRange); } - virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) + virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) { - m_Script.CallVoid("MoveToFormationOffset", target, x, z); + m_Script.CallVoid("FaceTowardsPoint", x, z); } - virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) + virtual void DiscardMove() { - m_Script.CallVoid("FaceTowardsPoint", x, z); + m_Script.CallVoid("DiscardMove"); } - virtual void StopMoving() + virtual void CompleteMove() { - m_Script.CallVoid("StopMoving"); + m_Script.CallVoid("CompleteMove"); } - virtual fixed GetCurrentSpeed() + virtual void SetAbortIfStuck(u8 shouldAbort) { - return m_Script.Call("GetCurrentSpeed"); + m_Script.CallVoid("SetAbortIfStuck", shouldAbort); + } + + virtual fixed GetActualSpeed() + { + return m_Script.Call("GetActualSpeed"); } virtual void SetSpeed(fixed speed) @@ -91,19 +98,29 @@ m_Script.CallVoid("SetSpeed", speed); } - virtual bool IsMoving() + virtual fixed GetTopSpeedRatio() + { + return m_Script.Call("GetTopSpeedRatio"); + } + + virtual bool IsActuallyMoving() + { + return m_Script.Call("IsActuallyMoving"); + } + + virtual bool IsTryingToMove() { - return m_Script.Call("IsMoving"); + return m_Script.Call("IsTryingToMove"); } - virtual fixed GetWalkSpeed() + virtual fixed GetSpeed() { - return m_Script.Call("GetWalkSpeed"); + return m_Script.Call("GetSpeed"); } - virtual fixed GetRunSpeed() + virtual fixed GetTemplateSpeed() { - return m_Script.Call("GetRunSpeed"); + return m_Script.Call("GetTemplateSpeed"); } virtual void SetFacePointAfterMove(bool facePointAfterMove) @@ -116,6 +133,11 @@ return m_Script.Call("GetPassabilityClass"); } + virtual fixed GetSpeedRatio() + { + return fixed::FromInt(1); + } + virtual std::string GetPassabilityClassName() { return m_Script.Call("GetPassabilityClassName"); Index: source/simulation2/components/ICmpVisual.h =================================================================== --- source/simulation2/components/ICmpVisual.h +++ source/simulation2/components/ICmpVisual.h @@ -99,25 +99,10 @@ virtual void SelectAnimation(const std::string& name, bool once, fixed speed, const std::wstring& soundgroup) = 0; /** - * Replaces a specified animation with another. Only affects the special speed-based - * animation determination behaviour. - * @param name Animation to match. - * @param replace Animation that should replace the matched animation. + * Tell the visual actor that the unit is currently moving at the given speed. + * If speed is 0, the unit will become idle. */ - virtual void ReplaceMoveAnimation(const std::string& name, const std::string& replace) = 0; - - /** - * Ensures that the given animation will be used when it normally would be, - * removing reference to any animation that might replace it. - * @param name Animation name to remove from the replacement map. - */ - virtual void ResetMoveAnimation(const std::string& name) = 0; - - /** - * Start playing the walk/run animations, scaled to the unit's movement speed. - * @param runThreshold movement speed at which to switch to the run animation - */ - virtual void SelectMovementAnimation(fixed runThreshold) = 0; + virtual void SetMovingSpeed(fixed movingSpeed) = 0; /** * Adjust the speed of the current animation, so it can match simulation events. Index: source/simulation2/components/ICmpVisual.cpp =================================================================== --- source/simulation2/components/ICmpVisual.cpp +++ source/simulation2/components/ICmpVisual.cpp @@ -24,9 +24,6 @@ BEGIN_INTERFACE_WRAPPER(Visual) DEFINE_INTERFACE_METHOD_2("SetVariant", void, ICmpVisual, SetVariant, CStr, CStr) DEFINE_INTERFACE_METHOD_4("SelectAnimation", void, ICmpVisual, SelectAnimation, std::string, bool, fixed, std::wstring) -DEFINE_INTERFACE_METHOD_1("SelectMovementAnimation", void, ICmpVisual, SelectMovementAnimation, fixed) -DEFINE_INTERFACE_METHOD_1("ResetMoveAnimation", void, ICmpVisual, ResetMoveAnimation, std::string) -DEFINE_INTERFACE_METHOD_2("ReplaceMoveAnimation", void, ICmpVisual, ReplaceMoveAnimation, std::string, std::string) DEFINE_INTERFACE_METHOD_1("SetAnimationSyncRepeat", void, ICmpVisual, SetAnimationSyncRepeat, fixed) DEFINE_INTERFACE_METHOD_1("SetAnimationSyncOffset", void, ICmpVisual, SetAnimationSyncOffset, fixed) DEFINE_INTERFACE_METHOD_4("SetShadingColor", void, ICmpVisual, SetShadingColor, fixed, fixed, fixed, fixed) Index: source/simulation2/helpers/Geometry.h =================================================================== --- source/simulation2/helpers/Geometry.h +++ source/simulation2/helpers/Geometry.h @@ -30,6 +30,21 @@ namespace Geometry { +/* + * Check if we should treat a square as a circle, given the radius + * of the resulting circle and a distance to it + * used by UnitMotion and ObstructionManager + */ +inline bool ShouldTreatTargetAsCircle(const fixed& range, const fixed& circleRadius) +{ + // 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); +} + /** * Checks if a point is inside the given rotated rectangle. * Points precisely on an edge are considered to be inside. Index: source/simulation2/scripting/MessageTypeConversions.cpp =================================================================== --- source/simulation2/scripting/MessageTypeConversions.cpp +++ source/simulation2/scripting/MessageTypeConversions.cpp @@ -267,20 +267,46 @@ //////////////////////////////// -JS::Value CMessageMotionChanged::ToJSVal(ScriptInterface& scriptInterface) const +JS::Value CMessageBeginMove::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); - SET_MSG_PROPERTY(starting); - SET_MSG_PROPERTY(error); return JS::ObjectValue(*obj); } -CMessage* CMessageMotionChanged::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +CMessage* CMessageBeginMove::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) { FROMJSVAL_SETUP(); - GET_MSG_PROPERTY(bool, starting); - GET_MSG_PROPERTY(bool, error); - return new CMessageMotionChanged(starting, error); + return new CMessageBeginMove(); +} + +//////////////////////////////// + +JS::Value CMessagePausedMove::ToJSVal(ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + return JS::ObjectValue(*obj); +} + +CMessage* CMessagePausedMove::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + return new CMessagePausedMove(); +} + +//////////////////////////////// + +JS::Value CMessageFinishedMove::ToJSVal(ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + SET_MSG_PROPERTY(failed); + return JS::ObjectValue(*obj); +} + +CMessage* CMessageFinishedMove::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + GET_MSG_PROPERTY(bool, failed); + return new CMessageFinishedMove(failed); } //////////////////////////////// Index: source/tools/atlas/GameInterface/ActorViewer.cpp =================================================================== --- source/tools/atlas/GameInterface/ActorViewer.cpp +++ source/tools/atlas/GameInterface/ActorViewer.cpp @@ -375,7 +375,7 @@ { CmpPtr cmpUnitMotion(m.Simulation2, m.Entity); if (cmpUnitMotion) - speed = cmpUnitMotion->GetWalkSpeed().ToFloat(); + speed = cmpUnitMotion->GetTemplateSpeed().ToFloat(); else speed = 7.f; // typical unit speed @@ -385,7 +385,7 @@ { CmpPtr cmpUnitMotion(m.Simulation2, m.Entity); if (cmpUnitMotion) - speed = cmpUnitMotion->GetRunSpeed().ToFloat(); + speed = cmpUnitMotion->GetTemplateSpeed().ToFloat(); else speed = 12.f; // typical unit speed