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
// 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" });

View File

@ -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;

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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.

View File

@ -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.

View File

@ -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);
}

View File

@ -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);

View File

@ -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.