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