Index: ps/trunk/binaries/data/mods/public/maps/scenarios/unit_pushing_test.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scenarios/unit_pushing_test.js
+++ ps/trunk/binaries/data/mods/public/maps/scenarios/unit_pushing_test.js
@@ -110,6 +110,16 @@
}
};
+experiments.units_superdense_forest_of_fast_units = {
+ "spawn": (gx, gy) => {
+ for (let i = -12; i <= 12; i += 2)
+ for (let j = -12; j <= 12; j += 2)
+ QuickSpawn(gx + i, gy + 50 + j, FAST_UNIT_TEMPLATE);
+ WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy, FAST_UNIT_TEMPLATE));
+ WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy-10, LARGE_UNIT_TEMPLATE));
+ }
+};
+
experiments.building = {
"spawn": (gx, gy) => {
let target = QuickSpawn(gx + 20, gy + 20, "foundation|structures/athen/storehouse");
@@ -146,41 +156,49 @@
}
};
-experiments.sep1 = {
- "spawn": (gx, gy) => {}
-};
-
-experiments.battle = {
+experiments.multicrossing = {
"spawn": (gx, gy) => {
- for (let i = 0; i < 4; ++i)
- for (let j = 0; j < 8; ++j)
- {
- QuickSpawn(gx + i, gy + j, REG_UNIT_TEMPLATE);
- QuickSpawn(gx + i, gy + 50 + j, REG_UNIT_TEMPLATE, ATTACKER);
- }
+ for (let i = 0; i < 20; i += 2)
+ for (let j = 0; j < 20; j += 2)
+ WalkTo(gx+10, gy+70, false, QuickSpawn(gx + i, gy + j, REG_UNIT_TEMPLATE));
+ for (let i = 0; i < 20; i += 2)
+ for (let j = 0; j < 20; j += 2)
+ WalkTo(gx+10, gy, false, QuickSpawn(gx + i, gy + j + 70, REG_UNIT_TEMPLATE));
}
};
-experiments.sep2 = {
- "spawn": (gx, gy) => {}
-};
-
-
-experiments.overlapping = {
+// Same as above but not as aligned.
+experiments.multicrossing_spaced = {
"spawn": (gx, gy) => {
- for (let i = 0; i < 20; ++i)
- QuickSpawn(gx, gy, REG_UNIT_TEMPLATE);
+ for (let i = 0; i < 20; i += 2)
+ for (let j = 0; j < 20; j += 2)
+ WalkTo(gx+10, gy+70, false, QuickSpawn(gx + i, gy + j, REG_UNIT_TEMPLATE));
+ for (let i = 0; i < 20; i += 2)
+ for (let j = 0; j < 20; j += 2)
+ WalkTo(gx+10 + 5, gy, false, QuickSpawn(gx + i + 5, gy + j + 70, REG_UNIT_TEMPLATE));
}
};
-experiments.multicrossing = {
+// Same as above but not as aligned.
+experiments.multicrossing_spaced_2 = {
"spawn": (gx, gy) => {
for (let i = 0; i < 20; i += 2)
for (let j = 0; j < 20; j += 2)
WalkTo(gx+10, gy+70, false, QuickSpawn(gx + i, gy + j, REG_UNIT_TEMPLATE));
for (let i = 0; i < 20; i += 2)
for (let j = 0; j < 20; j += 2)
- WalkTo(gx+10, gy, false, QuickSpawn(gx + i, gy + j + 70, REG_UNIT_TEMPLATE));
+ WalkTo(gx+10 - 5, gy, false, QuickSpawn(gx + i - 5, gy + j + 70, REG_UNIT_TEMPLATE));
+ }
+};
+
+experiments.crossing_perpendicular = {
+ "spawn": (gx, gy) => {
+ for (let i = 0; i < 20; i += 4)
+ for (let j = 0; j < 20; j += 4)
+ WalkTo(gx+10, gy+70, false, QuickSpawn(gx + i, gy + j, REG_UNIT_TEMPLATE));
+ for (let i = 0; i < 20; i += 4)
+ for (let j = 0; j < 20; j += 4)
+ WalkTo(gx - 35, gy + 35, false, QuickSpawn(gx + i + 35, gy + j + 35, REG_UNIT_TEMPLATE));
}
};
@@ -194,14 +212,44 @@
}
};
+
+experiments.sep1 = {
+ "spawn": (gx, gy) => {}
+};
+
+experiments.battle = {
+ "spawn": (gx, gy) => {
+ for (let i = 0; i < 4; ++i)
+ for (let j = 0; j < 8; ++j)
+ {
+ QuickSpawn(gx + i, gy + j, REG_UNIT_TEMPLATE);
+ QuickSpawn(gx + i, gy + 50 + j, REG_UNIT_TEMPLATE, ATTACKER);
+ }
+ }
+};
+
+experiments.sep2 = {
+ "spawn": (gx, gy) => {}
+};
+
+
+experiments.overlapping = {
+ "spawn": (gx, gy) => {
+ for (let i = 0; i < 20; ++i)
+ QuickSpawn(gx, gy, REG_UNIT_TEMPLATE);
+ for (let i = 0; i < 20; ++i)
+ QuickSpawn(gx+15, gy+15, REG_UNIT_TEMPLATE);
+ }
+};
+
var perf_experiments = {};
// Perf check: put units everywhere, not moving.
perf_experiments.Idle = {
"spawn": () => {
const spacing = 12;
- for (let x = 0; x < 20*16*4 - 20; x += spacing)
- for (let z = 0; z < 20*16*4 - 20; z += spacing)
+ for (let x = 0; x < 20*4*4 - 20; x += spacing)
+ for (let z = 0; z < 20*4*4 - 20; z += spacing)
QuickSpawn(x, z, REG_UNIT_TEMPLATE);
}
};
@@ -278,6 +326,7 @@
}
};
+
var woodcutting = (gx, gy) => {
let dropsite = QuickSpawn(gx + 50, gy, "structures/athen/storehouse");
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
@@ -318,14 +367,15 @@
for (let key in experiments)
{
experiments[key].spawn(gx, gy);
- gx += 60;
+ gx += 90;
if (gx > 20*16*4-20)
{
- gx = 20;
- gy += 100;
+ gx = 100;
+ gy += 150;
}
}
/**/
+ //perf_experiments.LotsaLocalCollisions.spawn();
/*
let time = 0;
for (let key in perf_experiments)
Index: ps/trunk/binaries/data/mods/public/simulation/data/pathfinder.rng
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/pathfinder.rng
+++ ps/trunk/binaries/data/mods/public/simulation/data/pathfinder.rng
@@ -15,9 +15,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
Index: ps/trunk/binaries/data/mods/public/simulation/data/pathfinder.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/pathfinder.xml
+++ ps/trunk/binaries/data/mods/public/simulation/data/pathfinder.xml
@@ -9,18 +9,27 @@
- 1.6
+ 1.4
-
-
- 2
+
+ 1.5
-
- 2.5
+ 4.0
+
+
+
+
+
+
+
+
+
+ 0.9
+ 0.4
@@ -29,6 +38,14 @@
0.2
+
+
+
+
+ 0.5
+
+ 0.6
+
Index: ps/trunk/source/maths/Fixed.h
===================================================================
--- ps/trunk/source/maths/Fixed.h
+++ ps/trunk/source/maths/Fixed.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -142,6 +142,12 @@
return CFixed(n << fract_bits);
}
+ // TODO C++20: this won't be necessary when operator/(int) can be made constexpr.
+ static constexpr CFixed FromFraction(int n, int d)
+ {
+ return CFixed(static_cast(static_cast(n) << fract_bits) / d);
+ }
+
static constexpr CFixed FromFloat(float n)
{
if (!std::isfinite(n))
Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.h
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotion.h
+++ ps/trunk/source/simulation2/components/CCmpUnitMotion.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -42,6 +42,8 @@
#include "ps/Profile.h"
#include "renderer/Scene.h"
+#include
+
// NB: this implementation of ICmpUnitMotion is very tightly coupled with UnitMotionManager.
// As such, both are compiled in the same TU.
@@ -424,8 +426,6 @@
case MT_Deserialized:
{
OnValueModification();
- if (!ENTITY_IS_LOCAL(GetEntityId()))
- CmpPtr(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController);
break;
}
}
@@ -483,7 +483,7 @@
WaypointPath shortPath = m_ShortPath;
WaypointPath longPath = m_LongPath;
- PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, speed, angle);
+ PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, speed, angle, 0);
return pos;
}
@@ -753,7 +753,7 @@
* This does not send actually change the position.
* @returns true if the move was obstructed.
*/
- bool PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle) const;
+ bool PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle, uint8_t pushingPressure) const;
/**
* Update other components on our speed.
@@ -1049,7 +1049,7 @@
// to it, then throw away our current path and go straight to it.
state.wentStraight = TryGoingStraightToTarget(state.initialPos, true);
- state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.speed, state.angle);
+ state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.speed, state.angle, state.pushingPressure);
}
void CCmpUnitMotion::PostMove(CCmpUnitMotionManager::MotionState& state, fixed dt)
@@ -1065,11 +1065,6 @@
{
// Update the Position component after our movement (if we actually moved anywhere)
CFixedVector2D offset = state.pos - state.initialPos;
- // When moving always set the angle in the direction of the movement,
- // if we are not trying to move, assume this is pushing-related movement,
- // and maintain the current angle instead.
- if (IsMoveRequested())
- state.angle = atan2_approx(offset.X, offset.Y);
state.cmpPosition->MoveAndTurnTo(state.pos.X, state.pos.Y, state.angle);
// Calculate the mean speed over this past turn.
@@ -1126,7 +1121,7 @@
return false;
}
-bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle) const
+bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle, uint8_t pushingPressure) const
{
// 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())
@@ -1138,11 +1133,6 @@
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
- // that problem.
-
CmpPtr cmpPathfinder(GetSystemEntity());
ENSURE(cmpPathfinder);
@@ -1151,6 +1141,28 @@
if (IsMovingAsFormation())
basicSpeed = m_Speed.Multiply(m_RunMultiplier);
+ // If pushing pressure is applied, slow the unit down.
+ if (pushingPressure)
+ {
+ // Values below this pressure don't slow the unit down (avoids slowing groups down).
+ constexpr int pressureMinThreshold = 10;
+
+ // Lower speed up to a floor to prevent units from getting stopped.
+ // This helped pushing particularly for fast units, since they'll end up slowing down.
+ constexpr int maxPressure = CCmpUnitMotionManager::MAX_PRESSURE - pressureMinThreshold - 80;
+ constexpr entity_pos_t floorSpeed = entity_pos_t::FromFraction(3, 2);
+ static_assert(maxPressure > 0);
+
+ uint8_t slowdown = maxPressure - std::min(maxPressure, std::max(0, pushingPressure - pressureMinThreshold));
+ basicSpeed = basicSpeed.Multiply(fixed::FromInt(slowdown) / maxPressure);
+ // NB: lowering this too much will make the units behave a lot like viscous fluid
+ // when the density becomes extreme. While perhaps realistic (and kind of neat),
+ // it's not very helpful for gameplay. Empirically, a value of 1.5 avoids most of the effect
+ // while still slowing down movement significantly, and seems like a good balance.
+ // Min with the template speed to allow units that are explicitly absurdly slow.
+ basicSpeed = std::max(std::min(m_TemplateWalkSpeed, floorSpeed), basicSpeed);
+ }
+
// Find the speed factor of the underlying terrain.
// (We only care about the tile we start on - it doesn't matter if we're moving
// partially onto a much slower/faster tile).
@@ -1184,18 +1196,19 @@
CFixedVector2D offset = target - pos;
- fixed angleDiff = angle - atan2_approx(offset.X, offset.Y);
- fixed absoluteAngleDiff = angleDiff.Absolute();
- if (absoluteAngleDiff > entity_angle_t::Pi())
- absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff;
-
- // We only rotate to the instantTurnAngle angle. The rest we rotate during movement.
- if (absoluteAngleDiff > m_InstantTurnAngle)
- {
- // Stop moving when rotating this far.
- speed = zero;
- if (turnRate > zero && !offset.IsZero())
+ if (turnRate > zero && !offset.IsZero())
+ {
+ fixed angleDiff = angle - atan2_approx(offset.X, offset.Y);
+ fixed absoluteAngleDiff = angleDiff.Absolute();
+ if (absoluteAngleDiff > entity_angle_t::Pi())
+ absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff;
+
+ // We only rotate to the instantTurnAngle angle. The rest we rotate during movement.
+ if (absoluteAngleDiff > m_InstantTurnAngle)
{
+ // Stop moving when rotating this far.
+ speed = zero;
+
fixed maxRotation = turnRate.Multiply(timeLeft);
// Figure out whether rotating will increase or decrease the angle, and how far we need to rotate in that direction.
@@ -1213,13 +1226,14 @@
angle = atan2_approx(offset.X, offset.Y);
timeLeft = std::min(maxRotation, maxRotation - absoluteAngleDiff + m_InstantTurnAngle) / turnRate;
}
- }
- else
- {
- // Modify the speed depending on the angle difference.
- fixed sin, cos;
- sincos_approx(angleDiff, sin, cos);
- speed = speed.Multiply(cos);
+ else
+ {
+ // Modify the speed depending on the angle difference.
+ fixed sin, cos;
+ sincos_approx(angleDiff, sin, cos);
+ speed = speed.Multiply(cos);
+ angle = atan2_approx(offset.X, offset.Y);
+ }
}
// Work out how far we can travel in timeLeft.
@@ -1792,7 +1806,7 @@
}
m_ExpectedPathTicket.m_Type = Ticket::SHORT_PATH;
- m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, true, GetGroup(), GetEntityId());
+ m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, ShouldCollideWithMovingUnits(), GetGroup(), GetEntityId());
}
bool CCmpUnitMotion::MoveTo(MoveRequest request)
Index: ps/trunk/source/simulation2/components/CCmpUnitMotionManager.h
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotionManager.h
+++ ps/trunk/source/simulation2/components/CCmpUnitMotionManager.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -31,25 +31,26 @@
class CCmpUnitMotionManager : public ICmpUnitMotionManager
{
public:
- static void ClassInit(CComponentManager& componentManager)
- {
- componentManager.SubscribeToMessageType(MT_TerrainChanged);
- componentManager.SubscribeToMessageType(MT_TurnStart);
- componentManager.SubscribeToMessageType(MT_Update_Final);
- componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
- componentManager.SubscribeToMessageType(MT_Update_MotionFormation);
- }
+ static void ClassInit(CComponentManager& componentManager);
DEFAULT_COMPONENT_ALLOCATOR(UnitMotionManager)
+ /**
+ * Maximum value for pushing pressure.
+ */
+ static constexpr int MAX_PRESSURE = 255;
+
// Persisted state for each unit.
struct MotionState
{
- MotionState(CmpPtr cmpPos, CCmpUnitMotion* cmpMotion);
+ MotionState(ICmpPosition* cmpPos, CCmpUnitMotion* cmpMotion);
// Component references - these must be kept alive for the duration of motion.
- // NB: this is generally not something one should do, but because of the tight coupling here it's doable.
- CmpPtr cmpPosition;
+ // NB: this is generally a super dangerous thing to do,
+ // but the tight coupling with CCmpUnitMotion makes it workable.
+ // NB: this assumes that components do _not_ move in memory,
+ // which is currently a fair assumption but might change in the future.
+ ICmpPosition* cmpPosition;
CCmpUnitMotion* cmpUnitMotion;
// Position before units start moving
@@ -69,6 +70,11 @@
// (this is required because formations may be tight and large units may end up never settling.
entity_id_t controlGroup = INVALID_ENTITY;
+ // This is a ad-hoc counter to store under how much pushing 'pressure' an entity is.
+ // More pressure will slow the unit down and make it harder to push,
+ // which effectively bogs down groups of colliding units.
+ uint8_t pushingPressure = 0;
+
// Meta-flag -> this entity won't push nor be pushed.
// (used for entities that have their obstruction disabled).
bool ignore = false;
@@ -85,14 +91,25 @@
// "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.
+ // The maximal distance at which units push each other is the combined unit clearances, multipled by this factor,
+ // itself pre-multiplied by the circle-square correction factor.
+ entity_pos_t m_PushingRadiusMultiplier;
+ // Additive modifiers to the maximum pushing distance for moving units and idle units respectively.
entity_pos_t m_MovingPushExtension;
entity_pos_t m_StaticPushExtension;
+ // Multiplier for the pushing 'spread'.
+ // This should be understand as the % of the maximum distance where pushing will be "in full force".
+ entity_pos_t m_MovingPushingSpread;
+ entity_pos_t m_StaticPushingSpread;
+
// Pushing forces below this value are ignored - this prevents units moving forever by very small increments.
entity_pos_t m_MinimalPushing;
+ // Multiplier for pushing pressure strength.
+ entity_pos_t m_PushingPressureStrength;
+ // Per-turn reduction in pushing pressure.
+ entity_pos_t m_PushingPressureDecay;
+
// These vectors are reconstructed on deserialization.
EntityMap m_Units;
@@ -114,50 +131,10 @@
{
}
- virtual void Serialize(ISerializer& UNUSED(serialize))
- {
- }
+ virtual void Serialize(ISerializer& serialize);
+ virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize);
- virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize))
- {
- Init(paramNode);
- ResetSubdivisions();
- }
-
- virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
- {
- switch (msg.GetType())
- {
- case MT_TerrainChanged:
- {
- CmpPtr cmpTerrain(GetSystemEntity());
- if (cmpTerrain->GetVerticesPerSide() != m_MovingUnits.width())
- ResetSubdivisions();
- break;
- }
- case MT_TurnStart:
- {
- OnTurnStart();
- break;
- }
- case MT_Update_MotionFormation:
- {
- fixed dt = static_cast(msg).turnLength;
- m_ComputingMotion = true;
- MoveFormations(dt);
- m_ComputingMotion = false;
- break;
- }
- case MT_Update_MotionUnit:
- {
- fixed dt = static_cast(msg).turnLength;
- m_ComputingMotion = true;
- MoveUnits(dt);
- m_ComputingMotion = false;
- break;
- }
- }
- }
+ virtual void HandleMessage(const CMessage& msg, bool global);
virtual void Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController);
virtual void Unregister(entity_id_t ent);
@@ -169,10 +146,11 @@
virtual bool IsPushingActivated() const
{
- return m_PushingRadius != entity_pos_t::Zero();
+ return m_PushingRadiusMultiplier != entity_pos_t::Zero();
}
private:
+ void OnDeserialized();
void ResetSubdivisions();
void OnTurnStart();
@@ -183,17 +161,6 @@
void Push(EntityMap::value_type& a, EntityMap::value_type& b, fixed dt);
};
-void CCmpUnitMotionManager::ResetSubdivisions()
-{
- CmpPtr cmpTerrain(GetSystemEntity());
- if (!cmpTerrain)
- return;
-
- size_t size = cmpTerrain->GetMapSize();
- u16 gridSquareSize = static_cast(size / 20 + 1);
- m_MovingUnits.resize(gridSquareSize, gridSquareSize);
-}
-
REGISTER_COMPONENT_TYPE(UnitMotionManager)
#endif // INCLUDED_CCMPUNITMOTIONMANAGER
Index: ps/trunk/source/simulation2/components/CCmpUnitMotion_System.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotion_System.cpp
+++ ps/trunk/source/simulation2/components/CCmpUnitMotion_System.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -24,7 +24,14 @@
#include "ps/CLogger.h"
#include "ps/Profile.h"
+#include
+#include
#include
+#include
+
+#define DEBUG_STATS 0
+#define DEBUG_RENDER 0
+#define DEBUG_RENDER_ALL_PUSH 0
// NB: this TU contains the CCmpUnitMotion/CCmpUnitMotionManager couple.
// In practice, UnitMotionManager functions need access to the full implementation of UnitMotion,
@@ -32,38 +39,138 @@
// To avoid inclusion issues, implementation of UnitMotionManager that uses UnitMotion is here.
namespace {
- /**
- * Units push only within their own grid square. This is the size of each square (in arbitrary units).
- * TODO: check other values.
- */
- static const int PUSHING_GRID_SIZE = 20;
-
- /**
- * For pushing, treat the clearances as a circle - they're defined as squares,
- * so we'll take the circumscribing square (approximately).
- * Clerances are also full-width instead of half, so we want to divide by two. sqrt(2)/2 is about 0.71 < 5/7.
- */
- static const entity_pos_t PUSHING_CORRECTION = entity_pos_t::FromInt(5) / 7;
-
- /**
- * 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.
- * NB: this value interacts with the "minimal pushing" force,
- * as two perfectly overlapping units exert MAX_DISTANCE_FACTOR * Turn length in ms / REDUCTION_FACTOR
- * of force on each other each turn. If this is below the minimal pushing force, any 2 units can entirely overlap.
- */
- static const entity_pos_t MAX_DISTANCE_FACTOR = entity_pos_t::FromInt(5) / 2;
+/**
+ * Units push within their square and neighboring squares (except diagonals). This is the size of each square (in meters).
+ * I have tested grid sizes from 10 up to 80 and overall it made little difference to the performance,
+ * mostly, I suspect, because pushing is generally dwarfed by regular motion costs.
+ * However, the algorithm remains n^2 in comparisons so it's probably best to err on the side of smaller grids, which will have lower spikes.
+ * The balancing act is between comparisons, unordered_set insertions and unordered_set iterations.
+ * For these reasons, a value of 20 which is rather small but not overly so was chosen.
+ */
+constexpr int PUSHING_GRID_SIZE = 20;
+
+/**
+ * For pushing, treat the clearances as a circle - they're defined as squares,
+ * so we'll take the circumscribing square (approximately).
+ * Clerances are also full-width instead of half, so we want to divide by two. sqrt(2)/2 is about 0.71 < 5/7.
+ */
+constexpr entity_pos_t PUSHING_CORRECTION = entity_pos_t::FromFraction(5, 7);
+
+/**
+ * Arbitrary constant used to reduce pushing to levels that won't break physics for our turn length.
+ */
+constexpr int PUSHING_REDUCTION_FACTOR = 2;
+
+/**
+ * Maximum distance-related multiplier.
+ * NB: this value interacts with the "minimal pushing" force,
+ * as two perfectly overlapping units exert MAX_DISTANCE_FACTOR * Turn length in ms / REDUCTION_FACTOR
+ * of force on each other each turn. If this is below the minimal pushing force, any 2 units can entirely overlap.
+ */
+constexpr entity_pos_t MAX_DISTANCE_FACTOR = entity_pos_t::FromFraction(5, 2);
+
+/**
+ * When two units collide, if their movement dot product is below this value, give them a perpendicular nudge instead of trying to push in the regular way.
+ */
+constexpr entity_pos_t PERPENDICULAR_NUDGE_THRESHOLD = entity_pos_t::FromFraction(-1, 10);
+
+/**
+ * Pushing is dampened by pushing pressure, but this is capped so that units still get pushed.
+ */
+constexpr int MAX_PUSH_DAMPING_PRESSURE = 160;
+static_assert(MAX_PUSH_DAMPING_PRESSURE < CCmpUnitMotionManager::MAX_PRESSURE);
+
+/**
+ * When units are obstructed because they're being pushed away from where they want to go,
+ * raise the pushing pressure to at least this value.
+ */
+constexpr int MIN_PRESSURE_IF_OBSTRUCTED = 80;
+
+/**
+ * These two numbers are used to calculate pushing pressure between two units.
+ */
+constexpr entity_pos_t PRESSURE_STATIC_FACTOR = entity_pos_t::FromInt(2);
+constexpr int PRESSURE_DISTANCE_FACTOR = 5;
}
-CCmpUnitMotionManager::MotionState::MotionState(CmpPtr cmpPos, CCmpUnitMotion* cmpMotion)
+#if DEBUG_RENDER
+#include "maths/Frustum.h"
+
+void RenderDebugOverlay(SceneCollector& collector, const CFrustum& frustum, bool culling);
+
+struct SDebugData {
+ std::vector m_Spheres;
+ std::vector m_Lines;
+ std::vector m_Quads;
+} debugDataMotionMgr;
+#endif
+
+CCmpUnitMotionManager::MotionState::MotionState(ICmpPosition* cmpPos, CCmpUnitMotion* cmpMotion)
: cmpPosition(cmpPos), cmpUnitMotion(cmpMotion)
{
+ static_assert(MAX_PRESSURE <= std::numeric_limits::max(), "MAX_PRESSURE is higher than the maximum value of the underlying type.");
}
+void CCmpUnitMotionManager::ClassInit(CComponentManager& componentManager)
+{
+ componentManager.SubscribeToMessageType(MT_Deserialized);
+ componentManager.SubscribeToMessageType(MT_TerrainChanged);
+ componentManager.SubscribeToMessageType(MT_TurnStart);
+ componentManager.SubscribeToMessageType(MT_Update_Final);
+ componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
+ componentManager.SubscribeToMessageType(MT_Update_MotionFormation);
+#if DEBUG_RENDER
+ componentManager.SubscribeToMessageType(MT_RenderSubmit);
+#endif
+}
+
+void CCmpUnitMotionManager::HandleMessage(const CMessage& msg, bool UNUSED(global))
+{
+ switch (msg.GetType())
+ {
+ case MT_TerrainChanged:
+ {
+ CmpPtr cmpTerrain(GetSystemEntity());
+ if (cmpTerrain->GetVerticesPerSide() != m_MovingUnits.width())
+ ResetSubdivisions();
+ break;
+ }
+ case MT_TurnStart:
+ {
+ OnTurnStart();
+ break;
+ }
+ case MT_Update_MotionFormation:
+ {
+ fixed dt = static_cast(msg).turnLength;
+ m_ComputingMotion = true;
+ MoveFormations(dt);
+ m_ComputingMotion = false;
+ break;
+ }
+ case MT_Update_MotionUnit:
+ {
+ fixed dt = static_cast(msg).turnLength;
+ m_ComputingMotion = true;
+ MoveUnits(dt);
+ m_ComputingMotion = false;
+ break;
+ }
+ case MT_Deserialized:
+ {
+ OnDeserialized();
+ break;
+ }
+#if DEBUG_RENDER
+ case MT_RenderSubmit:
+ {
+ const CMessageRenderSubmit& msgData = static_cast (msg);
+ RenderDebugOverlay(msgData.collector, msgData.frustum, msgData.culling);
+ break;
+ }
+#endif
+ }
+}
void CCmpUnitMotionManager::Init(const CParamNode&)
{
// Load some data - see CCmpPathfinder.xml.
@@ -76,19 +183,43 @@
// 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 spread = pushingNode.GetChild("MovingSpread");
+ if (spread.IsOk())
+ {
+ m_MovingPushingSpread = Clamp(spread.ToFixed(), entity_pos_t::Zero(), entity_pos_t::FromInt(1));
+ if (m_MovingPushingSpread != spread.ToFixed())
+ LOGWARNING("Moving pushing spread was clamped to the 0-1 range.");
+ }
+ else
+ m_MovingPushingSpread = entity_pos_t::FromInt(5) / 8;
+ }
+
+ {
+ const CParamNode spread = pushingNode.GetChild("StaticSpread");
+ if (spread.IsOk())
+ {
+ m_StaticPushingSpread = Clamp(spread.ToFixed(), entity_pos_t::Zero(), entity_pos_t::FromInt(1));
+ if (m_StaticPushingSpread != spread.ToFixed())
+ LOGWARNING("Static pushing spread was clamped to the 0-1 range.");
+ }
+ else
+ m_StaticPushingSpread = entity_pos_t::FromInt(5) / 8;
+ }
+
const CParamNode radius = pushingNode.GetChild("Radius");
if (radius.IsOk())
{
- m_PushingRadius = radius.ToFixed();
- if (m_PushingRadius < entity_pos_t::Zero())
+ m_PushingRadiusMultiplier = radius.ToFixed();
+ if (m_PushingRadiusMultiplier < entity_pos_t::Zero())
{
- LOGWARNING("Pushing radius cannot be below 0. De-activating pushing but 'pathfinder.xml' should be updated.");
- m_PushingRadius = entity_pos_t::Zero();
+ LOGWARNING("Pushing radius multiplier cannot be below 0. De-activating pushing but 'pathfinder.xml' should be updated.");
+ m_PushingRadiusMultiplier = entity_pos_t::Zero();
}
// No upper value, but things won't behave sanely if values are too high.
}
else
- m_PushingRadius = entity_pos_t::FromInt(8) / 5;
+ m_PushingRadiusMultiplier = entity_pos_t::FromInt(8) / 5;
const CParamNode minForce = pushingNode.GetChild("MinimalForce");
if (minForce.IsOk())
@@ -108,11 +239,126 @@
m_MovingPushExtension = entity_pos_t::FromInt(5) / 2;
m_StaticPushExtension = entity_pos_t::FromInt(2);
}
+
+ const CParamNode pressureStrength = pushingNode.GetChild("PressureStrength");
+ if (pressureStrength.IsOk())
+ {
+ m_PushingPressureStrength = pressureStrength.ToFixed();
+ if (m_PushingPressureStrength < entity_pos_t::Zero())
+ {
+ LOGWARNING("Pushing pressure strength cannot be below 0. 'pathfinder.xml' should be updated.");
+ m_PushingPressureStrength = entity_pos_t::Zero();
+ }
+ // No upper value, but things won't behave sanely if values are too high.
+ }
+ else
+ m_PushingPressureStrength = entity_pos_t::FromInt(1);
+
+ const CParamNode pushingPressure = pushingNode.GetChild("PressureDecay");
+ if (pushingPressure.IsOk())
+ {
+ m_PushingPressureDecay = Clamp(pushingPressure.ToFixed(), entity_pos_t::Zero(), entity_pos_t::FromInt(1));
+ if (m_PushingPressureDecay != pushingPressure.ToFixed())
+ LOGWARNING("Pushing pressure decay was clamped to the 0-1 range.");
+ }
+ else
+ m_PushingPressureDecay = entity_pos_t::FromInt(6) / 10;
+
+}
+
+template<>
+struct SerializeHelper
+{
+ template
+ void operator()(S& serialize, const char* UNUSED(name), Serialize::qualify value)
+ {
+ Serializer(serialize, "pushing pressure", value.pushingPressure);
+ }
+};
+
+template<>
+struct SerializeHelper>
+{
+ void operator()(ISerializer& serialize, const char* UNUSED(name), EntityMap& value)
+ {
+ // Serialize manually, we don't have a default-constructor for deserialization.
+ Serializer(serialize, "size", static_cast(value.size()));
+ for (EntityMap::iterator it = value.begin(); it != value.end(); ++it)
+ {
+ Serializer(serialize, "ent id", it->first);
+ Serializer(serialize, "state", it->second);
+ }
+ }
+
+ void operator()(IDeserializer& deserialize, const char* UNUSED(name), EntityMap& value)
+ {
+ u32 units = 0;
+ Serializer(deserialize, "size", units);
+ for (u32 i = 0; i < units; ++i)
+ {
+ entity_id_t ent = INVALID_ENTITY;
+ Serializer(deserialize, "ent id", ent);
+ // Insert an invalid motion state, will be cleared up in MT_Deserialized.
+ CCmpUnitMotionManager::MotionState state(nullptr, nullptr);
+ Serializer(deserialize, "state", state);
+ value.insert(ent, state);
+ }
+ }
+};
+
+void CCmpUnitMotionManager::Serialize(ISerializer& serialize)
+{
+ Serializer(serialize, "m_Units", m_Units);
+ Serializer(serialize, "m_FormationControllers", m_FormationControllers);
+}
+
+void CCmpUnitMotionManager::Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
+{
+ Init(paramNode);
+ ResetSubdivisions();
+ Serializer(deserialize, "m_Units", m_Units);
+ Serializer(deserialize, "m_FormationControllers", m_FormationControllers);
+}
+
+/**
+ * This deserialization process is rather ugly, but it's required to store some data in the motion states.
+ * Ideally, the motion state would actually be CCmpUnitMotion themselves, but for data locality
+ * (because our components are stored randomly on the heap right now) they're not.
+ * If we ever change the simulation so that components could be registered by their managers and exposed,
+ * then we could just use CCmpUnitMotion directly and clean this code uglyness.
+ */
+void CCmpUnitMotionManager::OnDeserialized()
+{
+ // Fetch the components now that they exist.
+ // The rest of the data was already deserialized or will be reconstructed.
+ for (EntityMap::iterator it = m_Units.begin(); it != m_Units.end(); ++it)
+ {
+ it->second.cmpPosition = static_cast(QueryInterface(GetSimContext(), it->first, IID_Position));
+ // We can know for a fact that these are CCmpUnitMotion because those are the ones registering with us
+ // (and to ensure that they pass a CCmpUnitMotion pointer when registering).
+ it->second.cmpUnitMotion = static_cast(static_cast(QueryInterface(GetSimContext(), it->first, IID_UnitMotion)));
+ }
+ for (EntityMap::iterator it = m_FormationControllers.begin(); it != m_FormationControllers.end(); ++it)
+ {
+ it->second.cmpPosition = static_cast(QueryInterface(GetSimContext(), it->first, IID_Position));
+ it->second.cmpUnitMotion = static_cast(static_cast(QueryInterface(GetSimContext(), it->first, IID_UnitMotion)));
+ }
+}
+
+void CCmpUnitMotionManager::ResetSubdivisions()
+{
+ CmpPtr cmpTerrain(GetSystemEntity());
+ if (!cmpTerrain)
+ return;
+
+ size_t size = cmpTerrain->GetMapSize();
+ u16 gridSquareSize = static_cast(size / PUSHING_GRID_SIZE + 1);
+ m_MovingUnits.resize(gridSquareSize, gridSquareSize);
}
void CCmpUnitMotionManager::Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController)
{
- MotionState state(CmpPtr(GetSimContext(), ent), component);
+ MotionState state(static_cast(QueryInterface(GetSimContext(), ent, IID_Position)), component);
if (!formationController)
m_Units.insert(ent, state);
else
@@ -153,6 +399,16 @@
void CCmpUnitMotionManager::Move(EntityMap& ents, fixed dt)
{
+#if DEBUG_RENDER
+ debugDataMotionMgr.m_Spheres.clear();
+ debugDataMotionMgr.m_Lines.clear();
+ debugDataMotionMgr.m_Quads.clear();
+#endif
+#if DEBUG_STATS
+ int comparisons = 0;
+ double start = timer_Time();
+#endif
+
PROFILE2("MotionMgr_Move");
std::unordered_set::iterator>*> assigned;
for (EntityMap::iterator it = ents.begin(); it != ents.end(); ++it)
@@ -180,33 +436,108 @@
}
for (std::vector::iterator>* vec : assigned)
+ {
+#if DEBUG_RENDER
+ {
+ SOverlayLine gridL;
+ auto it = (*vec)[0];
+ gridL.PushCoords(CVector3D(it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE * PUSHING_GRID_SIZE,
+ it->second.cmpPosition->GetHeightFixed().ToDouble() + 2.f,
+ it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE * PUSHING_GRID_SIZE));
+ gridL.PushCoords(CVector3D(it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE * PUSHING_GRID_SIZE + PUSHING_GRID_SIZE,
+ it->second.cmpPosition->GetHeightFixed().ToDouble() + 2.f,
+ it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE * PUSHING_GRID_SIZE));
+ gridL.PushCoords(CVector3D(it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE * PUSHING_GRID_SIZE + PUSHING_GRID_SIZE,
+ it->second.cmpPosition->GetHeightFixed().ToDouble() + 2.f,
+ it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE * PUSHING_GRID_SIZE + PUSHING_GRID_SIZE));
+ gridL.PushCoords(CVector3D(it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE * PUSHING_GRID_SIZE,
+ it->second.cmpPosition->GetHeightFixed().ToDouble() + 2.f,
+ it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE * PUSHING_GRID_SIZE + PUSHING_GRID_SIZE));
+ gridL.PushCoords(CVector3D(it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE * PUSHING_GRID_SIZE,
+ it->second.cmpPosition->GetHeightFixed().ToDouble() + 2.f,
+ it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE * PUSHING_GRID_SIZE));
+ gridL.m_Color = CColor(1, 1, 0, 1);
+ debugDataMotionMgr.m_Lines.push_back(gridL);
+ }
+#endif
for (EntityMap::iterator& it : *vec)
+ {
if (it->second.needUpdate)
it->second.cmpUnitMotion->Move(it->second, dt);
+ // Decay pressure after moving so we can get the full 0-MAX_PRESSURE range of values.
+ it->second.pushingPressure = (m_PushingPressureDecay * it->second.pushingPressure).ToInt_RoundToZero();
+ }
+ }
// Skip pushing entirely if the radius is 0
- if (&ents == &m_Units && m_PushingRadius != entity_pos_t::Zero())
+ if (&ents == &m_Units && IsPushingActivated())
{
PROFILE2("MotionMgr_Pushing");
for (std::vector::iterator>* vec : assigned)
{
ENSURE(!vec->empty());
+ std::vector< std::vector::iterator>* > consider = { vec };
+
+ int x = (*vec)[0]->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE;
+ int z = (*vec)[0]->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE;
+ if (x + 1 < m_MovingUnits.width())
+ consider.push_back(&m_MovingUnits.get(x + 1, z));
+ if (x > 0)
+ consider.push_back(&m_MovingUnits.get(x - 1, z));
+ if (z + 1 < m_MovingUnits.height())
+ consider.push_back(&m_MovingUnits.get(x, z + 1));
+ if (z > 0)
+ consider.push_back(&m_MovingUnits.get(x, z - 1));
- std::vector::iterator>::iterator cit1 = vec->begin();
- do
+ for (EntityMap::iterator& it : *vec)
{
- if ((*cit1)->second.ignore)
+ if (it->second.ignore)
continue;
- std::vector::iterator>::iterator cit2 = cit1;
- while(++cit2 != vec->end())
- if (!(*cit2)->second.ignore)
- Push(**cit1, **cit2, dt);
+
+#if DEBUG_RENDER
+ // Plop a sphere at the unit end-pos.
+ {
+ SOverlaySphere sph;
+ sph.m_Center = CVector3D(it->second.pos.X.ToDouble(), it->second.cmpPosition->GetHeightFixed().ToDouble() + 13.f, it->second.pos.Y.ToDouble());
+ sph.m_Radius = it->second.cmpUnitMotion->m_Clearance.Multiply(PUSHING_CORRECTION).ToDouble();
+ // Color the sphere: the redder, the more 'bogged down' it is.
+ sph.m_Color = CColor(it->second.pushingPressure / static_cast(MAX_PRESSURE), 0, 0, 1);
+ debugDataMotionMgr.m_Spheres.push_back(sph);
+ }
+ /* Show the pushing sphere, kinda unreadable.
+ {
+ SOverlaySphere sph;
+ sph.m_Center = CVector3D(it->second.pos.X.ToDouble(), it->second.cmpPosition->GetHeightFixed().ToDouble() + 13.f, it->second.pos.Y.ToDouble());
+ sph.m_Radius = (it->second.cmpUnitMotion->m_Clearance.Multiply(PUSHING_CORRECTION).Multiply(m_PushingRadiusMultiplier) + (it->second.isMoving ? m_StaticPushExtension : m_MovingPushExtension)).ToDouble();
+ // Color the sphere: the redder, the more 'bogged down' it is.
+ sph.m_Color = CColor(it->second.pushingPressure / static_cast(MAX_PRESSURE), 0, 0, 0.1);
+ debugDataMotionMgr.m_Spheres.push_back(sph);
+ }*/
+ // Show the travel over this turn.
+ SOverlayLine line;
+ line.PushCoords(CVector3D(it->second.initialPos.X.ToDouble(),
+ it->second.cmpPosition->GetHeightFixed().ToDouble() + 13.f,
+ it->second.initialPos.Y.ToDouble()));
+ line.PushCoords(CVector3D(it->second.pos.X.ToDouble(),
+ it->second.cmpPosition->GetHeightFixed().ToDouble() + 13.f,
+ it->second.pos.Y.ToDouble()));
+ line.m_Color = CColor(1, 0, 1, 0.5);
+ debugDataMotionMgr.m_Lines.push_back(line);
+#endif
+ for (std::vector::iterator>* vec2 : consider)
+ for (EntityMap::iterator& it2 : *vec2)
+ if (it->first < it2->first && !it2->second.ignore)
+ {
+#if DEBUG_STATS
+ ++comparisons;
+#endif
+ Push(*it, *it2, dt);
+ }
}
- while(++cit1 != vec->end());
}
}
- if (m_PushingRadius != entity_pos_t::Zero())
+ if (IsPushingActivated())
{
PROFILE2("MotionMgr_PushAdjust");
CmpPtr cmpPathfinder(GetSystemEntity());
@@ -218,6 +549,50 @@
if (!it->second.needUpdate || it->second.ignore)
continue;
+#if DEBUG_RENDER
+ SOverlayLine line;
+ line.PushCoords(CVector3D(it->second.pos.X.ToDouble(),
+ it->second.cmpPosition->GetHeightFixed().ToDouble() + 15.1f ,
+ it->second.pos.Y.ToDouble()));
+ line.PushCoords(CVector3D(it->second.pos.X.ToDouble() + it->second.push.X.ToDouble() * 10.f,
+ it->second.cmpPosition->GetHeightFixed().ToDouble() + 15.1f ,
+ it->second.pos.Y.ToDouble() + it->second.push.Y.ToDouble() * 10.f));
+ line.m_Thickness = 0.05f;
+#endif
+
+ // Only apply pushing if the effect is significant enough.
+ if (it->second.push.CompareLength(m_MinimalPushing) <= 0)
+ {
+#if DEBUG_RENDER
+ line.m_Color = CColor(1, 1, 0, 0.6);
+ debugDataMotionMgr.m_Lines.push_back(line);
+#endif
+ it->second.push = CFixedVector2D();
+ continue;
+ }
+
+ // If there was an attempt at movement, and we're getting pushed significantly and
+ // away from where we'd like to go (measured by a low dot product)
+ // then mark the unit as obstructed, but push anyways.
+ // (this helps units stop earlier in many situations in a realistic-ish manner).
+ if (it->second.pos != it->second.initialPos
+ && (it->second.pos - it->second.initialPos).Dot(it->second.pos + it->second.push - it->second.initialPos) < entity_pos_t::FromInt(1)/2 && it->second.pushingPressure > 30)
+ {
+ it->second.wasObstructed = true;
+ it->second.pushingPressure = std::max(MIN_PRESSURE_IF_OBSTRUCTED, it->second.pushingPressure);
+ // Push anyways.
+ }
+#if DEBUG_RENDER
+ if (it->second.wasObstructed)
+ line.m_Color = CColor(1, 0, 0, 1);
+ else
+ line.m_Color = CColor(0, 1, 0, 1);
+ debugDataMotionMgr.m_Lines.push_back(line);
+#endif
+ // Dampen the pushing by the current pushing pressure
+ // (but prevent full dampening so that clumped units still get unclumped).
+ it->second.push = it->second.push * (MAX_PRESSURE - std::min(MAX_PUSH_DAMPING_PRESSURE, it->second.pushingPressure)) / MAX_PRESSURE;
+
// Prevent pushed units from crossing uncrossable boundaries
// (we can assume that normal movement didn't push units into impassable terrain).
if ((it->second.push.X != entity_pos_t::Zero() || it->second.push.Y != entity_pos_t::Zero()) &&
@@ -232,22 +607,9 @@
it->second.wasObstructed = true;
it->second.wentStraight = false;
it->second.push = CFixedVector2D();
+ continue;
}
- // Only apply pushing if the effect is significant enough.
- 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)
- // then mark the unit as obstructed still.
- if (it->second.pos != it->second.initialPos &&
- (it->second.pos - it->second.initialPos).Dot(it->second.pos + it->second.push - it->second.initialPos) < entity_pos_t::FromInt(1)/2)
- {
- it->second.wasObstructed = true;
- it->second.wentStraight = false;
- // Push anyways.
- }
- it->second.pos += it->second.push;
- }
+ it->second.pos += it->second.push;
it->second.push = CFixedVector2D();
}
}
@@ -261,6 +623,14 @@
data.second.cmpUnitMotion->PostMove(data.second, dt);
}
}
+#if DEBUG_STATS
+ int size = 0;
+ for (std::vector::iterator>* vec : assigned)
+ size += vec->size();
+ double time = timer_Time() - start;
+ if (comparisons > 0)
+ printf(">> %i comparisons over %li grids, %f units per grid in %f secs\n", comparisons, assigned.size(), size / (float)(assigned.size()), time);
+#endif
for (std::vector::iterator>* vec : assigned)
vec->clear();
}
@@ -287,44 +657,81 @@
entity_pos_t combinedClearance = (a.second.cmpUnitMotion->m_Clearance + b.second.cmpUnitMotion->m_Clearance).Multiply(PUSHING_CORRECTION);
entity_pos_t maxDist = combinedClearance;
if (!sameControlGroup)
- maxDist = combinedClearance.Multiply(m_PushingRadius) + (movingPush ? m_MovingPushExtension : m_StaticPushExtension);
+ maxDist = combinedClearance.Multiply(m_PushingRadiusMultiplier) + (movingPush ? m_MovingPushExtension : m_StaticPushExtension);
+ combinedClearance = maxDist.Multiply(movingPush ? m_MovingPushingSpread : m_StaticPushingSpread);
- CFixedVector2D offset = a.second.pos - b.second.pos;
+ // Compare the average position of the two units over the turn - this makes overall behaviour better,
+ // as we really care more about units that end up either crossing paths or staying together.
+ CFixedVector2D offset = ((a.second.pos + a.second.initialPos) - (b.second.pos + b.second.initialPos)) / 2;
+
+#if DEBUG_RENDER
+ SOverlayLine line;
+ line.PushCoords(CVector3D(a.second.pos.X.ToDouble(),
+ a.second.cmpPosition->GetHeightFixed().ToDouble() + 8,
+ a.second.pos.Y.ToDouble()));
+ line.PushCoords(CVector3D(b.second.pos.X.ToDouble(),
+ b.second.cmpPosition->GetHeightFixed().ToDouble() + 8,
+ b.second.pos.Y.ToDouble()));
+ if (offset.CompareLength(maxDist) > 0)
+ {
+#if DEBUG_RENDER_ALL_PUSH
+ line.m_Thickness = 0.01f;
+ line.m_Color = CColor(0, 0, 1, 0.4);
+ debugDataMotionMgr.m_Lines.push_back(line);
+ // then will return
+#endif
+ }
+#endif
if (offset.CompareLength(maxDist) > 0)
return;
- entity_pos_t offsetLength = offset.Length();
- // If the offset is small enough that precision would be problematic, pick an arbitrary vector instead.
- if (offsetLength <= entity_pos_t::Epsilon() * 10)
- {
- // Throw in some 'randomness' so that clumped units unclump more naturally.
- bool dir = a.first % 2;
- offset.X = entity_pos_t::FromInt(dir ? 1 : 0);
- offset.Y = entity_pos_t::FromInt(dir ? 0 : 1);
- offsetLength = entity_pos_t::Epsilon() * 10;
+ entity_pos_t offsetLength;
+
+ // If the units appear to have crossed paths, give them a strong perpendicular nudge.
+ // Ideally, this will make them look like they avoided each other.
+ // Worst case, either the collision detection isn't picked up or they'll end up bogged down.
+ // NB: the dot product mostly works because we used average positions earlier.
+ // NB: this kinda works only because our turn lengths are large enough to make this relevant.
+ // In an ideal world, we'd anticipate here instead.
+ // Turn it off for formations - our current 'reforming' code is bad and leads to bad behaviour.
+ if (!sameControlGroup && (a.second.pos - b.second.pos).Dot(a.second.initialPos - b.second.initialPos) < PERPENDICULAR_NUDGE_THRESHOLD)
+ {
+ CFixedVector2D posDelta = (a.second.pos - b.second.pos) - (a.second.initialPos - b.second.initialPos);
+ CFixedVector2D perp = posDelta.Perpendicular();
+ // Pick the best direction to avoid the target.
+ if (offset.Dot(perp) < (-offset).Dot(perp))
+ offset = -perp;
+ else
+ offset = perp;
+ offsetLength = offset.Length();
+ if (offsetLength > entity_pos_t::Epsilon() * 10)
+ {
+ // This needs to be a strong effect or it won't really work.
+ offset.X = offset.X / offsetLength * 3;
+ offset.Y = offset.Y / offsetLength * 3;
+ }
+ offsetLength = entity_pos_t::Zero();
}
else
{
- offset.X = offset.X / offsetLength;
- offset.Y = offset.Y / offsetLength;
- }
-
- // If the units are moving in opposite direction, check if they might have phased through each other.
- // If it looks like yes, move them perpendicularily so it looks like they avoid each other.
- // NB: this isn't very precise, nor will it catch 100% of intersections - it's meant as a cheap improvement.
- if (movingPush && (a.second.pos - a.second.initialPos).Dot(b.second.pos - b.second.initialPos) < entity_pos_t::Zero())
- // Perform some finer checking.
- if (Geometry::TestRayAASquare(a.second.initialPos - b.second.initialPos, a.second.pos - b.second.initialPos,
- CFixedVector2D(combinedClearance, combinedClearance))
- ||
- Geometry::TestRayAASquare(a.second.initialPos - b.second.pos, a.second.pos - b.second.pos,
- CFixedVector2D(combinedClearance, combinedClearance)))
+ offsetLength = offset.Length();
+ // If the offset is small enough that precision would be problematic, pick an arbitrary vector instead.
+ if (offsetLength <= entity_pos_t::Epsilon() * 10)
{
- offset = offset.Perpendicular();
- offsetLength = fixed::Zero();
+ // Throw in some 'randomness' so that clumped units unclump more naturaslly.
+ bool dir = a.first % 2;
+ offset.X = entity_pos_t::FromInt(dir ? 1 : 0);
+ offset.Y = entity_pos_t::FromInt(dir ? 0 : 1);
+ offsetLength = entity_pos_t::Epsilon() * 10;
}
+ else
+ {
+ offset.X = offset.X / offsetLength;
+ offset.Y = offset.Y / offsetLength;
+ }
+ }
- // The pushing distance factor is 1 if the edges are touching, >1 up to MAX if the units overlap, < 1 otherwise.
+ // The pushing distance factor is 1 at the spread-modified combined clearance, >1 up to MAX if the units 'overlap', < 1 otherwise.
entity_pos_t distanceFactor = maxDist - combinedClearance;
// Force units that overlap a lot to have the maximum factor.
if (distanceFactor <= entity_pos_t::Zero() || offsetLength < combinedClearance / 2)
@@ -341,4 +748,35 @@
// Divide by an arbitrary constant to avoid pushing too much.
a.second.push += pushingDir.Multiply(dt / PUSHING_REDUCTION_FACTOR);
b.second.push -= pushingDir.Multiply(dt / PUSHING_REDUCTION_FACTOR);
+
+ // Use a constant factor to get a more general slowdown in crowded area.
+ // The distance factor heavily dampens units that are overlapping.
+ int addedPressure = std::max(0, (PRESSURE_STATIC_FACTOR + (distanceFactor + entity_pos_t::FromInt(-2)/3) * PRESSURE_DISTANCE_FACTOR).Multiply(m_PushingPressureStrength).ToInt_RoundToZero());
+ a.second.pushingPressure = std::min(MAX_PRESSURE, a.second.pushingPressure + addedPressure);
+ b.second.pushingPressure = std::min(MAX_PRESSURE, b.second.pushingPressure + addedPressure);
+
+#if DEBUG_RENDER
+ // Make the lines thicker if the force is stronger.
+ line.m_Thickness = distanceFactor.ToDouble() / 10.0;
+ line.m_Color = CColor(1, addedPressure / 20.f, 0, 0.8);
+ debugDataMotionMgr.m_Lines.push_back(line);
+#endif
}
+
+#if DEBUG_RENDER
+void RenderDebugOverlay(SceneCollector& collector, const CFrustum& frustum, bool UNUSED(culling))
+{
+ for (SOverlaySphere& sph: debugDataMotionMgr.m_Spheres)
+ if (frustum.IsSphereVisible(sph.m_Center, sph.m_Radius))
+ collector.Submit(&sph);
+ for (SOverlayLine& l: debugDataMotionMgr.m_Lines)
+ if (frustum.IsPointVisible(l.m_Coords[0]) || frustum.IsPointVisible(l.m_Coords[1]))
+ collector.Submit(&l);
+ for (SOverlayQuad& quad: debugDataMotionMgr.m_Quads)
+ collector.Submit(&quad);
+}
+#endif
+
+#undef DEBUG_STATS
+#undef DEBUG_RENDER
+#undef DEBUG_RENDER_ALL_PUSH
Index: ps/trunk/source/simulation2/system/ComponentManager.cpp
===================================================================
--- ps/trunk/source/simulation2/system/ComponentManager.cpp
+++ ps/trunk/source/simulation2/system/ComponentManager.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -728,6 +728,7 @@
}
// Construct the new component
+ // NB: The unit motion manager relies on components not moving in memory once constructed.
IComponent* component = ct.alloc(m_ScriptInterface, obj);
ENSURE(component);