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 @@ -866,29 +866,13 @@ */ Formation.prototype.GetEstimatedOrientation = function(pos) { - let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); - let r = { "sin": 0, "cos": 1 }; - let unitAIState = cmpUnitAI.GetCurrentState(); - if (unitAIState == "FORMATIONCONTROLLER.WALKING" || unitAIState == "FORMATIONCONTROLLER.COMBAT.APPROACHING") - { - let targetPos = cmpUnitAI.GetTargetPositions(); - if (!targetPos.length) - return r; - let d = targetPos[0].sub(pos).normalize(); - if (!d.x && !d.y) - return r; - r.cos = d.y; - r.sin = d.x; - } - else - { - let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); - if (!cmpPosition) - return r; - let rot = cmpPosition.GetRotation().y; - r.sin = Math.sin(rot); - r.cos = Math.cos(rot); - } + let r = {}; + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition) + return r; + let rot = cmpPosition.GetRotation().y; + r.sin = Math.sin(rot); + r.cos = Math.cos(rot); return r; }; 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 @@ -952,6 +952,9 @@ this.FinishOrder(); return true; } + + this.ToggleTurnTime(); + let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); @@ -975,6 +978,9 @@ this.FinishOrder(); return true; } + + this.ToggleTurnTime(); + this.StartTimer(0, 1000); let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); @@ -1021,6 +1027,8 @@ } this.StartTimer(0, 1000); + this.ToggleTurnTime(); + let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true, "combat"); @@ -1593,6 +1601,7 @@ this.FinishOrder(); return true; } + this.ToggleTurnTime(); return false; }, @@ -1616,6 +1625,7 @@ this.FinishOrder(); return true; } + this.ToggleTurnTime(); // Show weapons rather than carried resources. this.SetAnimationVariant("combat"); @@ -1653,6 +1663,8 @@ return true; } + this.ToggleTurnTime(); + if (!this.patrolStartPosOrder) { this.patrolStartPosOrder = cmpPosition.GetPosition(); @@ -6096,6 +6108,12 @@ cmpUnitMotion.SetSpeedMultiplier(speed); }; +UnitAI.prototype.ToggleTurnTime = function() +{ + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + if (cmpUnitMotion) + cmpUnitMotion.ToggleTurnTime(); +}; /** * Try to match the targets current movement speed. * 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 @@ -327,6 +327,11 @@ // Ignore this - angle is controlled by the target-seeking code instead. }; +UnitMotionFlying.prototype.ToggleTurnTime = function() +{ + // Ignore this - unitMotionFlying has his own turning algorithm. +}; + UnitMotionFlying.prototype.StopMoving = function() { // Invert. Index: binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -176,6 +176,7 @@ "StopMoving": () => {}, "SetFacePointAfterMove": () => {}, "GetFacePointAfterMove": () => true, + "ToggleTurnTime": () => {}, "GetPassabilityClassName": () => "default" }); @@ -245,7 +246,8 @@ "MoveToPointRange": () => true, "SetFacePointAfterMove": () => {}, "GetFacePointAfterMove": () => true, - "GetPassabilityClassName": () => "default" + "GetPassabilityClassName": () => "default", + "ToggleTurnTime": () => {} }); controllerAI.OnCreate(); Index: binaries/data/mods/public/simulation/templates/template_formation.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_formation.xml +++ binaries/data/mods/public/simulation/templates/template_formation.xml @@ -53,8 +53,8 @@ 0 upright false - 0.0 - 3.0 + 0 + 3 0.75 Index: source/simulation2/components/CCmpPosition.cpp =================================================================== --- source/simulation2/components/CCmpPosition.cpp +++ source/simulation2/components/CCmpPosition.cpp @@ -129,7 +129,7 @@ "" "" "" - "" + "" "" ""; } @@ -536,6 +536,11 @@ return CFixedVector2D(m_PrevX, m_PrevZ); } + virtual float GetTurnRate() const + { + return m_RotYSpeed; + } + virtual void TurnTo(entity_angle_t y) { if (m_TurretParent != INVALID_ENTITY) Index: source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion.cpp +++ source/simulation2/components/CCmpUnitMotion.cpp @@ -140,6 +140,7 @@ fixed m_WalkSpeed, m_RunMultiplier; bool m_FacePointAfterMove; + bool m_TurnTime; // Number of path computations that failed (in a row). // When this gets above MAX_FAILED_PATH_COMPUTATIONS, inform other components @@ -227,6 +228,7 @@ m_FormationController = paramNode.GetChild("FormationController").ToBool(); m_FacePointAfterMove = true; + m_TurnTime = false; m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); m_SpeedMultiplier = fixed::FromInt(1); @@ -278,6 +280,7 @@ serialize.NumberFixed_Unbounded("current speed", m_CurSpeed); serialize.Bool("facePointAfterMove", m_FacePointAfterMove); + serialize.Bool("turnTime", m_TurnTime); SerializeVector()(serialize, "long path", m_LongPath.m_Waypoints); SerializeVector()(serialize, "short path", m_ShortPath.m_Waypoints); @@ -431,6 +434,11 @@ return m_FacePointAfterMove; } + virtual void ToggleTurnTime() + { + m_TurnTime = true; + } + virtual void SetDebugOverlay(bool enabled) { m_DebugOverlayEnabled = enabled; @@ -616,7 +624,7 @@ * This does not send actually change the position. * @returns true if the move was obstructed. */ - bool PerformMove(fixed dt, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos) const; + bool PerformMove(fixed dt, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle); /** * Update other components on our speed. @@ -876,24 +884,29 @@ // Keep track of the current unit's position during the update CFixedVector2D pos = initialPos; + entity_angle_t initialAngle = cmpPosition->GetRotation().Y; + entity_angle_t angle = initialAngle; // 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 bool wentStraight = TryGoingStraightToTarget(initialPos); - bool wasObstructed = PerformMove(dt, m_ShortPath, m_LongPath, pos); + bool wasObstructed = PerformMove(dt, m_ShortPath, m_LongPath, pos, angle); - // Update our speed over this turn so that the visual actor shows the correct animation. if (pos == initialPos) + { + // Update our speed over this turn so that the visual actor shows the correct animation. UpdateMovementState(fixed::Zero()); + if (angle != initialAngle) + cmpPosition->TurnTo(angle); + } else { // Update the Position component after our movement (if we actually moved anywhere) + // When moving always set the angle in the direction of the movement. CFixedVector2D offset = pos - initialPos; - - // Face towards the target - entity_angle_t angle = atan2_approx(offset.X, offset.Y); - cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle); + angle = atan2_approx(offset.X, offset.Y); + cmpPosition->MoveAndTurnTo(pos.X, pos.Y, angle); // Calculate the mean speed over this past turn. UpdateMovementState(offset.Length() / dt); @@ -942,12 +955,18 @@ return false; } -bool CCmpUnitMotion::PerformMove(fixed dt, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos) const +bool CCmpUnitMotion::PerformMove(fixed dt, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle) { // If there are no waypoint, behave as though we were obstructed and let HandleObstructedMove handle it. if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) return true; + // Wrap the angle to (-Pi, Pi]. + while (angle > entity_angle_t::Pi()) + angle -= entity_angle_t::Pi() * 2; + while (angle < -entity_angle_t::Pi()) + angle += entity_angle_t::Pi() * 2; + // TODO: there's some asymmetry here when units look at other // units' positions - the result will depend on the order of execution. // Maybe we should split the updates into multiple phases to minimise @@ -969,8 +988,6 @@ fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); - // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint - fixed timeLeft = dt; fixed zero = fixed::Zero(); @@ -987,6 +1004,39 @@ target = CFixedVector2D(shortPath.m_Waypoints.back().x, shortPath.m_Waypoints.back().z); CFixedVector2D offset = target - pos; + if (m_TurnTime) + { + CmpPtr cmpPosition(GetEntityHandle()); + fixed turnRate = fixed::FromFloat(cmpPosition->GetTurnRate()); + fixed maxRotation = turnRate.Multiply(timeLeft); + fixed angleDiff = angle - atan2_approx(offset.X, offset.Y); + if (angleDiff != zero) + { + fixed absoluteAngleDiff = angleDiff.Absolute(); + if (absoluteAngleDiff > entity_angle_t::Pi()) + absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff; + + // Figure out whether rotating will increase or decrease the angle, and how far we need to rotate in that direction. + int direction = (entity_angle_t::Zero() < angleDiff && angleDiff <= entity_angle_t::Pi()) || angleDiff < -entity_angle_t::Pi() ? -1 : 1; + + // Can't rotate far enough, just rotate in the correct direction. + if (absoluteAngleDiff > maxRotation) + { + angle += maxRotation * direction; + if (angle * direction > entity_angle_t::Pi()) + angle -= entity_angle_t::Pi() * 2 * direction; + break; + } + // Rotate to the next waypoint. + else + { + m_TurnTime = false; + angle = atan2_approx(offset.X, offset.Y); + timeLeft = (maxRotation - absoluteAngleDiff) / turnRate; + continue; + } + } + } // Work out how far we can travel in timeLeft fixed maxdist = maxSpeed.Multiply(timeLeft); Index: source/simulation2/components/ICmpPosition.h =================================================================== --- source/simulation2/components/ICmpPosition.h +++ source/simulation2/components/ICmpPosition.h @@ -185,6 +185,11 @@ virtual CFixedVector2D GetPreviousPosition2D() const = 0; /** + * Returns the turn rate in radians per second + */ + virtual float GetTurnRate() const = 0; + + /** * Rotate smoothly to the given angle around the upwards axis. * @param y clockwise radians from the +Z axis. */ Index: source/simulation2/components/ICmpPosition.cpp =================================================================== --- source/simulation2/components/ICmpPosition.cpp +++ source/simulation2/components/ICmpPosition.cpp @@ -42,6 +42,7 @@ DEFINE_INTERFACE_METHOD_CONST_0("GetPosition2D", CFixedVector2D, ICmpPosition, GetPosition2D) DEFINE_INTERFACE_METHOD_CONST_0("GetPreviousPosition", CFixedVector3D, ICmpPosition, GetPreviousPosition) DEFINE_INTERFACE_METHOD_CONST_0("GetPreviousPosition2D", CFixedVector2D, ICmpPosition, GetPreviousPosition2D) +DEFINE_INTERFACE_METHOD_CONST_0("GetTurnRate", float, ICmpPosition, GetTurnRate) DEFINE_INTERFACE_METHOD_1("TurnTo", void, ICmpPosition, TurnTo, entity_angle_t) DEFINE_INTERFACE_METHOD_1("SetYRotation", void, ICmpPosition, SetYRotation, entity_angle_t) DEFINE_INTERFACE_METHOD_2("SetXZRotation", void, ICmpPosition, SetXZRotation, entity_angle_t, entity_angle_t) Index: source/simulation2/components/ICmpUnitMotion.h =================================================================== --- source/simulation2/components/ICmpUnitMotion.h +++ source/simulation2/components/ICmpUnitMotion.h @@ -124,6 +124,11 @@ virtual bool GetFacePointAfterMove() const = 0; /** + * Set whether the unit will waste time rotating on the next turn when moving. + */ + virtual void ToggleTurnTime() = 0; + + /** * Get the unit's passability class. */ virtual pass_class_t GetPassabilityClass() const = 0; Index: source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- source/simulation2/components/ICmpUnitMotion.cpp +++ source/simulation2/components/ICmpUnitMotion.cpp @@ -39,6 +39,7 @@ DEFINE_INTERFACE_METHOD_CONST_0("GetUnitClearance", entity_pos_t, ICmpUnitMotion, GetUnitClearance) DEFINE_INTERFACE_METHOD_1("SetFacePointAfterMove", void, ICmpUnitMotion, SetFacePointAfterMove, bool) DEFINE_INTERFACE_METHOD_CONST_0("GetFacePointAfterMove", bool, ICmpUnitMotion, GetFacePointAfterMove) +DEFINE_INTERFACE_METHOD_0("ToggleTurnTime", void, ICmpUnitMotion, ToggleTurnTime) DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpUnitMotion, SetDebugOverlay, bool) END_INTERFACE_WRAPPER(UnitMotion) @@ -122,6 +123,11 @@ return m_Script.Call("GetFacePointAfterMove"); } + virtual void ToggleTurnTime() + { + m_Script.CallVoid("ToggleTurnTime"); + } + virtual pass_class_t GetPassabilityClass() const { return m_Script.Call("GetPassabilityClass"); Index: source/simulation2/components/tests/test_RangeManager.h =================================================================== --- source/simulation2/components/tests/test_RangeManager.h +++ source/simulation2/components/tests/test_RangeManager.h @@ -60,6 +60,7 @@ virtual CFixedVector2D GetPosition2D() const { return CFixedVector2D(); } virtual CFixedVector3D GetPreviousPosition() const { return CFixedVector3D(); } virtual CFixedVector2D GetPreviousPosition2D() const { return CFixedVector2D(); } + virtual float GetTurnRate() const { return 0; } virtual void TurnTo(entity_angle_t UNUSED(y)) { } virtual void SetYRotation(entity_angle_t UNUSED(y)) { } virtual void SetXZRotation(entity_angle_t UNUSED(x), entity_angle_t UNUSED(z)) { }