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. */