Index: binaries/data/mods/public/maps/scenarios/unit_pushing_test.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/maps/scenarios/unit_pushing_test.js
@@ -0,0 +1,209 @@
+const REG_UNIT_TEMPLATE = "units/athen/infantry_spearman_b";
+const FAST_UNIT_TEMPLATE = "units/athen/cavalry_swordsman_b";
+
+const ATTACKER = 2;
+
+var QuickSpawn = function(x, z, template, owner = 1)
+{
+ let ent = Engine.AddEntity(template);
+
+ let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ if (cmpEntOwnership)
+ cmpEntOwnership.SetOwner(owner);
+
+ let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
+ cmpEntPosition.JumpTo(x, z);
+ return ent;
+};
+
+var Rotate = function(angle, ent)
+{
+ let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
+ cmpEntPosition.SetYRotation(angle);
+ return ent;
+};
+
+var WalkTo = function(x, z, queued, ent, owner=1)
+{
+ ProcessCommand(owner, {
+ "type": "walk",
+ "entities": Array.isArray(ent) ? ent : [ent],
+ "x": x,
+ "z": z,
+ "queued": queued,
+ "force": true,
+ });
+ return ent;
+};
+
+var FormationWalkTo = function(x, z, queued, ent, owner=1)
+{
+ ProcessCommand(owner, {
+ "type": "walk",
+ "entities": Array.isArray(ent) ? ent : [ent],
+ "x": x,
+ "z": z,
+ "queued": queued,
+ "force": true,
+ "formation": "special/formations/box"
+ });
+ return ent;
+};
+
+var Attack = function(target, ent)
+{
+ let comm = {
+ "type": "attack",
+ "entities": Array.isArray(ent) ? ent : [ent],
+ "target": target,
+ "queued": true,
+ "force": true,
+ };
+ ProcessCommand(ATTACKER, comm);
+ return ent;
+};
+
+
+var gx;
+var gy;
+var experiments = {};
+
+// Perf check: put units everywhere, not moving.
+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)
+ QuickSpawn(x, z, REG_UNIT_TEMPLATE);
+ }
+};
+
+// Perf check: put units everywhere, moving.
+experiments.MovingAround = {
+ "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)
+ {
+ let ent = QuickSpawn(x, z, REG_UNIT_TEMPLATE);
+ for (let i = 0; i < 5; ++i)
+ {
+ WalkTo(x + 4, z, true, ent);
+ WalkTo(x + 4, z + 4, true, ent);
+ WalkTo(x, z + 4, true, ent);
+ WalkTo(x, z, true, ent);
+ }
+ }
+ }
+};
+// Perf check: fewer units moving more.
+experiments.LighterMovingAround = {
+ "spawn": () => {
+ const spacing = 36;
+ for (let x = 0; x < 20*16*4 - 20; x += spacing)
+ for (let z = 0; z < 20*16*4 - 20; z += spacing)
+ {
+ let ent = QuickSpawn(x, z, REG_UNIT_TEMPLATE);
+ for (let i = 0; i < 5; ++i)
+ {
+ WalkTo(x + 20, z, true, ent);
+ WalkTo(x + 20, z + 20, true, ent);
+ WalkTo(x, z + 20, true, ent);
+ WalkTo(x, z, true, ent);
+ }
+ }
+ }
+};
+
+// Perf check: rows of units crossing each other.
+experiments.BunchaCollisions = {
+ "spawn": () => {
+ const spacing = 36;
+ for (let x = 0; x < 20*16*4 - 20; x += spacing)
+ for (let z = 0; z < 20*16*4 - 20; z += spacing)
+ {
+ for (let i = 0; i < 10; ++i)
+ {
+ let ent = QuickSpawn(x + i, z + 20 * (i%2), REG_UNIT_TEMPLATE);
+ for (let ii = 0; ii < 5; ++ii)
+ {
+ WalkTo(x + i, z + 20, true, ent);
+ WalkTo(x + i, z, true, ent);
+ }
+ }
+ }
+ }
+};
+
+// Massive moshpit of pushing.
+experiments.LotsaLocalCollisions = {
+ "spawn": () => {
+ const spacing = 4;
+ for (let x = 100; x < 200; x += spacing)
+ for (let z = 100; z < 200; z += spacing)
+ {
+ let ent = QuickSpawn(x, z, REG_UNIT_TEMPLATE);
+ for (let ii = 0; ii < 20; ++ii)
+ WalkTo(randFloat(100, 200), randFloat(100, 200), true, ent);
+ }
+ }
+};
+
+var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
+
+Trigger.prototype.Setup = function()
+{
+ let start = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
+ gx = 20;
+ gy = 20;
+
+ let time = 0;
+ for (let key in experiments)
+ {
+ cmpTrigger.DoAfterDelay(1000 + time * 10000, "RunExperiment", { "exp": key });
+ time++;
+ }
+};
+
+Trigger.prototype.Cleanup = function()
+{
+ warn("cleanup");
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ let ents = cmpRangeManager.GetEntitiesByPlayer(1).concat(cmpRangeManager.GetEntitiesByPlayer(2));
+ for (let ent of ents)
+ Engine.DestroyEntity(ent);
+};
+
+Trigger.prototype.RunExperiment = function(data)
+{
+ warn("Start of " + data.exp);
+ experiments[data.exp].spawn();
+ cmpTrigger.DoAfterDelay(9500, "Cleanup", {});
+};
+
+Trigger.prototype.EndGame = function()
+{
+ Engine.QueryInterface(4, IID_Player).SetState("defeated", "trigger");
+ Engine.QueryInterface(3, IID_Player).SetState("won", "trigger");
+};
+
+/*
+var cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
+
+// Reduce player 1 vision range (or patrolling units reacct)
+cmpModifiersManager.AddModifiers("no_promotion", {
+ "Vision/Range": [{ "affects": ["Unit"], "replace": 5 }],
+}, 3); // player 1 is ent 3
+
+// Prevent promotions, messes up things.
+cmpModifiersManager.AddModifiers("no_promotion_A", {
+ "Promotion/RequiredXp": [{ "affects": ["Unit"], "replace": 50000 }],
+}, 3);
+cmpModifiersManager.AddModifiers("no_promotion_B", {
+ "Promotion/RequiredXp": [{ "affects": ["Unit"], "replace": 50000 }],
+}, 4); // player 2 is ent 4
+*/
+
+cmpTrigger.DoAfterDelay(1000, "Setup", {});
+
+cmpTrigger.DoAfterDelay(300000, "EndGame", {});
Index: binaries/data/mods/public/maps/scenarios/unit_pushing_test.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/maps/scenarios/unit_pushing_test.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+ default
+
+
+
+
+
+ 0
+ 0.5
+
+
+
+
+ ocean
+
+
+ 5
+ 4
+ 0.45
+ 0
+
+
+
+ 0
+ 1
+ 0.99
+ 0.1999
+ default
+
+
+
+
+
+
+
+
+
+
+
+
Index: source/main.cpp
===================================================================
--- source/main.cpp
+++ source/main.cpp
@@ -475,7 +475,10 @@
g_Profiler.Frame();
if (g_Game->IsGameFinished())
+ {
+ g_Profiler2.SaveToFile();
QuitEngine();
+ }
}
static void MainControllerInit()
Index: source/simulation2/components/CCmpObstructionManager.cpp
===================================================================
--- source/simulation2/components/CCmpObstructionManager.cpp
+++ source/simulation2/components/CCmpObstructionManager.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020 Wildfire Games.
+/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -481,6 +481,7 @@
virtual bool AreShapesInRange(const ObstructionSquare& source, const ObstructionSquare& target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const;
virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits = false) const;
+ virtual bool TestStaticLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r) const;
virtual bool TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, std::vector* out) const;
virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, std::vector* out) const;
@@ -907,6 +908,36 @@
return false;
}
+bool CCmpObstructionManager::TestStaticLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r) const
+{
+ PROFILE("TestStaticLine");
+
+ // Check that both end points are within the world (which means the whole line must be)
+ if (!IsInWorld(x0, z0, r) || !IsInWorld(x1, z1, r))
+ return true;
+
+ CFixedVector2D posMin (std::min(x0, x1) - r, std::min(z0, z1) - r);
+ CFixedVector2D posMax (std::max(x0, x1) + r, std::max(z0, z1) + r);
+
+ std::vector staticShapes;
+ m_StaticSubdivision.GetInRange(staticShapes, posMin, posMax);
+ for (const entity_id_t& shape : staticShapes)
+ {
+ std::map::const_iterator it = m_StaticShapes.find(shape);
+ ENSURE(it != m_StaticShapes.end());
+
+ if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
+ continue;
+
+ CFixedVector2D center(it->second.x, it->second.z);
+ CFixedVector2D halfSize(it->second.hw + r, it->second.hh + r);
+ if (Geometry::TestRaySquare(CFixedVector2D(x0, z0) - center, CFixedVector2D(x1, z1) - center, it->second.u, it->second.v, halfSize))
+ return true;
+ }
+
+ return false;
+}
+
bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filter,
entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h,
std::vector* out) const
Index: source/simulation2/components/CCmpPathfinder.cpp
===================================================================
--- source/simulation2/components/CCmpPathfinder.cpp
+++ source/simulation2/components/CCmpPathfinder.cpp
@@ -893,15 +893,14 @@
bool CCmpPathfinder::CheckMovement(const IObstructionTestFilter& filter,
entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r,
- pass_class_t passClass) const
+ pass_class_t passClass, bool checkUnits) const
{
PROFILE2_IFSPIKE("Check Movement", 0.001);
// Test against obstructions first. filter may discard pathfinding-blocking obstructions.
- // Use more permissive version of TestLine to allow unit-unit collisions to overlap slightly.
- // This makes movement smoother and more natural for units, overall.
CmpPtr cmpObstructionManager(GetSystemEntity());
- if (!cmpObstructionManager || cmpObstructionManager->TestLine(filter, x0, z0, x1, z1, r, true))
+ if (!cmpObstructionManager ||
+ (checkUnits ? cmpObstructionManager->TestLine(filter, x0, z0, x1, z1, r, true) : cmpObstructionManager->TestStaticLine(filter, x0, z0, x1, z1, r)))
return false;
// Then test against the terrain grid. This should not be necessary
Index: source/simulation2/components/CCmpPathfinder_Common.h
===================================================================
--- source/simulation2/components/CCmpPathfinder_Common.h
+++ source/simulation2/components/CCmpPathfinder_Common.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020 Wildfire Games.
+/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -214,7 +214,7 @@
virtual void SetAtlasOverlay(bool enable, pass_class_t passClass = 0);
- virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass) const;
+ virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass, bool checkUnits) const;
virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass, bool onlyCenterPoint) const;
Index: source/simulation2/components/CCmpUnitMotion.h
===================================================================
--- source/simulation2/components/CCmpUnitMotion.h
+++ source/simulation2/components/CCmpUnitMotion.h
@@ -948,9 +948,12 @@
else
{
// Update the Position component after our movement (if we actually moved anywhere)
- // When moving always set the angle in the direction of the movement.
CFixedVector2D offset = state.pos - state.initialPos;
- state.angle = atan2_approx(offset.X, offset.Y);
+ // 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.
@@ -1334,12 +1337,13 @@
specificIgnore = cmpTargetObstruction->GetObstruction();
}
+ // Check movement against units - we want to use the short pathfinder to walk around those if needed.
if (specificIgnore.valid())
{
- if (!cmpPathfinder->CheckMovement(SkipTagObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
+ if (!cmpPathfinder->CheckMovement(GetObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass, true))
return false;
}
- else if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
+ else if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass, true))
return false;
Index: source/simulation2/components/CCmpUnitMotionManager.h
===================================================================
--- source/simulation2/components/CCmpUnitMotionManager.h
+++ source/simulation2/components/CCmpUnitMotionManager.h
@@ -22,6 +22,8 @@
#include "ICmpUnitMotionManager.h"
#include "simulation2/MessageTypes.h"
+#include "simulation2/components/ICmpTerrain.h"
+#include "simulation2/helpers/Grid.h"
#include "simulation2/system/EntityMap.h"
class CCmpUnitMotion;
@@ -31,6 +33,7 @@
public:
static void ClassInit(CComponentManager& componentManager)
{
+ componentManager.SubscribeToMessageType(MT_TerrainChanged);
componentManager.SubscribeToMessageType(MT_TurnStart);
componentManager.SubscribeToMessageType(MT_Update_Final);
componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
@@ -52,22 +55,29 @@
// Transient position during the movement.
CFixedVector2D pos;
+ // Accumulated "pushing" from nearby units.
+ CFixedVector2D push;
+
fixed initialAngle;
fixed angle;
// If true, the entity needs to be handled during movement.
bool needUpdate;
- // 'Leak' from UnitMotion.
bool wentStraight;
bool wasObstructed;
+
+ // Marks a unit to be ignored for collisions.
+ // This is reset every turn, and is needed because units can otherwise end up inside each other,
+ // then being unable to move entirely.
+ bool tempIgnore;
};
EntityMap m_Units;
EntityMap m_FormationControllers;
- // Temporary vector, reconstructed each turn (stored here to avoid memory reallocations).
- std::vector::iterator> m_MovingUnits;
+ // The vectors are cleared each frame.
+ Grid::iterator>> m_MovingUnits;
bool m_ComputingMotion;
@@ -78,7 +88,6 @@
virtual void Init(const CParamNode& UNUSED(paramNode))
{
- m_MovingUnits.reserve(40);
}
virtual void Deinit()
@@ -92,12 +101,20 @@
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();
@@ -130,13 +147,27 @@
return m_ComputingMotion;
}
+private:
+ void ResetSubdivisions();
void OnTurnStart();
void MoveUnits(fixed dt);
void MoveFormations(fixed dt);
void Move(EntityMap& ents, fixed dt);
+
+ void Push(EntityMap::value_type& a, EntityMap::value_type& b);
};
+void CCmpUnitMotionManager::ResetSubdivisions()
+{
+ CmpPtr cmpTerrain(GetSystemEntity());
+ if (!cmpTerrain)
+ return;
+
+ size_t size = cmpTerrain->GetVerticesPerSide() - 1;
+ m_MovingUnits.resize(size * TERRAIN_TILE_SIZE / 20, size * TERRAIN_TILE_SIZE / 20);
+}
+
REGISTER_COMPONENT_TYPE(UnitMotionManager)
#endif // INCLUDED_CCMPUNITMOTIONMANAGER
Index: source/simulation2/components/CCmpUnitMotion_System.cpp
===================================================================
--- source/simulation2/components/CCmpUnitMotion_System.cpp
+++ source/simulation2/components/CCmpUnitMotion_System.cpp
@@ -35,6 +35,7 @@
component,
CFixedVector2D(),
CFixedVector2D(),
+ CFixedVector2D(fixed::FromInt(0), fixed::FromInt(0)),
fixed::Zero(),
fixed::Zero(),
false,
@@ -80,22 +81,166 @@
void CCmpUnitMotionManager::Move(EntityMap& ents, fixed dt)
{
- m_MovingUnits.clear();
+ PROFILE2("MotionMgr_Move");
+ std::set::iterator>*> assigned;
for (EntityMap::iterator it = ents.begin(); it != ents.end(); ++it)
{
- it->second.cmpUnitMotion->PreMove(it->second);
- if (!it->second.needUpdate)
+ it->second.tempIgnore = false;
+ if (!it->second.cmpPosition->IsInWorld())
+ {
+ it->second.needUpdate = false;
continue;
- m_MovingUnits.push_back(it);
+ }
+ else
+ it->second.cmpUnitMotion->PreMove(it->second);
it->second.initialPos = it->second.cmpPosition->GetPosition2D();
it->second.initialAngle = it->second.cmpPosition->GetRotation().Y;
it->second.pos = it->second.initialPos;
it->second.angle = it->second.initialAngle;
+ std::vector::iterator>& subdiv = m_MovingUnits.get(it->second.pos.X.ToInt_RoundToZero() / 20, it->second.pos.Y.ToInt_RoundToZero() / 20);
+ subdiv.emplace_back(it);
+ assigned.emplace(&subdiv);
}
- for (EntityMap::iterator& it : m_MovingUnits)
- it->second.cmpUnitMotion->Move(it->second, dt);
+ for (std::vector::iterator>* vec : assigned)
+ for (EntityMap::iterator& it : *vec)
+ if (it->second.needUpdate)
+ it->second.cmpUnitMotion->Move(it->second, dt);
- for (EntityMap::iterator& it : m_MovingUnits)
- it->second.cmpUnitMotion->PostMove(it->second, dt);
+ if (&ents == &m_Units)
+ {
+ PROFILE2("MotionMgr_Pushing");
+ for (std::vector::iterator>* vec : assigned)
+ {
+ ENSURE(!vec->empty());
+
+ std::vector::iterator>::iterator cit1 = vec->begin();
+ do
+ {
+ std::vector::iterator>::iterator cit2 = cit1;
+ while(++cit2 != vec->end())
+ Push(**cit1, **cit2);
+ }
+ while(++cit1 != vec->end());
+ }
+ }
+
+ {
+ PROFILE2("MotionMgr_PushAdjust");
+ CmpPtr cmpPathfinder(GetSystemEntity());
+ for (std::vector::iterator>* vec : assigned)
+ {
+ for (EntityMap::iterator& it : *vec)
+ {
+
+ if (!it->second.needUpdate)
+ continue;
+
+ // Prevent pushed units from crossing static 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()) &&
+ !cmpPathfinder->CheckMovement(it->second.cmpUnitMotion->GetObstructionFilter(),
+ it->second.pos.X, it->second.pos.Y,
+ it->second.pos.X + it->second.push.X, it->second.pos.Y + it->second.push.Y,
+ it->second.cmpUnitMotion->m_Clearance, it->second.cmpUnitMotion->m_PassClass))
+ {
+ // Mark them as obstructed - this could possibly be optimised
+ // perhaps it'd make more sense to mark the pushers as blocked.
+ it->second.wasObstructed = true;
+ it->second.wentStraight = false;
+ it->second.push = CFixedVector2D();
+ }
+ // At this point, check if we still have significant path collisions.
+ for (EntityMap::iterator& otherIt : *vec)
+ {
+ if (it->first == otherIt->first || otherIt->second.tempIgnore)
+ continue;
+ entity_pos_t dist = (it->second.cmpUnitMotion->m_Clearance + otherIt->second.cmpUnitMotion->m_Clearance) / 2;
+ if ((it->second.pos + it->second.push - otherIt->second.pos - otherIt->second.push).CompareLength(dist) > 0)
+ continue;
+ LOGWARNING("blocked %i", it->first);
+ // Block the current entity. This has cascading effects which aren't accounted for,
+ // the assumption being that the original position was valid, so this seems OK.
+ it->second.wasObstructed = true;
+ it->second.wentStraight = false;
+ // To prevent this entity from blocking the other entity (which can deadlock them), ignore it then.
+ it->second.tempIgnore = true;
+ it->second.push = CFixedVector2D();
+ it->second.pos = (it->second.initialPos + it->second.pos) / 2;
+ break;
+ }
+ // Beyond some level of pushing, consider the unit to be obstructed and don't move it so much
+ if (it->second.push.CompareLength(fixed::FromInt(2)) >= 0)
+ {
+ LOGWARNING("Too pushed %i", it->first);
+ it->second.wasObstructed = true;
+ it->second.wentStraight = false;
+ it->second.pos = it->second.initialPos + (it->second.pos - it->second.initialPos) / 2 + it->second.push / 2;
+ }
+ else
+ it->second.pos += it->second.push;
+ it->second.push = CFixedVector2D();
+ }
+ }
+ }
+ {
+ PROFILE2("MotionMgr_PostMove");
+ for (EntityMap::value_type& data : ents)
+ {
+ if (!data.second.needUpdate)
+ continue;
+ data.second.cmpUnitMotion->PostMove(data.second, dt);
+ }
+ }
+ for (std::vector::iterator>* vec : assigned)
+ vec->clear();
+}
+
+void CCmpUnitMotionManager::Push(EntityMap::value_type& a, EntityMap::value_type& b)
+{
+ CFixedVector2D offset = a.second.pos - b.second.pos;
+ int movingPush = a.second.cmpUnitMotion->IsMoveRequested() + b.second.cmpUnitMotion->IsMoveRequested();
+ entity_pos_t combinedClearance = (a.second.cmpUnitMotion->m_Clearance + b.second.cmpUnitMotion->m_Clearance) / 2;
+ // Distance reduces the pushing forces. Movement increases it.
+ entity_pos_t maxDist = combinedClearance * (3 + movingPush) / 2;
+ 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)
+ {
+ offset.X = entity_pos_t::FromInt(1);
+ offset.Y = entity_pos_t::FromInt(0);
+ offsetLength = entity_pos_t::FromInt(1);
+ }
+ else
+ {
+ offset.X = offset.X / offsetLength;
+ offset.Y = offset.Y / offsetLength;
+ }
+
+ // TODO: ought to simulate in-flight pushing, e.g. perpendicular effect for units that cross each other's paths.
+ entity_pos_t distanceFactor = maxDist - offsetLength;
+ if (distanceFactor < fixed::FromInt(1)/10)
+ return;
+
+ a.second.needUpdate = true;
+ b.second.needUpdate = true;
+
+ offset = offset.Multiply(distanceFactor);
+ int factA = 5;
+ int factB = 5;
+ if (a.second.cmpUnitMotion->IsMoveRequested() && !b.second.cmpUnitMotion->IsMoveRequested())
+ {
+ factA = 8;
+ factB = 2;
+ }
+ else if (!a.second.cmpUnitMotion->IsMoveRequested() && b.second.cmpUnitMotion->IsMoveRequested())
+ {
+ factA = 2;
+ factB = 8;
+ }
+ a.second.push += offset * factA / 10;
+ b.second.push -= offset * factB / 10;
}
Index: source/simulation2/components/ICmpObstructionManager.h
===================================================================
--- source/simulation2/components/ICmpObstructionManager.h
+++ source/simulation2/components/ICmpObstructionManager.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020 Wildfire Games.
+/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -244,6 +244,12 @@
* @return true if there is a collision
*/
virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits) const = 0;
+ /**
+ * Collision test a flat-ended thick line against the current set of static shapes.
+ * @see TestLine
+ * @return true if there is a collision
+ */
+ virtual bool TestStaticLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r) const = 0;
/**
* Collision test a static square shape against the current set of shapes.
Index: source/simulation2/components/ICmpPathfinder.h
===================================================================
--- source/simulation2/components/ICmpPathfinder.h
+++ source/simulation2/components/ICmpPathfinder.h
@@ -143,7 +143,7 @@
* or impassable terrain.
* Returns true if the movement is okay.
*/
- virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass) const = 0;
+ virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass, bool checkUnits = false) const = 0;
/**
* Check whether a unit placed here is valid and doesn't hit any obstructions
Index: source/simulation2/helpers/LongPathfinder.h
===================================================================
--- source/simulation2/helpers/LongPathfinder.h
+++ source/simulation2/helpers/LongPathfinder.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020 Wildfire Games.
+/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -260,9 +260,7 @@
/**
* Given a path with an arbitrary collection of waypoints, updates the
- * waypoints to be nicer. Calls "Testline" between waypoints
- * so that bended paths can become straight if there's nothing in between
- * (this happens because A* is 8-direction, and the map isn't actually a grid).
+ * waypoints to be nicer.
* If @param maxDist is non-zero, path waypoints will be espaced by at most @param maxDist.
* In that case the distance between (x0, z0) and the first waypoint will also be made less than maxDist.
*/