Index: binaries/data/mods/public/maps/scenarios/unit_pushing_test.js =================================================================== --- binaries/data/mods/public/maps/scenarios/unit_pushing_test.js +++ binaries/data/mods/public/maps/scenarios/unit_pushing_test.js @@ -170,6 +170,8 @@ "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); } }; @@ -184,6 +186,29 @@ } }; +// Same as above but not as aligned. +experiments.multicrossing_spaced = { + "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 + 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)); + } +}; + experiments.elephant_formation = { "spawn": (gx, gy) => { let ents = []; @@ -318,7 +343,7 @@ for (let key in experiments) { experiments[key].spawn(gx, gy); - gx += 60; + gx += 90; if (gx > 20*16*4-20) { gx = 20; Index: binaries/data/mods/public/simulation/data/pathfinder.rng =================================================================== --- binaries/data/mods/public/simulation/data/pathfinder.rng +++ binaries/data/mods/public/simulation/data/pathfinder.rng @@ -6,6 +6,9 @@ + + + @@ -18,6 +21,12 @@ + + + + + + Index: binaries/data/mods/public/simulation/data/pathfinder.xml =================================================================== --- binaries/data/mods/public/simulation/data/pathfinder.xml +++ binaries/data/mods/public/simulation/data/pathfinder.xml @@ -9,18 +9,26 @@ - 1.6 + 1.4 - - - 2 + + 1.7 - - 2.5 + 2.3 + + + + + + + + + + 0.8 @@ -29,6 +37,13 @@ 0.2 + + + + + 0.8 + + 0.55 Index: source/simulation2/components/CCmpUnitMotion.h =================================================================== --- source/simulation2/components/CCmpUnitMotion.h +++ 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 @@ -424,8 +424,6 @@ case MT_Deserialized: { OnValueModification(); - if (!ENTITY_IS_LOCAL(GetEntityId())) - CmpPtr(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController); break; } } @@ -483,7 +481,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 +751,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 +1047,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) @@ -1126,7 +1124,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 +1136,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 +1144,25 @@ 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. + constexpr int pressureMinThreshold = 10; + + // Leave some leeway to avoid slowing down too much over slight pushes, + // while also making sure that units don't completely stop, + // which would make movement un-necessarily awkward. + constexpr int maxPressure = std::numeric_limits::max() - pressureMinThreshold; + + // Acts as the floor speed: (maxPressure - this) / maxPressure + constexpr int pressureMaxThreshold = maxPressure - 20; + static_assert(pressureMaxThreshold > 0); + + uint8_t slowdown = maxPressure - std::min(pressureMaxThreshold, std::max(0, pushingPressure - pressureMinThreshold)); + basicSpeed = basicSpeed.Multiply(fixed::FromInt(slowdown) / maxPressure); + } + // 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). Index: source/simulation2/components/CCmpUnitMotionManager.h =================================================================== --- source/simulation2/components/CCmpUnitMotionManager.h +++ 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,21 @@ 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) // 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 +65,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 +86,24 @@ // "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_PushingSpread; + // 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 +125,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 +140,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 +155,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: source/simulation2/components/CCmpUnitMotion_System.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion_System.cpp +++ 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 @@ -26,6 +26,12 @@ #include +#include "maths/Frustum.h" + +#define DEBUG_STATS 0 +#define DEBUG_RENDER 0 +#define DEBUG_RENDER_FOR_PERF 0 + // NB: this TU contains the CCmpUnitMotion/CCmpUnitMotionManager couple. // In practice, UnitMotionManager functions need access to the full implementation of UnitMotion, // but UnitMotion needs access to MotionState (defined in UnitMotionManager). @@ -33,8 +39,12 @@ namespace { /** - * Units push only within their own grid square. This is the size of each square (in arbitrary units). - * TODO: check other values. + * 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. */ static const int PUSHING_GRID_SIZE = 20; @@ -51,7 +61,7 @@ static const int PUSHING_REDUCTION_FACTOR = 2; /** - * Maximum distance multiplier. + * 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. @@ -59,11 +69,81 @@ static const entity_pos_t MAX_DISTANCE_FACTOR = entity_pos_t::FromInt(5) / 2; } -CCmpUnitMotionManager::MotionState::MotionState(CmpPtr cmpPos, CCmpUnitMotion* cmpMotion) +#if DEBUG_RENDER +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) { } +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 +156,29 @@ // 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("Spread"); + if (spread.IsOk()) + { + m_PushingSpread = Clamp(spread.ToFixed(), entity_pos_t::Zero(), entity_pos_t::FromInt(1)); + if (m_PushingSpread != spread.ToFixed()) + LOGWARNING("Pushing spread was clamped to the 0-1 range."); + } + else + m_PushingSpread = 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 +198,126 @@ m_MovingPushExtension = entity_pos_t::FromInt(5) / 2; m_StaticPushExtension = entity_pos_t::FromInt(2); } + + const CParamNode pressureStrength = pushingNode.GetChild("PushingPressureStrength"); + 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("PushingPressureDecay"); + 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 +358,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 +395,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-255 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 }; - std::vector::iterator>::iterator cit1 = vec->begin(); - do + 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)); + + 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 / 255.f, 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 / 255.f, 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 +508,28 @@ 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; + } + // 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 +544,29 @@ 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 we're getting pushed significantly and + // away from where we'd like to go(measured by a low dot product and) + // then mark the unit as obstructed still. + else 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 > 100) { - // 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.wasObstructed = true; + it->second.wentStraight = false; + // 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.pos += it->second.push * (255 - std::min(150, it->second.pushingPressure)) / 255; it->second.push = CFixedVector2D(); } } @@ -261,6 +580,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 +614,74 @@ 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(m_PushingSpread); + } - 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 && DEBUG_RENDER_FOR_PERF + 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) + { + line.m_Thickness = 0.01f; + line.m_Color = CColor(0, 0, 1, 0.4); + debugDataMotionMgr.m_Lines.push_back(line); + // then will return + } +#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) + 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. + if ((a.second.pos - b.second.pos).Dot(a.second.initialPos - b.second.initialPos) < entity_pos_t::FromInt(-1)/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; + offset = ((a.second.pos - b.second.pos) - (a.second.initialPos - b.second.initialPos)).Perpendicular(); + 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; + 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 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; + } } - // 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))) - { - offset = offset.Perpendicular(); - offsetLength = fixed::Zero(); - } - - // 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) @@ -336,9 +693,40 @@ a.second.needUpdate = true; b.second.needUpdate = true; +#if DEBUG_RENDER && DEBUG_RENDER_FOR_PERF + // Make the lines thicker if the force is stronger. + line.m_Thickness = distanceFactor.ToDouble() / 10.0; + line.m_Color = CColor(1, 0, 0, 0.8); + debugDataMotionMgr.m_Lines.push_back(line); +#endif + CFixedVector2D pushingDir = offset.Multiply(distanceFactor); // 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 = (entity_pos_t::FromInt(2) + distanceFactor * 3).Multiply(m_PushingPressureStrength).ToInt_RoundToZero(); + a.second.pushingPressure = std::min(255, a.second.pushingPressure + addedPressure); + b.second.pushingPressure = std::min(255, b.second.pushingPressure + addedPressure); } + +#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_FOR_PERF Index: source/simulation2/system/ComponentManager.cpp =================================================================== --- source/simulation2/system/ComponentManager.cpp +++ 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);