diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js
index e6e6d65c57..a31bf88ee2 100644
--- a/binaries/data/mods/public/simulation/components/UnitAI.js
+++ b/binaries/data/mods/public/simulation/components/UnitAI.js
@@ -5107,7 +5107,7 @@ UnitAI.prototype.SetFormationController = function(ent)
// Set obstruction group, so we can walk through members
// of our own formation (or ourself if not in formation)
- var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
+ const cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
{
if (ent == INVALID_ENTITY)
@@ -5116,6 +5116,10 @@ UnitAI.prototype.SetFormationController = function(ent)
cmpObstruction.SetControlGroup(ent);
}
+ const cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ if (cmpUnitMotion)
+ cmpUnitMotion.SetMemberOfFormation(ent);
+
// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
if (ent == INVALID_ENTITY)
this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
diff --git a/binaries/data/mods/public/simulation/components/UnitMotionFlying.js b/binaries/data/mods/public/simulation/components/UnitMotionFlying.js
index fa2eb10fcd..277a445f84 100644
--- a/binaries/data/mods/public/simulation/components/UnitMotionFlying.js
+++ b/binaries/data/mods/public/simulation/components/UnitMotionFlying.js
@@ -296,6 +296,11 @@ UnitMotionFlying.prototype.MoveToTargetRange = function(target, minRange, maxRan
return true;
};
+UnitMotionFlying.prototype.SetMemberOfFormation = function()
+{
+ // Ignored.
+};
+
UnitMotionFlying.prototype.GetWalkSpeed = function()
{
return +this.template.MaxSpeed;
diff --git a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
index b587a9d8d5..38480a92ea 100644
--- a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
+++ b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
@@ -170,6 +170,7 @@ function TestFormationExiting(mode)
"GetWalkSpeed": () => 1,
"MoveToFormationOffset": (target, x, z) => {},
"MoveToTargetRange": (target, min, max) => true,
+ "SetMemberOfFormation": () => {},
"StopMoving": () => {},
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
@@ -349,6 +350,7 @@ function TestMoveIntoFormationWhileAttacking()
"GetWalkSpeed": () => 1,
"MoveToFormationOffset": (target, x, z) => {},
"MoveToTargetRange": (target, min, max) => true,
+ "SetMemberOfFormation": () => {},
"StopMoving": () => {},
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
diff --git a/binaries/data/mods/public/simulation/data/pathfinder.rng b/binaries/data/mods/public/simulation/data/pathfinder.rng
index e3bdd2d457..c732022b0a 100644
--- a/binaries/data/mods/public/simulation/data/pathfinder.rng
+++ b/binaries/data/mods/public/simulation/data/pathfinder.rng
@@ -4,8 +4,21 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/binaries/data/mods/public/simulation/data/pathfinder.xml b/binaries/data/mods/public/simulation/data/pathfinder.xml
index c8c1a4c3d9..bb5c3d1146 100644
--- a/binaries/data/mods/public/simulation/data/pathfinder.xml
+++ b/binaries/data/mods/public/simulation/data/pathfinder.xml
@@ -4,11 +4,30 @@
20
-
-
-
-
- 1.6
+
+
+
+
+
+ 1.6
+
+
+
+
+
+ 2
+
+
+
+
+ 2.5
+
+
+
+
+
+ 0.2
+
diff --git a/binaries/data/mods/public/simulation/templates/special/formations/testudo.xml b/binaries/data/mods/public/simulation/templates/special/formations/testudo.xml
index 9f606c007a..4078bb0c6d 100644
--- a/binaries/data/mods/public/simulation/templates/special/formations/testudo.xml
+++ b/binaries/data/mods/public/simulation/templates/special/formations/testudo.xml
@@ -7,7 +7,7 @@
Hero Champion Elite Advanced Basic
Testudo
square
- 0.50
+ 0.5
0.4
fillFromTheSides
0.8
diff --git a/source/simulation2/components/CCmpUnitMotion.h b/source/simulation2/components/CCmpUnitMotion.h
index 072064fd1d..6828004d92 100644
--- a/source/simulation2/components/CCmpUnitMotion.h
+++ b/source/simulation2/components/CCmpUnitMotion.h
@@ -143,7 +143,7 @@ public:
// Template state:
- bool m_FormationController;
+ bool m_IsFormationController;
fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier;
pass_class_t m_PassClass;
@@ -209,6 +209,9 @@ public:
MoveRequest(entity_id_t target, CFixedVector2D offset) : m_Type(OFFSET), m_Entity(target), m_Position(offset) {};
} m_MoveRequest;
+ // If this is not INVALID_ENTITY, the unit is a formation member.
+ entity_id_t m_FormationController = INVALID_ENTITY;
+
// If the entity moves, it will do so at m_WalkSpeed * m_SpeedMultiplier.
fixed m_SpeedMultiplier;
// This caches the resulting speed from m_WalkSpeed * m_SpeedMultiplier for convenience.
@@ -253,7 +256,7 @@ public:
virtual void Init(const CParamNode& paramNode)
{
- m_FormationController = paramNode.GetChild("FormationController").ToBool();
+ m_IsFormationController = paramNode.GetChild("FormationController").ToBool();
m_FacePointAfterMove = true;
@@ -307,6 +310,8 @@ public:
serialize.NumberFixed_Unbounded("target min range", m_MoveRequest.m_MinRange);
serialize.NumberFixed_Unbounded("target max range", m_MoveRequest.m_MaxRange);
+ serialize.NumberU32_Unbounded("formation controller", m_FormationController);
+
serialize.NumberFixed_Unbounded("speed multiplier", m_SpeedMultiplier);
serialize.NumberFixed_Unbounded("current speed", m_CurSpeed);
@@ -358,7 +363,7 @@ public:
case MT_Create:
{
if (!ENTITY_IS_LOCAL(GetEntityId()))
- CmpPtr(GetSystemEntity())->Register(this, GetEntityId(), m_FormationController);
+ CmpPtr(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController);
break;
}
case MT_Destroy:
@@ -390,7 +395,7 @@ public:
{
OnValueModification();
if (!ENTITY_IS_LOCAL(GetEntityId()))
- CmpPtr(GetSystemEntity())->Register(this, GetEntityId(), m_FormationController);
+ CmpPtr(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController);
break;
}
}
@@ -501,9 +506,16 @@ public:
return MoveTo(MoveRequest(target, minRange, maxRange));
}
- virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z)
+ virtual void MoveToFormationOffset(entity_id_t controller, entity_pos_t x, entity_pos_t z)
{
- MoveTo(MoveRequest(target, CFixedVector2D(x, z)));
+ SetMemberOfFormation(controller);
+ // Pass the controller to the move request anyways.
+ MoveTo(MoveRequest(controller, CFixedVector2D(x, z)));
+ }
+
+ virtual void SetMemberOfFormation(entity_id_t controller)
+ {
+ m_FormationController = controller;
}
virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
@@ -542,19 +554,18 @@ public:
private:
bool IsFormationMember() const
{
- // TODO: this really shouldn't be what we are checking for.
- return m_MoveRequest.m_Type == MoveRequest::OFFSET;
+ return m_FormationController != INVALID_ENTITY;
}
bool IsFormationControllerMoving() const
{
- CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity);
+ CmpPtr cmpControllerMotion(GetSimContext(), m_FormationController);
return cmpControllerMotion && cmpControllerMotion->IsMoveRequested();
}
entity_id_t GetGroup() const
{
- return IsFormationMember() ? m_MoveRequest.m_Entity : GetEntityId();
+ return IsFormationMember() ? m_FormationController : GetEntityId();
}
void SetParticipateInPushing(bool pushing)
@@ -975,7 +986,7 @@ void CCmpUnitMotion::PreMove(CCmpUnitMotionManager::MotionState& state)
if (!m_BlockMovement)
return;
- state.controlGroup = IsFormationMember() ? m_MoveRequest.m_Entity : INVALID_ENTITY;
+ state.controlGroup = IsFormationMember() ? m_FormationController : INVALID_ENTITY;
// Update moving flag, this is an internal construct used for pushing,
// so it does not really reflect whether the unit is actually moving or not.
diff --git a/source/simulation2/components/CCmpUnitMotionManager.h b/source/simulation2/components/CCmpUnitMotionManager.h
index 98fb51f65a..8641e66cd6 100644
--- a/source/simulation2/components/CCmpUnitMotionManager.h
+++ b/source/simulation2/components/CCmpUnitMotionManager.h
@@ -81,9 +81,15 @@ public:
bool isMoving = false;
};
- // Multiplier for the pushing radius. Pre-multiplied by the circle-square correction factor.
// "Template" state, not serialized (cannot be changed mid-game).
+
+ // Multiplier for the pushing radius. Pre-multiplied by the circle-square correction factor.
entity_pos_t m_PushingRadius;
+ // Additive modifiers to the pushing radius for moving units and idle units respectively.
+ entity_pos_t m_MovingPushExtension;
+ entity_pos_t m_StaticPushExtension;
+ // Pushing forces below this value are ignored - this prevents units moving forever by very small increments.
+ entity_pos_t m_MinimalPushing;
// These vectors are reconstructed on deserialization.
diff --git a/source/simulation2/components/CCmpUnitMotion_System.cpp b/source/simulation2/components/CCmpUnitMotion_System.cpp
index f0fe0fac8a..e12698c402 100644
--- a/source/simulation2/components/CCmpUnitMotion_System.cpp
+++ b/source/simulation2/components/CCmpUnitMotion_System.cpp
@@ -38,11 +38,6 @@ namespace {
*/
static const int PUSHING_GRID_SIZE = 20;
- /**
- * Pushing is ignored if the combined push force has lower magnitude than this.
- */
- static const entity_pos_t MINIMAL_PUSHING = entity_pos_t::FromInt(3) / 10;
-
/**
* For pushing, treat the clearances as a circle - they're defined as squares,
* so we'll take the circumscribing square (approximately).
@@ -50,15 +45,15 @@ namespace {
*/
static const entity_pos_t PUSHING_CORRECTION = entity_pos_t::FromInt(5) / 7;
- /**
- * When moving, units exert a pushing influence at a greater distance.
- */
- static const entity_pos_t PUSHING_MOVING_INFLUENCE_EXTENSION = entity_pos_t::FromInt(1);
-
/**
* Arbitrary constant used to reduce pushing to levels that won't break physics for our turn length.
*/
static const int PUSHING_REDUCTION_FACTOR = 2;
+
+ /**
+ * Maximum distance multiplier.
+ */
+ static const entity_pos_t MAX_DISTANCE_FACTOR = entity_pos_t::FromInt(2);
}
CCmpUnitMotionManager::MotionState::MotionState(CmpPtr cmpPos, CCmpUnitMotion* cmpMotion)
@@ -73,7 +68,12 @@ void CCmpUnitMotionManager::Init(const CParamNode&)
// TODO: there seems to be no real reason why we could not register a 'system' entity somewhere instead.
CParamNode externalParamNode;
CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml", "pathfinder");
- const CParamNode radius = externalParamNode.GetChild("Pathfinder").GetChild("PushingRadius");
+ CParamNode pushingNode = externalParamNode.GetChild("Pathfinder").GetChild("Pushing");
+
+ // NB: all values are given sane default, but they are not treated as optional in the schema,
+ // so the XML file is the reference.
+
+ const CParamNode radius = pushingNode.GetChild("Radius");
if (radius.IsOk())
{
m_PushingRadius = radius.ToFixed();
@@ -86,7 +86,25 @@ void CCmpUnitMotionManager::Init(const CParamNode&)
}
else
m_PushingRadius = entity_pos_t::FromInt(8) / 5;
- m_PushingRadius = m_PushingRadius.Multiply(PUSHING_CORRECTION);
+
+ const CParamNode minForce = pushingNode.GetChild("MinimalForce");
+ if (minForce.IsOk())
+ m_MinimalPushing = minForce.ToFixed();
+ else
+ m_MinimalPushing = entity_pos_t::FromInt(2) / 10;
+
+ const CParamNode movingExt = pushingNode.GetChild("MovingExtension");
+ const CParamNode staticExt = pushingNode.GetChild("StaticExtension");
+ if (movingExt.IsOk() && staticExt.IsOk())
+ {
+ m_MovingPushExtension = movingExt.ToFixed();
+ m_StaticPushExtension = staticExt.ToFixed();
+ }
+ else
+ {
+ m_MovingPushExtension = entity_pos_t::FromInt(5) / 2;
+ m_StaticPushExtension = entity_pos_t::FromInt(2);
+ }
}
void CCmpUnitMotionManager::Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController)
@@ -212,7 +230,7 @@ void CCmpUnitMotionManager::Move(EntityMap& ents, fixed dt)
it->second.push = CFixedVector2D();
}
// Only apply pushing if the effect is significant enough.
- if (it->second.push.CompareLength(MINIMAL_PUSHING) > 0)
+ if (it->second.push.CompareLength(m_MinimalPushing) > 0)
{
// If there was an attempt at movement, and the pushed movement is in a sufficiently different direction
// (measured by an extremely arbitrary dot product)
@@ -255,16 +273,17 @@ void CCmpUnitMotionManager::Push(EntityMap::value_type& a, EntityMa
// Exception: units in the same control group (i.e. the same formation) never push farther than themselves
// and are also allowed to push idle units (obstructions are ignored within formations,
// so pushing idle units makes one member crossing the formation look better).
- if (a.second.controlGroup != INVALID_ENTITY && a.second.controlGroup == b.second.controlGroup)
+ bool sameControlGroup = a.second.controlGroup != INVALID_ENTITY && a.second.controlGroup == b.second.controlGroup;
+ if (sameControlGroup)
movingPush = 0;
if (movingPush == 1)
return;
- entity_pos_t combinedClearance = (a.second.cmpUnitMotion->m_Clearance + b.second.cmpUnitMotion->m_Clearance).Multiply(m_PushingRadius);
+ entity_pos_t combinedClearance = (a.second.cmpUnitMotion->m_Clearance + b.second.cmpUnitMotion->m_Clearance).Multiply(PUSHING_CORRECTION);
entity_pos_t maxDist = combinedClearance;
- if (movingPush)
- maxDist += PUSHING_MOVING_INFLUENCE_EXTENSION;
+ if (!sameControlGroup)
+ maxDist = combinedClearance.Multiply(m_PushingRadius) + (movingPush ? m_MovingPushExtension : m_StaticPushExtension);
CFixedVector2D offset = a.second.pos - b.second.pos;
if (offset.CompareLength(maxDist) > 0)
@@ -301,11 +320,12 @@ void CCmpUnitMotionManager::Push(EntityMap::value_type& a, EntityMa
offsetLength = fixed::Zero();
}
-
-
// The formula expects 'normal' pushing if the two entities edges are touching.
- entity_pos_t distanceFactor = movingPush ? (maxDist - offsetLength) / (maxDist - combinedClearance) : combinedClearance - offsetLength + entity_pos_t::FromInt(1);
- distanceFactor = Clamp(distanceFactor, entity_pos_t::Zero(), entity_pos_t::FromInt(2));
+ entity_pos_t distanceFactor = maxDist - combinedClearance;
+ if (distanceFactor <= entity_pos_t::Zero())
+ distanceFactor = MAX_DISTANCE_FACTOR;
+ else
+ distanceFactor = Clamp((maxDist - offsetLength) / distanceFactor, entity_pos_t::Zero(), MAX_DISTANCE_FACTOR);
// Mark both as needing an update so they actually get moved.
a.second.needUpdate = true;
@@ -314,6 +334,6 @@ void CCmpUnitMotionManager::Push(EntityMap::value_type& a, EntityMa
CFixedVector2D pushingDir = offset.Multiply(distanceFactor);
// Divide by an arbitrary constant to avoid pushing too much.
- a.second.push += pushingDir.Multiply(movingPush ? dt : dt / PUSHING_REDUCTION_FACTOR);
- b.second.push -= pushingDir.Multiply(movingPush ? dt : dt / PUSHING_REDUCTION_FACTOR);
+ a.second.push += pushingDir.Multiply(dt / PUSHING_REDUCTION_FACTOR);
+ b.second.push -= pushingDir.Multiply(dt / PUSHING_REDUCTION_FACTOR);
}
diff --git a/source/simulation2/components/ICmpUnitMotion.cpp b/source/simulation2/components/ICmpUnitMotion.cpp
index 09068e7107..589d678808 100644
--- a/source/simulation2/components/ICmpUnitMotion.cpp
+++ b/source/simulation2/components/ICmpUnitMotion.cpp
@@ -26,6 +26,7 @@ BEGIN_INTERFACE_WRAPPER(UnitMotion)
DEFINE_INTERFACE_METHOD("MoveToPointRange", ICmpUnitMotion, MoveToPointRange)
DEFINE_INTERFACE_METHOD("MoveToTargetRange", ICmpUnitMotion, MoveToTargetRange)
DEFINE_INTERFACE_METHOD("MoveToFormationOffset", ICmpUnitMotion, MoveToFormationOffset)
+DEFINE_INTERFACE_METHOD("SetMemberOfFormation", ICmpUnitMotion, SetMemberOfFormation)
DEFINE_INTERFACE_METHOD("IsTargetRangeReachable", ICmpUnitMotion, IsTargetRangeReachable)
DEFINE_INTERFACE_METHOD("FaceTowardsPoint", ICmpUnitMotion, FaceTowardsPoint)
DEFINE_INTERFACE_METHOD("StopMoving", ICmpUnitMotion, StopMoving)
@@ -63,6 +64,11 @@ public:
m_Script.CallVoid("MoveToFormationOffset", target, x, z);
}
+ virtual void SetMemberOfFormation(entity_id_t controller)
+ {
+ m_Script.CallVoid("SetMemberOfFormation", controller);
+ }
+
virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
return m_Script.Call("IsTargetRangeReachable", target, minRange, maxRange);
diff --git a/source/simulation2/components/ICmpUnitMotion.h b/source/simulation2/components/ICmpUnitMotion.h
index caf8c5a78a..e85f0969a8 100644
--- a/source/simulation2/components/ICmpUnitMotion.h
+++ b/source/simulation2/components/ICmpUnitMotion.h
@@ -56,9 +56,16 @@ public:
/**
* Join a formation, and move towards a given offset relative to the formation controller entity.
- * Continues following the formation until given a different command.
+ * The unit will remain 'in formation' fromthe perspective of UnitMotion
+ * until SetMemberOfFormation(INVALID_ENTITY) is passed.
*/
- virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0;
+ virtual void MoveToFormationOffset(entity_id_t controller, entity_pos_t x, entity_pos_t z) = 0;
+
+ /**
+ * Set/unset the unit as a formation member.
+ * @param controller - if INVALID_ENTITY, the unit is no longer a formation member. Otherwise it is and this is the controller.
+ */
+ virtual void SetMemberOfFormation(entity_id_t controller) = 0;
/**
* Check if the target is reachable.