1
1
forked from 0ad/0ad

Further Pushing tweaks: more customisable, longer ranges.

This overall decreases the deathball effect from units walking to each
other a bit.
- Fix formations - this cleans up a UnitMotion hack for formations,
making it possible to increase pushing ranges without breaking closely
knit formations like testudo.
- Make MINIMAL_PUSHING and the MOVE_EXTENSION configurable, and add a
STATIC_EXTENSION as well.
- Increase the pushing range significantly, making units sparser.

Differential Revision: https://code.wildfiregames.com/D4098
This was SVN commit r25708.
This commit is contained in:
wraitii 2021-06-06 15:25:52 +00:00
parent 063408c252
commit 40cbde1925
11 changed files with 139 additions and 46 deletions

View File

@ -5107,7 +5107,7 @@ UnitAI.prototype.SetFormationController = function(ent)
// Set obstruction group, so we can walk through members // Set obstruction group, so we can walk through members
// of our own formation (or ourself if not in formation) // 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 (cmpObstruction)
{ {
if (ent == INVALID_ENTITY) if (ent == INVALID_ENTITY)
@ -5116,6 +5116,10 @@ UnitAI.prototype.SetFormationController = function(ent)
cmpObstruction.SetControlGroup(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 we were removed from a formation, let the FSM switch back to INDIVIDUAL
if (ent == INVALID_ENTITY) if (ent == INVALID_ENTITY)
this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" }); this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });

View File

@ -296,6 +296,11 @@ UnitMotionFlying.prototype.MoveToTargetRange = function(target, minRange, maxRan
return true; return true;
}; };
UnitMotionFlying.prototype.SetMemberOfFormation = function()
{
// Ignored.
};
UnitMotionFlying.prototype.GetWalkSpeed = function() UnitMotionFlying.prototype.GetWalkSpeed = function()
{ {
return +this.template.MaxSpeed; return +this.template.MaxSpeed;

View File

@ -170,6 +170,7 @@ function TestFormationExiting(mode)
"GetWalkSpeed": () => 1, "GetWalkSpeed": () => 1,
"MoveToFormationOffset": (target, x, z) => {}, "MoveToFormationOffset": (target, x, z) => {},
"MoveToTargetRange": (target, min, max) => true, "MoveToTargetRange": (target, min, max) => true,
"SetMemberOfFormation": () => {},
"StopMoving": () => {}, "StopMoving": () => {},
"SetFacePointAfterMove": () => {}, "SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true, "GetFacePointAfterMove": () => true,
@ -349,6 +350,7 @@ function TestMoveIntoFormationWhileAttacking()
"GetWalkSpeed": () => 1, "GetWalkSpeed": () => 1,
"MoveToFormationOffset": (target, x, z) => {}, "MoveToFormationOffset": (target, x, z) => {},
"MoveToTargetRange": (target, min, max) => true, "MoveToTargetRange": (target, min, max) => true,
"SetMemberOfFormation": () => {},
"StopMoving": () => {}, "StopMoving": () => {},
"SetFacePointAfterMove": () => {}, "SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true, "GetFacePointAfterMove": () => true,

View File

@ -4,8 +4,21 @@
<element name="MaxSameTurnMoves"> <element name="MaxSameTurnMoves">
<data type="nonNegativeInteger"/> <data type="nonNegativeInteger"/>
</element> </element>
<element name="PushingRadius"> <element name="Pushing">
<data type="decimal"/> <interleave>
<element name="Radius">
<data type="decimal"/>
</element>
<element name="StaticExtension">
<data type="decimal"/>
</element>
<element name="MovingExtension">
<data type="decimal"/>
</element>
<element name="MinimalForce">
<data type="decimal"/>
</element>
</interleave>
</element> </element>
<element name="PassabilityClasses"> <element name="PassabilityClasses">
<oneOrMore> <oneOrMore>

View File

@ -4,11 +4,30 @@
<!-- Setting the value to 0 disable this functionality --> <!-- Setting the value to 0 disable this functionality -->
<MaxSameTurnMoves>20</MaxSameTurnMoves> <MaxSameTurnMoves>20</MaxSameTurnMoves>
<!-- Multiplier for the distance at which units push each other. --> <Pushing>
<!-- Setting the value to 0 disables unit pushing entirely. --> <!-- Units push each other if they are within 'clearance * radius' meters. -->
<!-- Note that values above 2-3 are likely to start behaving weirdly --> <!-- Setting the value to 0 disables unit pushing entirely. -->
<!-- (in particular, formations offsets may need adjusting). --> <!-- Note that values above 2-3 are likely to start behaving weirdly. -->
<PushingRadius>1.6</PushingRadius> <!-- You can also tweaks extensions below. -->
<Radius>1.6</Radius>
<!-- Actual pushing radius for non-moving units is: -->
<!-- Clearance * PushingRadius + StaticPushExtension -->
<!-- NB: Once idle units start being pushed, they become moving units, -->
<!-- so this should be understood as the maximum unit density. -->
<StaticExtension>2</StaticExtension>
<!-- Actual pushing radius for moving units is: -->
<!-- Clearance * PushingRadius + MovingPushExtension -->
<!-- This is the factor that has the largest influence on actual sparsity. -->
<MovingExtension>2.5</MovingExtension>
<!-- After the combined pushing force of all neighboring units is calculated, -->
<!-- if the value is below this number, treat it as effectively zero. -->
<!-- This nullifies very small pushes, and makes things look and behave nicer. -->
<!-- NB: This impacts the pushing max distance. -->
<MinimalForce>0.2</MinimalForce>
</Pushing>
<PassabilityClasses> <PassabilityClasses>

View File

@ -7,7 +7,7 @@
<SortingClasses>Hero Champion Elite Advanced Basic</SortingClasses> <SortingClasses>Hero Champion Elite Advanced Basic</SortingClasses>
<FormationName>Testudo</FormationName> <FormationName>Testudo</FormationName>
<FormationShape>square</FormationShape> <FormationShape>square</FormationShape>
<UnitSeparationWidthMultiplier>0.50</UnitSeparationWidthMultiplier> <UnitSeparationWidthMultiplier>0.5</UnitSeparationWidthMultiplier>
<UnitSeparationDepthMultiplier>0.4</UnitSeparationDepthMultiplier> <UnitSeparationDepthMultiplier>0.4</UnitSeparationDepthMultiplier>
<SortingOrder>fillFromTheSides</SortingOrder> <SortingOrder>fillFromTheSides</SortingOrder>
<WidthDepthRatio>0.8</WidthDepthRatio> <WidthDepthRatio>0.8</WidthDepthRatio>

View File

@ -143,7 +143,7 @@ public:
// Template state: // Template state:
bool m_FormationController; bool m_IsFormationController;
fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier; fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier;
pass_class_t m_PassClass; 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) {}; MoveRequest(entity_id_t target, CFixedVector2D offset) : m_Type(OFFSET), m_Entity(target), m_Position(offset) {};
} m_MoveRequest; } 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. // If the entity moves, it will do so at m_WalkSpeed * m_SpeedMultiplier.
fixed m_SpeedMultiplier; fixed m_SpeedMultiplier;
// This caches the resulting speed from m_WalkSpeed * m_SpeedMultiplier for convenience. // This caches the resulting speed from m_WalkSpeed * m_SpeedMultiplier for convenience.
@ -253,7 +256,7 @@ public:
virtual void Init(const CParamNode& paramNode) virtual void Init(const CParamNode& paramNode)
{ {
m_FormationController = paramNode.GetChild("FormationController").ToBool(); m_IsFormationController = paramNode.GetChild("FormationController").ToBool();
m_FacePointAfterMove = true; m_FacePointAfterMove = true;
@ -307,6 +310,8 @@ public:
serialize.NumberFixed_Unbounded("target min range", m_MoveRequest.m_MinRange); serialize.NumberFixed_Unbounded("target min range", m_MoveRequest.m_MinRange);
serialize.NumberFixed_Unbounded("target max range", m_MoveRequest.m_MaxRange); 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("speed multiplier", m_SpeedMultiplier);
serialize.NumberFixed_Unbounded("current speed", m_CurSpeed); serialize.NumberFixed_Unbounded("current speed", m_CurSpeed);
@ -358,7 +363,7 @@ public:
case MT_Create: case MT_Create:
{ {
if (!ENTITY_IS_LOCAL(GetEntityId())) if (!ENTITY_IS_LOCAL(GetEntityId()))
CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Register(this, GetEntityId(), m_FormationController); CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController);
break; break;
} }
case MT_Destroy: case MT_Destroy:
@ -390,7 +395,7 @@ public:
{ {
OnValueModification(); OnValueModification();
if (!ENTITY_IS_LOCAL(GetEntityId())) if (!ENTITY_IS_LOCAL(GetEntityId()))
CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Register(this, GetEntityId(), m_FormationController); CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController);
break; break;
} }
} }
@ -501,9 +506,16 @@ public:
return MoveTo(MoveRequest(target, minRange, maxRange)); 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); virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
@ -542,19 +554,18 @@ public:
private: private:
bool IsFormationMember() const bool IsFormationMember() const
{ {
// TODO: this really shouldn't be what we are checking for. return m_FormationController != INVALID_ENTITY;
return m_MoveRequest.m_Type == MoveRequest::OFFSET;
} }
bool IsFormationControllerMoving() const bool IsFormationControllerMoving() const
{ {
CmpPtr<ICmpUnitMotion> cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity); CmpPtr<ICmpUnitMotion> cmpControllerMotion(GetSimContext(), m_FormationController);
return cmpControllerMotion && cmpControllerMotion->IsMoveRequested(); return cmpControllerMotion && cmpControllerMotion->IsMoveRequested();
} }
entity_id_t GetGroup() const entity_id_t GetGroup() const
{ {
return IsFormationMember() ? m_MoveRequest.m_Entity : GetEntityId(); return IsFormationMember() ? m_FormationController : GetEntityId();
} }
void SetParticipateInPushing(bool pushing) void SetParticipateInPushing(bool pushing)
@ -975,7 +986,7 @@ void CCmpUnitMotion::PreMove(CCmpUnitMotionManager::MotionState& state)
if (!m_BlockMovement) if (!m_BlockMovement)
return; 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, // 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. // so it does not really reflect whether the unit is actually moving or not.

View File

@ -81,9 +81,15 @@ public:
bool isMoving = false; 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). // "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; 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. // These vectors are reconstructed on deserialization.

View File

@ -38,11 +38,6 @@ namespace {
*/ */
static const int PUSHING_GRID_SIZE = 20; 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, * For pushing, treat the clearances as a circle - they're defined as squares,
* so we'll take the circumscribing square (approximately). * 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; 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. * Arbitrary constant used to reduce pushing to levels that won't break physics for our turn length.
*/ */
static const int PUSHING_REDUCTION_FACTOR = 2; 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<ICmpPosition> cmpPos, CCmpUnitMotion* cmpMotion) CCmpUnitMotionManager::MotionState::MotionState(CmpPtr<ICmpPosition> 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. // TODO: there seems to be no real reason why we could not register a 'system' entity somewhere instead.
CParamNode externalParamNode; CParamNode externalParamNode;
CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml", "pathfinder"); 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()) if (radius.IsOk())
{ {
m_PushingRadius = radius.ToFixed(); m_PushingRadius = radius.ToFixed();
@ -86,7 +86,25 @@ void CCmpUnitMotionManager::Init(const CParamNode&)
} }
else else
m_PushingRadius = entity_pos_t::FromInt(8) / 5; 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) void CCmpUnitMotionManager::Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController)
@ -212,7 +230,7 @@ void CCmpUnitMotionManager::Move(EntityMap<MotionState>& ents, fixed dt)
it->second.push = CFixedVector2D(); it->second.push = CFixedVector2D();
} }
// Only apply pushing if the effect is significant enough. // 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 // If there was an attempt at movement, and the pushed movement is in a sufficiently different direction
// (measured by an extremely arbitrary dot product) // (measured by an extremely arbitrary dot product)
@ -255,16 +273,17 @@ void CCmpUnitMotionManager::Push(EntityMap<MotionState>::value_type& a, EntityMa
// Exception: units in the same control group (i.e. the same formation) never push farther than themselves // 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, // 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). // 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; movingPush = 0;
if (movingPush == 1) if (movingPush == 1)
return; 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; entity_pos_t maxDist = combinedClearance;
if (movingPush) if (!sameControlGroup)
maxDist += PUSHING_MOVING_INFLUENCE_EXTENSION; maxDist = combinedClearance.Multiply(m_PushingRadius) + (movingPush ? m_MovingPushExtension : m_StaticPushExtension);
CFixedVector2D offset = a.second.pos - b.second.pos; CFixedVector2D offset = a.second.pos - b.second.pos;
if (offset.CompareLength(maxDist) > 0) if (offset.CompareLength(maxDist) > 0)
@ -301,11 +320,12 @@ void CCmpUnitMotionManager::Push(EntityMap<MotionState>::value_type& a, EntityMa
offsetLength = fixed::Zero(); offsetLength = fixed::Zero();
} }
// The formula expects 'normal' pushing if the two entities edges are touching. // 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); entity_pos_t distanceFactor = maxDist - combinedClearance;
distanceFactor = Clamp(distanceFactor, entity_pos_t::Zero(), entity_pos_t::FromInt(2)); 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. // Mark both as needing an update so they actually get moved.
a.second.needUpdate = true; a.second.needUpdate = true;
@ -314,6 +334,6 @@ void CCmpUnitMotionManager::Push(EntityMap<MotionState>::value_type& a, EntityMa
CFixedVector2D pushingDir = offset.Multiply(distanceFactor); CFixedVector2D pushingDir = offset.Multiply(distanceFactor);
// Divide by an arbitrary constant to avoid pushing too much. // Divide by an arbitrary constant to avoid pushing too much.
a.second.push += pushingDir.Multiply(movingPush ? dt : dt / PUSHING_REDUCTION_FACTOR); a.second.push += pushingDir.Multiply(dt / PUSHING_REDUCTION_FACTOR);
b.second.push -= pushingDir.Multiply(movingPush ? dt : dt / PUSHING_REDUCTION_FACTOR); b.second.push -= pushingDir.Multiply(dt / PUSHING_REDUCTION_FACTOR);
} }

View File

@ -26,6 +26,7 @@ BEGIN_INTERFACE_WRAPPER(UnitMotion)
DEFINE_INTERFACE_METHOD("MoveToPointRange", ICmpUnitMotion, MoveToPointRange) DEFINE_INTERFACE_METHOD("MoveToPointRange", ICmpUnitMotion, MoveToPointRange)
DEFINE_INTERFACE_METHOD("MoveToTargetRange", ICmpUnitMotion, MoveToTargetRange) DEFINE_INTERFACE_METHOD("MoveToTargetRange", ICmpUnitMotion, MoveToTargetRange)
DEFINE_INTERFACE_METHOD("MoveToFormationOffset", ICmpUnitMotion, MoveToFormationOffset) DEFINE_INTERFACE_METHOD("MoveToFormationOffset", ICmpUnitMotion, MoveToFormationOffset)
DEFINE_INTERFACE_METHOD("SetMemberOfFormation", ICmpUnitMotion, SetMemberOfFormation)
DEFINE_INTERFACE_METHOD("IsTargetRangeReachable", ICmpUnitMotion, IsTargetRangeReachable) DEFINE_INTERFACE_METHOD("IsTargetRangeReachable", ICmpUnitMotion, IsTargetRangeReachable)
DEFINE_INTERFACE_METHOD("FaceTowardsPoint", ICmpUnitMotion, FaceTowardsPoint) DEFINE_INTERFACE_METHOD("FaceTowardsPoint", ICmpUnitMotion, FaceTowardsPoint)
DEFINE_INTERFACE_METHOD("StopMoving", ICmpUnitMotion, StopMoving) DEFINE_INTERFACE_METHOD("StopMoving", ICmpUnitMotion, StopMoving)
@ -63,6 +64,11 @@ public:
m_Script.CallVoid("MoveToFormationOffset", target, x, z); 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) virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{ {
return m_Script.Call<bool>("IsTargetRangeReachable", target, minRange, maxRange); return m_Script.Call<bool>("IsTargetRangeReachable", target, minRange, maxRange);

View File

@ -56,9 +56,16 @@ public:
/** /**
* Join a formation, and move towards a given offset relative to the formation controller entity. * 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. * Check if the target is reachable.