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:
parent
063408c252
commit
40cbde1925
@ -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" });
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -4,8 +4,21 @@
|
||||
<element name="MaxSameTurnMoves">
|
||||
<data type="nonNegativeInteger"/>
|
||||
</element>
|
||||
<element name="PushingRadius">
|
||||
<data type="decimal"/>
|
||||
<element name="Pushing">
|
||||
<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 name="PassabilityClasses">
|
||||
<oneOrMore>
|
||||
|
@ -4,11 +4,30 @@
|
||||
<!-- Setting the value to 0 disable this functionality -->
|
||||
<MaxSameTurnMoves>20</MaxSameTurnMoves>
|
||||
|
||||
<!-- Multiplier for the distance at which units push each other. -->
|
||||
<!-- Setting the value to 0 disables unit pushing entirely. -->
|
||||
<!-- Note that values above 2-3 are likely to start behaving weirdly -->
|
||||
<!-- (in particular, formations offsets may need adjusting). -->
|
||||
<PushingRadius>1.6</PushingRadius>
|
||||
<Pushing>
|
||||
<!-- Units push each other if they are within 'clearance * radius' meters. -->
|
||||
<!-- Setting the value to 0 disables unit pushing entirely. -->
|
||||
<!-- Note that values above 2-3 are likely to start behaving weirdly. -->
|
||||
<!-- 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>
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
<SortingClasses>Hero Champion Elite Advanced Basic</SortingClasses>
|
||||
<FormationName>Testudo</FormationName>
|
||||
<FormationShape>square</FormationShape>
|
||||
<UnitSeparationWidthMultiplier>0.50</UnitSeparationWidthMultiplier>
|
||||
<UnitSeparationWidthMultiplier>0.5</UnitSeparationWidthMultiplier>
|
||||
<UnitSeparationDepthMultiplier>0.4</UnitSeparationDepthMultiplier>
|
||||
<SortingOrder>fillFromTheSides</SortingOrder>
|
||||
<WidthDepthRatio>0.8</WidthDepthRatio>
|
||||
|
@ -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<ICmpUnitMotionManager>(GetSystemEntity())->Register(this, GetEntityId(), m_FormationController);
|
||||
CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController);
|
||||
break;
|
||||
}
|
||||
case MT_Destroy:
|
||||
@ -390,7 +395,7 @@ public:
|
||||
{
|
||||
OnValueModification();
|
||||
if (!ENTITY_IS_LOCAL(GetEntityId()))
|
||||
CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Register(this, GetEntityId(), m_FormationController);
|
||||
CmpPtr<ICmpUnitMotionManager>(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<ICmpUnitMotion> cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity);
|
||||
CmpPtr<ICmpUnitMotion> 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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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<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.
|
||||
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<MotionState>& 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<MotionState>::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<MotionState>::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<MotionState>::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);
|
||||
}
|
||||
|
@ -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<bool>("IsTargetRangeReachable", target, minRange, maxRange);
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user