/* 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 * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "CCmpUnitMotion.h" #include "CCmpUnitMotionManager.h" #include "maths/MathUtil.h" #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, // but UnitMotion needs access to MotionState (defined in UnitMotionManager). // To avoid inclusion issues, implementation of UnitMotionManager that uses UnitMotion is here. namespace { /** * 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); /** * Maximum pushing multiplier for a single push calculation. * This exists for numerical stability of the system between a lightweight and a heavy unit. */ constexpr int MAX_PUSHING_MULTIPLIER = 4; /** * 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; } #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. // This assumes the pathfinder component is initialised first and registers the validator. // TODO: there seems to be no real reason why we could not register a 'system' entity somewhere instead. CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml", "pathfinder"); CParamNode pushingNode = externalParamNode.GetChild("Pathfinder").GetChild("Pushing"); // 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_PushingRadiusMultiplier = radius.ToFixed(); if (m_PushingRadiusMultiplier < 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_PushingRadiusMultiplier = entity_pos_t::FromInt(8) / 5; const CParamNode minForce = pushingNode.GetChild("MinimalForce"); if (minForce.IsOk()) m_MinimalPushing = minForce.ToFixed(); else m_MinimalPushing = entity_pos_t::FromInt(2) / 10; const CParamNode movingExt = pushingNode.GetChild("MovingExtension"); const CParamNode staticExt = pushingNode.GetChild("StaticExtension"); if (movingExt.IsOk() && staticExt.IsOk()) { m_MovingPushExtension = movingExt.ToFixed(); m_StaticPushExtension = staticExt.ToFixed(); } else { 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(static_cast(QueryInterface(GetSimContext(), ent, IID_Position)), component); if (!formationController) m_Units.insert(ent, state); else m_FormationControllers.insert(ent, state); } void CCmpUnitMotionManager::Unregister(entity_id_t ent) { EntityMap::iterator it = m_Units.find(ent); if (it != m_Units.end()) { m_Units.erase(it); return; } it = m_FormationControllers.find(ent); if (it != m_FormationControllers.end()) m_FormationControllers.erase(it); } void CCmpUnitMotionManager::OnTurnStart() { for (EntityMap::value_type& data : m_FormationControllers) data.second.cmpUnitMotion->OnTurnStart(); for (EntityMap::value_type& data : m_Units) data.second.cmpUnitMotion->OnTurnStart(); } void CCmpUnitMotionManager::MoveUnits(fixed dt) { Move(m_Units, dt); } void CCmpUnitMotionManager::MoveFormations(fixed dt) { Move(m_FormationControllers, dt); } 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) { if (!it->second.cmpPosition->IsInWorld()) { it->second.needUpdate = false; continue; } 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.speed = it->second.cmpUnitMotion->GetCurrentSpeed(); it->second.angle = it->second.initialAngle; ENSURE(it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE < m_MovingUnits.width() && it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE < m_MovingUnits.height()); std::vector::iterator>& subdiv = m_MovingUnits.get( it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE, it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE ); subdiv.emplace_back(it); assigned.emplace(&subdiv); } 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 && 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)); for (EntityMap::iterator& it : *vec) { if (it->second.ignore) continue; #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); } } } } if (IsPushingActivated()) { PROFILE2("MotionMgr_PushAdjust"); CmpPtr cmpPathfinder(GetSystemEntity()); for (std::vector::iterator>* vec : assigned) { for (EntityMap::iterator& it : *vec) { 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()) && !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(); continue; } 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); } } #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(); } // TODO: ought to better simulate in-flight pushing, e.g. if units would cross in-between turns. void CCmpUnitMotionManager::Push(EntityMap::value_type& a, EntityMap::value_type& b, fixed dt) { // The hard problem for pushing is knowing when to actually use the pathfinder to go around unpushable obstacles. // For simplicitly, the current logic separates moving & stopped entities: // moving entities will push moving entities, but not stopped ones, and vice-versa. // this still delivers most of the value of pushing, without a lot of the complexity. int movingPush = a.second.isMoving + b.second.isMoving; // Exception: units in the same control group (i.e. the same formation) never push farther than themselves // and are also allowed to push idle units (obstructions are ignored within formations, // so pushing idle units makes one member crossing the formation look better). bool sameControlGroup = a.second.controlGroup != INVALID_ENTITY && a.second.controlGroup == b.second.controlGroup; if (sameControlGroup) movingPush = 0; if (movingPush == 1) return; 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_PushingRadiusMultiplier) + (movingPush ? m_MovingPushExtension : m_StaticPushExtension); combinedClearance = maxDist.Multiply(movingPush ? m_MovingPushingSpread : m_StaticPushingSpread); // 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; // 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 { 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; } } // 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) distanceFactor = MAX_DISTANCE_FACTOR; else distanceFactor = Clamp((maxDist - offsetLength) / distanceFactor, entity_pos_t::Zero(), MAX_DISTANCE_FACTOR); // Mark both as needing an update so they actually get moved. a.second.needUpdate = true; b.second.needUpdate = true; CFixedVector2D pushingDir = offset.Multiply(distanceFactor); // These cannot be zero, checked in the schema. entity_pos_t aWeight = a.second.cmpUnitMotion->GetWeight(); entity_pos_t bWeight = b.second.cmpUnitMotion->GetWeight(); // Final corrections: // - divide by an arbitrary constant to avoid pushing too much. // - multiply by the weight ratio (limiting the maximum positive push for numerical accuracy). entity_pos_t timeFactor = dt / PUSHING_REDUCTION_FACTOR; entity_pos_t maxPushing = timeFactor * MAX_PUSHING_MULTIPLIER; a.second.push += pushingDir.Multiply(std::min(bWeight.MulDiv(timeFactor, aWeight), maxPushing)); b.second.push -= pushingDir.Multiply(std::min(aWeight.MulDiv(timeFactor, bWeight), maxPushing)); // 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