/* 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 "simulation2/system/Component.h" #include "ICmpObstructionManager.h" #include "ICmpPosition.h" #include "ICmpRangeManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Grid.h" #include "simulation2/helpers/Rasterize.h" #include "simulation2/helpers/Render.h" #include "simulation2/helpers/Spatial.h" #include "simulation2/serialization/SerializedTypes.h" #include "graphics/Overlay.h" #include "maths/MathUtil.h" #include "ps/Profile.h" #include "renderer/Scene.h" #include "ps/CLogger.h" // Externally, tags are opaque non-zero positive integers. // Internally, they are tagged (by shape) indexes into shape lists. // idx must be non-zero. #define TAG_IS_VALID(tag) ((tag).valid()) #define TAG_IS_UNIT(tag) (((tag).n & 1) == 0) #define TAG_IS_STATIC(tag) (((tag).n & 1) == 1) #define UNIT_INDEX_TO_TAG(idx) tag_t(((idx) << 1) | 0) #define STATIC_INDEX_TO_TAG(idx) tag_t(((idx) << 1) | 1) #define TAG_TO_INDEX(tag) ((tag).n >> 1) namespace { /** * Size of each obstruction subdivision square. * TODO: find the optimal number instead of blindly guessing. */ constexpr entity_pos_t OBSTRUCTION_SUBDIVISION_SIZE = entity_pos_t::FromInt(32); /** * Internal representation of axis-aligned circular shapes for moving units */ struct UnitShape { entity_id_t entity; entity_pos_t x, z; entity_pos_t clearance; ICmpObstructionManager::flags_t flags; entity_id_t group; // control group (typically the owner entity, or a formation controller entity) (units ignore collisions with others in the same group) }; /** * Internal representation of arbitrary-rotation static square shapes for buildings */ struct StaticShape { entity_id_t entity; entity_pos_t x, z; // world-space coordinates CFixedVector2D u, v; // orthogonal unit vectors - axes of local coordinate space entity_pos_t hw, hh; // half width/height in local coordinate space ICmpObstructionManager::flags_t flags; entity_id_t group; entity_id_t group2; }; } // anonymous namespace /** * Serialization helper template for UnitShape */ template<> struct SerializeHelper { template void operator()(S& serialize, const char* UNUSED(name), Serialize::qualify value) const { serialize.NumberU32_Unbounded("entity", value.entity); serialize.NumberFixed_Unbounded("x", value.x); serialize.NumberFixed_Unbounded("z", value.z); serialize.NumberFixed_Unbounded("clearance", value.clearance); serialize.NumberU8_Unbounded("flags", value.flags); serialize.NumberU32_Unbounded("group", value.group); } }; /** * Serialization helper template for StaticShape */ template<> struct SerializeHelper { template void operator()(S& serialize, const char* UNUSED(name), Serialize::qualify value) const { serialize.NumberU32_Unbounded("entity", value.entity); serialize.NumberFixed_Unbounded("x", value.x); serialize.NumberFixed_Unbounded("z", value.z); serialize.NumberFixed_Unbounded("u.x", value.u.X); serialize.NumberFixed_Unbounded("u.y", value.u.Y); serialize.NumberFixed_Unbounded("v.x", value.v.X); serialize.NumberFixed_Unbounded("v.y", value.v.Y); serialize.NumberFixed_Unbounded("hw", value.hw); serialize.NumberFixed_Unbounded("hh", value.hh); serialize.NumberU8_Unbounded("flags", value.flags); serialize.NumberU32_Unbounded("group", value.group); serialize.NumberU32_Unbounded("group2", value.group2); } }; class CCmpObstructionManager final : public ICmpObstructionManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays } DEFAULT_COMPONENT_ALLOCATOR(ObstructionManager) bool m_DebugOverlayEnabled; bool m_DebugOverlayDirty; std::vector m_DebugOverlayLines; SpatialSubdivision m_UnitSubdivision; SpatialSubdivision m_StaticSubdivision; // TODO: using std::map is a bit inefficient; is there a better way to store these? std::map m_UnitShapes; std::map m_StaticShapes; u32 m_UnitShapeNext; // next allocated id u32 m_StaticShapeNext; entity_pos_t m_MaxClearance; bool m_PassabilityCircular; entity_pos_t m_WorldX0; entity_pos_t m_WorldZ0; entity_pos_t m_WorldX1; entity_pos_t m_WorldZ1; static std::string GetSchema() { return ""; } void Init(const CParamNode& UNUSED(paramNode)) override { m_DebugOverlayEnabled = false; m_DebugOverlayDirty = true; m_UnitShapeNext = 1; m_StaticShapeNext = 1; m_UpdateInformations.dirty = true; m_UpdateInformations.globallyDirty = true; m_PassabilityCircular = false; m_WorldX0 = m_WorldZ0 = m_WorldX1 = m_WorldZ1 = entity_pos_t::Zero(); // Initialise with bogus values (these will get replaced when // SetBounds is called) ResetSubdivisions(entity_pos_t::FromInt(1024), entity_pos_t::FromInt(1024)); } void Deinit() override { } template void SerializeCommon(S& serialize) { Serializer(serialize, "unit subdiv", m_UnitSubdivision); Serializer(serialize, "static subdiv", m_StaticSubdivision); serialize.NumberFixed_Unbounded("max clearance", m_MaxClearance); Serializer(serialize, "unit shapes", m_UnitShapes); Serializer(serialize, "static shapes", m_StaticShapes); serialize.NumberU32_Unbounded("unit shape next", m_UnitShapeNext); serialize.NumberU32_Unbounded("static shape next", m_StaticShapeNext); serialize.Bool("circular", m_PassabilityCircular); serialize.NumberFixed_Unbounded("world x0", m_WorldX0); serialize.NumberFixed_Unbounded("world z0", m_WorldZ0); serialize.NumberFixed_Unbounded("world x1", m_WorldX1); serialize.NumberFixed_Unbounded("world z1", m_WorldZ1); } void Serialize(ISerializer& serialize) override { // TODO: this could perhaps be optimised by not storing all the obstructions, // and instead regenerating them from the other entities on Deserialize SerializeCommon(serialize); } void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) override { Init(paramNode); SerializeCommon(deserialize); i32 size = ((m_WorldX1-m_WorldX0)/Pathfinding::NAVCELL_SIZE_INT).ToInt_RoundToInfinity(); m_UpdateInformations.dirtinessGrid = Grid(size, size); } void HandleMessage(const CMessage& msg, bool UNUSED(global)) override { switch (msg.GetType()) { case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } } } // NB: on deserialization, this function is not called after the component is reset. // So anything that happens here should be safely serialized. void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1) override { m_WorldX0 = x0; m_WorldZ0 = z0; m_WorldX1 = x1; m_WorldZ1 = z1; MakeDirtyAll(); // Subdivision system bounds: ENSURE(x0.IsZero() && z0.IsZero()); // don't bother implementing non-zero offsets yet ResetSubdivisions(x1, z1); i32 size = ((m_WorldX1-m_WorldX0)/Pathfinding::NAVCELL_SIZE_INT).ToInt_RoundToInfinity(); m_UpdateInformations.dirtinessGrid = Grid(size, size); CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) m_MaxClearance = cmpPathfinder->GetMaximumClearance(); } void ResetSubdivisions(entity_pos_t x1, entity_pos_t z1) { m_UnitSubdivision.Reset(x1, z1, OBSTRUCTION_SUBDIVISION_SIZE); m_StaticSubdivision.Reset(x1, z1, OBSTRUCTION_SUBDIVISION_SIZE); for (std::map::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it) { CFixedVector2D center(it->second.x, it->second.z); CFixedVector2D halfSize(it->second.clearance, it->second.clearance); m_UnitSubdivision.Add(it->first, center - halfSize, center + halfSize); } for (std::map::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it) { CFixedVector2D center(it->second.x, it->second.z); CFixedVector2D bbHalfSize = Geometry::GetHalfBoundingBox(it->second.u, it->second.v, CFixedVector2D(it->second.hw, it->second.hh)); m_StaticSubdivision.Add(it->first, center - bbHalfSize, center + bbHalfSize); } } tag_t AddUnitShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_pos_t clearance, flags_t flags, entity_id_t group) override { UnitShape shape = { ent, x, z, clearance, flags, group }; u32 id = m_UnitShapeNext++; m_UnitShapes[id] = shape; m_UnitSubdivision.Add(id, CFixedVector2D(x - clearance, z - clearance), CFixedVector2D(x + clearance, z + clearance)); MakeDirtyUnit(flags, id, shape); return UNIT_INDEX_TO_TAG(id); } tag_t AddStaticShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h, flags_t flags, entity_id_t group, entity_id_t group2 /* = INVALID_ENTITY */) override { fixed s, c; sincos_approx(a, s, c); CFixedVector2D u(c, -s); CFixedVector2D v(s, c); StaticShape shape = { ent, x, z, u, v, w/2, h/2, flags, group, group2 }; u32 id = m_StaticShapeNext++; m_StaticShapes[id] = shape; CFixedVector2D center(x, z); CFixedVector2D bbHalfSize = Geometry::GetHalfBoundingBox(u, v, CFixedVector2D(w/2, h/2)); m_StaticSubdivision.Add(id, center - bbHalfSize, center + bbHalfSize); MakeDirtyStatic(flags, id, shape); return STATIC_INDEX_TO_TAG(id); } ObstructionSquare GetUnitShapeObstruction(entity_pos_t x, entity_pos_t z, entity_pos_t clearance) const override { CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero()); CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1)); ObstructionSquare o = { x, z, u, v, clearance, clearance }; return o; } ObstructionSquare GetStaticShapeObstruction(entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h) const override { fixed s, c; sincos_approx(a, s, c); CFixedVector2D u(c, -s); CFixedVector2D v(s, c); ObstructionSquare o = { x, z, u, v, w/2, h/2 }; return o; } void MoveShape(tag_t tag, entity_pos_t x, entity_pos_t z, entity_angle_t a) override { ENSURE(TAG_IS_VALID(tag)); if (TAG_IS_UNIT(tag)) { UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)]; MakeDirtyUnit(shape.flags, TAG_TO_INDEX(tag), shape); // dirty the old shape region m_UnitSubdivision.Move(TAG_TO_INDEX(tag), CFixedVector2D(shape.x - shape.clearance, shape.z - shape.clearance), CFixedVector2D(shape.x + shape.clearance, shape.z + shape.clearance), CFixedVector2D(x - shape.clearance, z - shape.clearance), CFixedVector2D(x + shape.clearance, z + shape.clearance)); shape.x = x; shape.z = z; MakeDirtyUnit(shape.flags, TAG_TO_INDEX(tag), shape); // dirty the new shape region } else { fixed s, c; sincos_approx(a, s, c); CFixedVector2D u(c, -s); CFixedVector2D v(s, c); StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)]; MakeDirtyStatic(shape.flags, TAG_TO_INDEX(tag), shape); // dirty the old shape region CFixedVector2D fromBbHalfSize = Geometry::GetHalfBoundingBox(shape.u, shape.v, CFixedVector2D(shape.hw, shape.hh)); CFixedVector2D toBbHalfSize = Geometry::GetHalfBoundingBox(u, v, CFixedVector2D(shape.hw, shape.hh)); m_StaticSubdivision.Move(TAG_TO_INDEX(tag), CFixedVector2D(shape.x, shape.z) - fromBbHalfSize, CFixedVector2D(shape.x, shape.z) + fromBbHalfSize, CFixedVector2D(x, z) - toBbHalfSize, CFixedVector2D(x, z) + toBbHalfSize); shape.x = x; shape.z = z; shape.u = u; shape.v = v; MakeDirtyStatic(shape.flags, TAG_TO_INDEX(tag), shape); // dirty the new shape region } } void SetUnitMovingFlag(tag_t tag, bool moving) override { ENSURE(TAG_IS_VALID(tag) && TAG_IS_UNIT(tag)); if (TAG_IS_UNIT(tag)) { UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)]; if (moving) shape.flags |= FLAG_MOVING; else shape.flags &= (flags_t)~FLAG_MOVING; MakeDirtyDebug(); } } void SetUnitControlGroup(tag_t tag, entity_id_t group) override { ENSURE(TAG_IS_VALID(tag) && TAG_IS_UNIT(tag)); if (TAG_IS_UNIT(tag)) { UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)]; shape.group = group; } } void SetStaticControlGroup(tag_t tag, entity_id_t group, entity_id_t group2) override { ENSURE(TAG_IS_VALID(tag) && TAG_IS_STATIC(tag)); if (TAG_IS_STATIC(tag)) { StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)]; shape.group = group; shape.group2 = group2; } } void RemoveShape(tag_t tag) override { ENSURE(TAG_IS_VALID(tag)); if (TAG_IS_UNIT(tag)) { UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)]; m_UnitSubdivision.Remove(TAG_TO_INDEX(tag), CFixedVector2D(shape.x - shape.clearance, shape.z - shape.clearance), CFixedVector2D(shape.x + shape.clearance, shape.z + shape.clearance)); MakeDirtyUnit(shape.flags, TAG_TO_INDEX(tag), shape); m_UnitShapes.erase(TAG_TO_INDEX(tag)); } else { StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)]; CFixedVector2D center(shape.x, shape.z); CFixedVector2D bbHalfSize = Geometry::GetHalfBoundingBox(shape.u, shape.v, CFixedVector2D(shape.hw, shape.hh)); m_StaticSubdivision.Remove(TAG_TO_INDEX(tag), center - bbHalfSize, center + bbHalfSize); MakeDirtyStatic(shape.flags, TAG_TO_INDEX(tag), shape); m_StaticShapes.erase(TAG_TO_INDEX(tag)); } } ObstructionSquare GetObstruction(tag_t tag) const override { ENSURE(TAG_IS_VALID(tag)); if (TAG_IS_UNIT(tag)) { const UnitShape& shape = m_UnitShapes.at(TAG_TO_INDEX(tag)); CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero()); CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1)); ObstructionSquare o = { shape.x, shape.z, u, v, shape.clearance, shape.clearance }; return o; } else { const StaticShape& shape = m_StaticShapes.at(TAG_TO_INDEX(tag)); ObstructionSquare o = { shape.x, shape.z, shape.u, shape.v, shape.hw, shape.hh }; return o; } } fixed DistanceToPoint(entity_id_t ent, entity_pos_t px, entity_pos_t pz) const override; fixed MaxDistanceToPoint(entity_id_t ent, entity_pos_t px, entity_pos_t pz) const override; fixed DistanceToTarget(entity_id_t ent, entity_id_t target) const override; fixed MaxDistanceToTarget(entity_id_t ent, entity_id_t target) const override; fixed DistanceBetweenShapes(const ObstructionSquare& source, const ObstructionSquare& target) const override; fixed MaxDistanceBetweenShapes(const ObstructionSquare& source, const ObstructionSquare& target) const override; bool IsInPointRange(entity_id_t ent, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const override; bool IsInTargetRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const override; bool IsInTargetParabolicRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin, bool opposite) const override; bool IsPointInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) const override; bool AreShapesInRange(const ObstructionSquare& source, const ObstructionSquare& target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const override; bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits = false) const override; bool TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, std::vector* out) const override; bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, std::vector* out) const override; void Rasterize(Grid& grid, const std::vector& passClasses, bool fullUpdate) override; void GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector& squares) const override; void GetUnitObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector& squares) const override; void GetStaticObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector& squares) const override; void GetUnitsOnObstruction(const ObstructionSquare& square, std::vector& out, const IObstructionTestFilter& filter, bool strict = false) const override; void GetStaticObstructionsOnObstruction(const ObstructionSquare& square, std::vector& out, const IObstructionTestFilter& filter) const override; void SetPassabilityCircular(bool enabled) override { m_PassabilityCircular = enabled; MakeDirtyAll(); CMessageObstructionMapShapeChanged msg; GetSimContext().GetComponentManager().BroadcastMessage(msg); } bool GetPassabilityCircular() const override { return m_PassabilityCircular; } void SetDebugOverlay(bool enabled) override { m_DebugOverlayEnabled = enabled; m_DebugOverlayDirty = true; if (!enabled) m_DebugOverlayLines.clear(); } void RenderSubmit(SceneCollector& collector); void UpdateInformations(GridUpdateInformation& informations) override { if (!m_UpdateInformations.dirtinessGrid.blank()) informations.MergeAndClear(m_UpdateInformations); } private: // Dynamic updates for the long-range pathfinder GridUpdateInformation m_UpdateInformations; // These vectors might contain shapes that were deleted std::vector m_DirtyStaticShapes; std::vector m_DirtyUnitShapes; /** * Mark all previous Rasterize()d grids as dirty, and the debug display. * Call this when the world bounds have changed. */ void MakeDirtyAll() { m_UpdateInformations.dirty = true; m_UpdateInformations.globallyDirty = true; m_UpdateInformations.dirtinessGrid.reset(); m_DebugOverlayDirty = true; } /** * Mark the debug display as dirty. * Call this when nothing has changed except a unit's 'moving' flag. */ void MakeDirtyDebug() { m_DebugOverlayDirty = true; } inline void MarkDirtinessGrid(const entity_pos_t& x, const entity_pos_t& z, const entity_pos_t& r) { MarkDirtinessGrid(x, z, CFixedVector2D(r, r)); } inline void MarkDirtinessGrid(const entity_pos_t& x, const entity_pos_t& z, const CFixedVector2D& hbox) { if (m_UpdateInformations.dirtinessGrid.m_W == 0) return; u16 j0, j1, i0, i1; Pathfinding::NearestNavcell(x - hbox.X, z - hbox.Y, i0, j0, m_UpdateInformations.dirtinessGrid.m_W, m_UpdateInformations.dirtinessGrid.m_H); Pathfinding::NearestNavcell(x + hbox.X, z + hbox.Y, i1, j1, m_UpdateInformations.dirtinessGrid.m_W, m_UpdateInformations.dirtinessGrid.m_H); for (int j = j0; j < j1; ++j) for (int i = i0; i < i1; ++i) m_UpdateInformations.dirtinessGrid.set(i, j, 1); } /** * Mark all previous Rasterize()d grids as dirty, if they depend on this shape. * Call this when a static shape has changed. */ void MakeDirtyStatic(flags_t flags, u32 index, const StaticShape& shape) { m_DebugOverlayDirty = true; if (flags & (FLAG_BLOCK_PATHFINDING | FLAG_BLOCK_FOUNDATION)) { m_UpdateInformations.dirty = true; if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), index) == m_DirtyStaticShapes.end()) m_DirtyStaticShapes.push_back(index); // All shapes overlapping the updated part of the grid should be dirtied too. // We are going to invalidate the region of the grid corresponding to the modified shape plus its clearance, // and we need to get the shapes whose clearance can overlap this area. So we need to extend the search area // by two times the maximum clearance. CFixedVector2D center(shape.x, shape.z); CFixedVector2D hbox = Geometry::GetHalfBoundingBox(shape.u, shape.v, CFixedVector2D(shape.hw, shape.hh)); CFixedVector2D expand(m_MaxClearance, m_MaxClearance); std::vector staticsNear; m_StaticSubdivision.GetInRange(staticsNear, center - hbox - expand*2, center + hbox + expand*2); for (u32& staticId : staticsNear) if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), staticId) == m_DirtyStaticShapes.end()) m_DirtyStaticShapes.push_back(staticId); std::vector unitsNear; m_UnitSubdivision.GetInRange(unitsNear, center - hbox - expand*2, center + hbox + expand*2); for (u32& unitId : unitsNear) if (std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), unitId) == m_DirtyUnitShapes.end()) m_DirtyUnitShapes.push_back(unitId); MarkDirtinessGrid(shape.x, shape.z, hbox + expand); } } /** * Mark all previous Rasterize()d grids as dirty, if they depend on this shape. * Call this when a unit shape has changed. */ void MakeDirtyUnit(flags_t flags, u32 index, const UnitShape& shape) { m_DebugOverlayDirty = true; if (flags & (FLAG_BLOCK_PATHFINDING | FLAG_BLOCK_FOUNDATION)) { m_UpdateInformations.dirty = true; if (std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), index) == m_DirtyUnitShapes.end()) m_DirtyUnitShapes.push_back(index); // All shapes overlapping the updated part of the grid should be dirtied too. // We are going to invalidate the region of the grid corresponding to the modified shape plus its clearance, // and we need to get the shapes whose clearance can overlap this area. So we need to extend the search area // by two times the maximum clearance. CFixedVector2D center(shape.x, shape.z); std::vector staticsNear; m_StaticSubdivision.GetNear(staticsNear, center, shape.clearance + m_MaxClearance*2); for (u32& staticId : staticsNear) if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), staticId) == m_DirtyStaticShapes.end()) m_DirtyStaticShapes.push_back(staticId); std::vector unitsNear; m_UnitSubdivision.GetNear(unitsNear, center, shape.clearance + m_MaxClearance*2); for (u32& unitId : unitsNear) if (std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), unitId) == m_DirtyUnitShapes.end()) m_DirtyUnitShapes.push_back(unitId); MarkDirtinessGrid(shape.x, shape.z, shape.clearance + m_MaxClearance); } } /** * Return whether the given point is within the world bounds by at least r */ inline bool IsInWorld(entity_pos_t x, entity_pos_t z, entity_pos_t r) const { return (m_WorldX0+r <= x && x <= m_WorldX1-r && m_WorldZ0+r <= z && z <= m_WorldZ1-r); } /** * Return whether the given point is within the world bounds */ inline bool IsInWorld(const CFixedVector2D& p) const { return (m_WorldX0 <= p.X && p.X <= m_WorldX1 && m_WorldZ0 <= p.Y && p.Y <= m_WorldZ1); } void RasterizeHelper(Grid& grid, ICmpObstructionManager::flags_t requireMask, bool fullUpdate, pass_class_t appliedMask, entity_pos_t clearance = fixed::Zero()) const; }; REGISTER_COMPONENT_TYPE(ObstructionManager) /** * DistanceTo function family, all end up in calculating a vector length, DistanceBetweenShapes or * MaxDistanceBetweenShapes. The MaxFoo family calculates the opposite edge opposite edge distance. * When the distance is undefined we return -1. */ fixed CCmpObstructionManager::DistanceToPoint(entity_id_t ent, entity_pos_t px, entity_pos_t pz) const { ObstructionSquare obstruction; CmpPtr cmpObstruction(GetSimContext(), ent); if (cmpObstruction && cmpObstruction->GetObstructionSquare(obstruction)) { ObstructionSquare point; point.x = px; point.z = pz; return DistanceBetweenShapes(obstruction, point); } CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition || !cmpPosition->IsInWorld()) return fixed::FromInt(-1); return (CFixedVector2D(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y) - CFixedVector2D(px, pz)).Length(); } fixed CCmpObstructionManager::MaxDistanceToPoint(entity_id_t ent, entity_pos_t px, entity_pos_t pz) const { ObstructionSquare obstruction; CmpPtr cmpObstruction(GetSimContext(), ent); if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction)) { ObstructionSquare point; point.x = px; point.z = pz; return MaxDistanceBetweenShapes(obstruction, point); } CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition || !cmpPosition->IsInWorld()) return fixed::FromInt(-1); return (CFixedVector2D(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y) - CFixedVector2D(px, pz)).Length(); } fixed CCmpObstructionManager::DistanceToTarget(entity_id_t ent, entity_id_t target) const { ObstructionSquare obstruction; CmpPtr cmpObstruction(GetSimContext(), ent); if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction)) { CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition || !cmpPosition->IsInWorld()) return fixed::FromInt(-1); return DistanceToPoint(target, cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y); } ObstructionSquare target_obstruction; CmpPtr cmpObstructionTarget(GetSimContext(), target); if (!cmpObstructionTarget || !cmpObstructionTarget->GetObstructionSquare(target_obstruction)) { CmpPtr cmpPositionTarget(GetSimContext(), target); if (!cmpPositionTarget || !cmpPositionTarget->IsInWorld()) return fixed::FromInt(-1); return DistanceToPoint(ent, cmpPositionTarget->GetPosition2D().X, cmpPositionTarget->GetPosition2D().Y); } return DistanceBetweenShapes(obstruction, target_obstruction); } fixed CCmpObstructionManager::MaxDistanceToTarget(entity_id_t ent, entity_id_t target) const { ObstructionSquare obstruction; CmpPtr cmpObstruction(GetSimContext(), ent); if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction)) { CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition || !cmpPosition->IsInWorld()) return fixed::FromInt(-1); return MaxDistanceToPoint(target, cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y); } ObstructionSquare target_obstruction; CmpPtr cmpObstructionTarget(GetSimContext(), target); if (!cmpObstructionTarget || !cmpObstructionTarget->GetObstructionSquare(target_obstruction)) { CmpPtr cmpPositionTarget(GetSimContext(), target); if (!cmpPositionTarget || !cmpPositionTarget->IsInWorld()) return fixed::FromInt(-1); return MaxDistanceToPoint(ent, cmpPositionTarget->GetPosition2D().X, cmpPositionTarget->GetPosition2D().Y); } return MaxDistanceBetweenShapes(obstruction, target_obstruction); } fixed CCmpObstructionManager::DistanceBetweenShapes(const ObstructionSquare& source, const ObstructionSquare& target) const { // Sphere-sphere collision. if (source.hh == fixed::Zero() && target.hh == fixed::Zero()) return (CFixedVector2D(target.x, target.z) - CFixedVector2D(source.x, source.z)).Length() - source.hw - target.hw; // Square to square. if (source.hh != fixed::Zero() && target.hh != fixed::Zero()) return Geometry::DistanceSquareToSquare( CFixedVector2D(target.x, target.z) - CFixedVector2D(source.x, source.z), source.u, source.v, CFixedVector2D(source.hw, source.hh), target.u, target.v, CFixedVector2D(target.hw, target.hh)); // To cover both remaining cases, shape a is the square one, shape b is the circular one. const ObstructionSquare& a = source.hh == fixed::Zero() ? target : source; const ObstructionSquare& b = source.hh == fixed::Zero() ? source : target; return Geometry::DistanceToSquare( CFixedVector2D(b.x, b.z) - CFixedVector2D(a.x, a.z), a.u, a.v, CFixedVector2D(a.hw, a.hh), true) - b.hw; } fixed CCmpObstructionManager::MaxDistanceBetweenShapes(const ObstructionSquare& source, const ObstructionSquare& target) const { // Sphere-sphere collision. if (source.hh == fixed::Zero() && target.hh == fixed::Zero()) return (CFixedVector2D(target.x, target.z) - CFixedVector2D(source.x, source.z)).Length() + source.hw + target.hw; // Square to square. if (source.hh != fixed::Zero() && target.hh != fixed::Zero()) return Geometry::MaxDistanceSquareToSquare( CFixedVector2D(target.x, target.z) - CFixedVector2D(source.x, source.z), source.u, source.v, CFixedVector2D(source.hw, source.hh), target.u, target.v, CFixedVector2D(target.hw, target.hh)); // To cover both remaining cases, shape a is the square one, shape b is the circular one. const ObstructionSquare& a = source.hh == fixed::Zero() ? target : source; const ObstructionSquare& b = source.hh == fixed::Zero() ? source : target; return Geometry::MaxDistanceToSquare( CFixedVector2D(b.x, b.z) - CFixedVector2D(a.x, a.z), a.u, a.v, CFixedVector2D(a.hw, a.hh), true) + b.hw; } /** * IsInRange function family depending on the DistanceTo family. * * In range if the edge to edge distance is inferior to maxRange * and if the opposite edge to opposite edge distance is greater than minRange when the opposite bool is true * or when the opposite bool is false the edge to edge distance is more than minRange. * * Using the opposite egde for minRange means that a unit is in range of a building if it is farther than * clearance-buildingsize, which is generally going to be negative (and thus this returns true). * NB: from a game POV, this means units can easily fire on buildings, which is good, * but it also means that buildings can easily fire on units. Buildings are usually meant * to fire from the edge, not the opposite edge, so this looks odd. For this reason one can choose * to set the opposite bool false and use the edge to egde distance. * * We don't use squares because the are likely to overflow. * TODO Avoid the overflows and use squares instead. * We use a 0.0001 margin to avoid rounding errors. */ bool CCmpObstructionManager::IsInPointRange(entity_id_t ent, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const { fixed dist = DistanceToPoint(ent, px, pz); return maxRange != NEVER_IN_RANGE && dist != fixed::FromInt(-1) && (dist <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange == ALWAYS_IN_RANGE) && (opposite ? MaxDistanceToPoint(ent, px, pz) : dist) >= minRange - fixed::FromFloat(0.0001f); } bool CCmpObstructionManager::IsInTargetRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const { fixed dist = DistanceToTarget(ent, target); return maxRange != NEVER_IN_RANGE && dist != fixed::FromInt(-1) && (dist <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange == ALWAYS_IN_RANGE) && (opposite ? MaxDistanceToTarget(ent, target) : dist) >= minRange - fixed::FromFloat(0.0001f); } bool CCmpObstructionManager::IsInTargetParabolicRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin, bool opposite) const { CmpPtr cmpRangeManager(GetSystemEntity()); return IsInTargetRange(ent, target, minRange, cmpRangeManager->GetEffectiveParabolicRange(ent, target, maxRange, yOrigin), opposite); } bool CCmpObstructionManager::IsPointInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) const { entity_pos_t distance = (CFixedVector2D(x, z) - CFixedVector2D(px, pz)).Length(); return maxRange != NEVER_IN_RANGE && (distance <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange == ALWAYS_IN_RANGE) && distance >= minRange - fixed::FromFloat(0.0001f); } bool CCmpObstructionManager::AreShapesInRange(const ObstructionSquare& source, const ObstructionSquare& target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const { fixed dist = DistanceBetweenShapes(source, target); return maxRange != NEVER_IN_RANGE && dist != fixed::FromInt(-1) && (dist <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange == ALWAYS_IN_RANGE) && (opposite ? MaxDistanceBetweenShapes(source, target) : dist) >= minRange - fixed::FromFloat(0.0001f); } bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits) const { PROFILE("TestLine"); // Check that both end points are within the world (which means the whole line must be) if (!IsInWorld(x0, z0, r) || !IsInWorld(x1, z1, r)) return true; CFixedVector2D posMin (std::min(x0, x1) - r, std::min(z0, z1) - r); CFixedVector2D posMax (std::max(x0, x1) + r, std::max(z0, z1) + r); // actual radius used for unit-unit collisions. If relaxClearanceForUnits, will be smaller to allow more overlap. entity_pos_t unitUnitRadius = r; if (relaxClearanceForUnits) unitUnitRadius -= entity_pos_t::FromInt(1)/2; std::vector unitShapes; m_UnitSubdivision.GetInRange(unitShapes, posMin, posMax); for (const entity_id_t& shape : unitShapes) { std::map::const_iterator it = m_UnitShapes.find(shape); ENSURE(it != m_UnitShapes.end()); if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY)) continue; CFixedVector2D center(it->second.x, it->second.z); CFixedVector2D halfSize(it->second.clearance + unitUnitRadius, it->second.clearance + unitUnitRadius); if (Geometry::TestRayAASquare(CFixedVector2D(x0, z0) - center, CFixedVector2D(x1, z1) - center, halfSize)) return true; } std::vector staticShapes; m_StaticSubdivision.GetInRange(staticShapes, posMin, posMax); for (const entity_id_t& shape : staticShapes) { std::map::const_iterator it = m_StaticShapes.find(shape); ENSURE(it != m_StaticShapes.end()); if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2)) continue; CFixedVector2D center(it->second.x, it->second.z); CFixedVector2D halfSize(it->second.hw + r, it->second.hh + r); if (Geometry::TestRaySquare(CFixedVector2D(x0, z0) - center, CFixedVector2D(x1, z1) - center, it->second.u, it->second.v, halfSize)) return true; } return false; } bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, std::vector* out) const { PROFILE("TestStaticShape"); if (out) out->clear(); fixed s, c; sincos_approx(a, s, c); CFixedVector2D u(c, -s); CFixedVector2D v(s, c); CFixedVector2D center(x, z); CFixedVector2D halfSize(w/2, h/2); CFixedVector2D corner1 = u.Multiply(halfSize.X) + v.Multiply(halfSize.Y); CFixedVector2D corner2 = u.Multiply(halfSize.X) - v.Multiply(halfSize.Y); // Check that all corners are within the world (which means the whole shape must be) if (!IsInWorld(center + corner1) || !IsInWorld(center + corner2) || !IsInWorld(center - corner1) || !IsInWorld(center - corner2)) { if (out) out->push_back(INVALID_ENTITY); // no entity ID, so just push an arbitrary marker else return true; } fixed bbHalfWidth = std::max(corner1.X.Absolute(), corner2.X.Absolute()); fixed bbHalfHeight = std::max(corner1.Y.Absolute(), corner2.Y.Absolute()); CFixedVector2D posMin(x - bbHalfWidth, z - bbHalfHeight); CFixedVector2D posMax(x + bbHalfWidth, z + bbHalfHeight); std::vector unitShapes; m_UnitSubdivision.GetInRange(unitShapes, posMin, posMax); for (entity_id_t& shape : unitShapes) { std::map::const_iterator it = m_UnitShapes.find(shape); ENSURE(it != m_UnitShapes.end()); if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY)) continue; CFixedVector2D center1(it->second.x, it->second.z); if (Geometry::PointIsInSquare(center1 - center, u, v, CFixedVector2D(halfSize.X + it->second.clearance, halfSize.Y + it->second.clearance))) { if (out) out->push_back(it->second.entity); else return true; } } std::vector staticShapes; m_StaticSubdivision.GetInRange(staticShapes, posMin, posMax); for (entity_id_t& shape : staticShapes) { std::map::const_iterator it = m_StaticShapes.find(shape); ENSURE(it != m_StaticShapes.end()); if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2)) continue; CFixedVector2D center1(it->second.x, it->second.z); CFixedVector2D halfSize1(it->second.hw, it->second.hh); if (Geometry::TestSquareSquare(center, u, v, halfSize, center1, it->second.u, it->second.v, halfSize1)) { if (out) out->push_back(it->second.entity); else return true; } } if (out) return !out->empty(); // collided if the list isn't empty else return false; // didn't collide, if we got this far } bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t clearance, std::vector* out) const { PROFILE("TestUnitShape"); // Check that the shape is within the world if (!IsInWorld(x, z, clearance)) { if (out) out->push_back(INVALID_ENTITY); // no entity ID, so just push an arbitrary marker else return true; } CFixedVector2D center(x, z); CFixedVector2D posMin(x - clearance, z - clearance); CFixedVector2D posMax(x + clearance, z + clearance); std::vector unitShapes; m_UnitSubdivision.GetInRange(unitShapes, posMin, posMax); for (const entity_id_t& shape : unitShapes) { std::map::const_iterator it = m_UnitShapes.find(shape); ENSURE(it != m_UnitShapes.end()); if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY)) continue; entity_pos_t c1 = it->second.clearance; if (!( it->second.x + c1 < x - clearance || it->second.x - c1 > x + clearance || it->second.z + c1 < z - clearance || it->second.z - c1 > z + clearance)) { if (out) out->push_back(it->second.entity); else return true; } } std::vector staticShapes; m_StaticSubdivision.GetInRange(staticShapes, posMin, posMax); for (const entity_id_t& shape : staticShapes) { std::map::const_iterator it = m_StaticShapes.find(shape); ENSURE(it != m_StaticShapes.end()); if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2)) continue; CFixedVector2D center1(it->second.x, it->second.z); if (Geometry::PointIsInSquare(center1 - center, it->second.u, it->second.v, CFixedVector2D(it->second.hw + clearance, it->second.hh + clearance))) { if (out) out->push_back(it->second.entity); else return true; } } if (out) return !out->empty(); // collided if the list isn't empty else return false; // didn't collide, if we got this far } void CCmpObstructionManager::Rasterize(Grid& grid, const std::vector& passClasses, bool fullUpdate) { PROFILE3("Rasterize Obstructions"); // Cells are only marked as blocked if the whole cell is strictly inside the shape. // (That ensures the shape's geometric border is always reachable.) // Pass classes will get shapes rasterized on them depending on their Obstruction value. // Classes with another value than "pathfinding" should not use Clearance. std::map pathfindingMasks; u16 foundationMask = 0; for (const PathfinderPassability& passability : passClasses) { switch (passability.m_Obstructions) { case PathfinderPassability::PATHFINDING: { std::map::iterator it = pathfindingMasks.find(passability.m_Clearance); if (it == pathfindingMasks.end()) pathfindingMasks[passability.m_Clearance] = passability.m_Mask; else it->second |= passability.m_Mask; break; } case PathfinderPassability::FOUNDATION: foundationMask |= passability.m_Mask; break; default: continue; } } // FLAG_BLOCK_PATHFINDING and FLAG_BLOCK_FOUNDATION are the only flags taken into account by MakeDirty* functions, // so they should be the only ones rasterized using with the help of m_Dirty*Shapes vectors. for (auto& maskPair : pathfindingMasks) RasterizeHelper(grid, FLAG_BLOCK_PATHFINDING, fullUpdate, maskPair.second, maskPair.first); RasterizeHelper(grid, FLAG_BLOCK_FOUNDATION, fullUpdate, foundationMask); m_DirtyStaticShapes.clear(); m_DirtyUnitShapes.clear(); } void CCmpObstructionManager::RasterizeHelper(Grid& grid, ICmpObstructionManager::flags_t requireMask, bool fullUpdate, pass_class_t appliedMask, entity_pos_t clearance) const { for (auto& pair : m_StaticShapes) { const StaticShape& shape = pair.second; if (!(shape.flags & requireMask)) continue; if (!fullUpdate && std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), pair.first) == m_DirtyStaticShapes.end()) continue; // TODO: it might be nice to rasterize with rounded corners for large 'expand' values. ObstructionSquare square = { shape.x, shape.z, shape.u, shape.v, shape.hw, shape.hh }; SimRasterize::Spans spans; SimRasterize::RasterizeRectWithClearance(spans, square, clearance, Pathfinding::NAVCELL_SIZE); for (SimRasterize::Span& span : spans) { i16 j = Clamp(span.j, (i16)0, (i16)(grid.m_H-1)); i16 i0 = std::max(span.i0, (i16)0); i16 i1 = std::min(span.i1, (i16)grid.m_W); for (i16 i = i0; i < i1; ++i) grid.set(i, j, grid.get(i, j) | appliedMask); } } for (auto& pair : m_UnitShapes) { if (!(pair.second.flags & requireMask)) continue; if (!fullUpdate && std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), pair.first) == m_DirtyUnitShapes.end()) continue; CFixedVector2D center(pair.second.x, pair.second.z); entity_pos_t r = pair.second.clearance + clearance; u16 i0, j0, i1, j1; Pathfinding::NearestNavcell(center.X - r, center.Y - r, i0, j0, grid.m_W, grid.m_H); Pathfinding::NearestNavcell(center.X + r, center.Y + r, i1, j1, grid.m_W, grid.m_H); for (u16 j = j0+1; j < j1; ++j) for (u16 i = i0+1; i < i1; ++i) grid.set(i, j, grid.get(i, j) | appliedMask); } } void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector& squares) const { GetUnitObstructionsInRange(filter, x0, z0, x1, z1, squares); GetStaticObstructionsInRange(filter, x0, z0, x1, z1, squares); } void CCmpObstructionManager::GetUnitObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector& squares) const { PROFILE("GetObstructionsInRange"); ENSURE(x0 <= x1 && z0 <= z1); std::vector unitShapes; m_UnitSubdivision.GetInRange(unitShapes, CFixedVector2D(x0, z0), CFixedVector2D(x1, z1)); for (entity_id_t& unitShape : unitShapes) { std::map::const_iterator it = m_UnitShapes.find(unitShape); ENSURE(it != m_UnitShapes.end()); if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY)) continue; entity_pos_t c = it->second.clearance; // Skip this object if it's completely outside the requested range if (it->second.x + c < x0 || it->second.x - c > x1 || it->second.z + c < z0 || it->second.z - c > z1) continue; CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero()); CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1)); squares.emplace_back(ObstructionSquare{ it->second.x, it->second.z, u, v, c, c }); } } void CCmpObstructionManager::GetStaticObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector& squares) const { PROFILE("GetObstructionsInRange"); ENSURE(x0 <= x1 && z0 <= z1); std::vector staticShapes; m_StaticSubdivision.GetInRange(staticShapes, CFixedVector2D(x0, z0), CFixedVector2D(x1, z1)); for (entity_id_t& staticShape : staticShapes) { std::map::const_iterator it = m_StaticShapes.find(staticShape); ENSURE(it != m_StaticShapes.end()); if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2)) continue; entity_pos_t r = it->second.hw + it->second.hh; // overestimate the max dist of an edge from the center // Skip this object if its overestimated bounding box is completely outside the requested range if (it->second.x + r < x0 || it->second.x - r > x1 || it->second.z + r < z0 || it->second.z - r > z1) continue; // TODO: maybe we should use Geometry::GetHalfBoundingBox to be more precise? squares.emplace_back(ObstructionSquare{ it->second.x, it->second.z, it->second.u, it->second.v, it->second.hw, it->second.hh }); } } void CCmpObstructionManager::GetUnitsOnObstruction(const ObstructionSquare& square, std::vector& out, const IObstructionTestFilter& filter, bool strict) const { PROFILE("GetUnitsOnObstruction"); // In order to avoid getting units on impassable cells, we want to find all // units subject to the RasterizeRectWithClearance of the building's shape with the // unit's clearance covers the navcell the unit is on. std::vector unitShapes; CFixedVector2D center(square.x, square.z); CFixedVector2D expandedBox = Geometry::GetHalfBoundingBox(square.u, square.v, CFixedVector2D(square.hw, square.hh)) + CFixedVector2D(m_MaxClearance, m_MaxClearance); m_UnitSubdivision.GetInRange(unitShapes, center - expandedBox, center + expandedBox); std::map rasterizedRects; for (const u32& unitShape : unitShapes) { std::map::const_iterator it = m_UnitShapes.find(unitShape); ENSURE(it != m_UnitShapes.end()); const UnitShape& shape = it->second; if (!filter.TestShape(UNIT_INDEX_TO_TAG(unitShape), shape.flags, shape.group, INVALID_ENTITY)) continue; if (rasterizedRects.find(shape.clearance) == rasterizedRects.end()) { // The rasterization is an approximation of the real shapes. // Depending on your use, you may want to be more or less strict on the rasterization, // ie this may either return some units that aren't actually on the shape (if strict is set) // or this may not return some units that are on the shape (if strict is not set). // Foundations need to be non-strict, as otherwise it sometimes detects the builder units // as being on the shape, so it orders them away. SimRasterize::Spans& newSpans = rasterizedRects[shape.clearance]; if (strict) SimRasterize::RasterizeRectWithClearance(newSpans, square, shape.clearance, Pathfinding::NAVCELL_SIZE); else SimRasterize::RasterizeRectWithClearance(newSpans, square, shape.clearance-Pathfinding::CLEARANCE_EXTENSION_RADIUS, Pathfinding::NAVCELL_SIZE); } SimRasterize::Spans& spans = rasterizedRects[shape.clearance]; // Check whether the unit's center is on a navcell that's in // any of the spans u16 i = (shape.x / Pathfinding::NAVCELL_SIZE).ToInt_RoundToNegInfinity(); u16 j = (shape.z / Pathfinding::NAVCELL_SIZE).ToInt_RoundToNegInfinity(); for (const SimRasterize::Span& span : spans) { if (j == span.j && span.i0 <= i && i < span.i1) { out.push_back(shape.entity); break; } } } } void CCmpObstructionManager::GetStaticObstructionsOnObstruction(const ObstructionSquare& square, std::vector& out, const IObstructionTestFilter& filter) const { PROFILE("GetStaticObstructionsOnObstruction"); std::vector staticShapes; CFixedVector2D center(square.x, square.z); CFixedVector2D expandedBox = Geometry::GetHalfBoundingBox(square.u, square.v, CFixedVector2D(square.hw, square.hh)); m_StaticSubdivision.GetInRange(staticShapes, center - expandedBox, center + expandedBox); for (const u32& staticShape : staticShapes) { std::map::const_iterator it = m_StaticShapes.find(staticShape); ENSURE(it != m_StaticShapes.end()); const StaticShape& shape = it->second; if (!filter.TestShape(STATIC_INDEX_TO_TAG(staticShape), shape.flags, shape.group, shape.group2)) continue; if (Geometry::TestSquareSquare( center, square.u, square.v, CFixedVector2D(square.hw, square.hh), CFixedVector2D(shape.x, shape.z), shape.u, shape.v, CFixedVector2D(shape.hw, shape.hh))) { out.push_back(shape.entity); } } } void CCmpObstructionManager::RenderSubmit(SceneCollector& collector) { if (!m_DebugOverlayEnabled) return; CColor defaultColor(0, 0, 1, 1); CColor movingColor(1, 0, 1, 1); CColor boundsColor(1, 1, 0, 1); // If the shapes have changed, then regenerate all the overlays if (m_DebugOverlayDirty) { m_DebugOverlayLines.clear(); m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = boundsColor; SimRender::ConstructSquareOnGround(GetSimContext(), (m_WorldX0+m_WorldX1).ToFloat()/2.f, (m_WorldZ0+m_WorldZ1).ToFloat()/2.f, (m_WorldX1-m_WorldX0).ToFloat(), (m_WorldZ1-m_WorldZ0).ToFloat(), 0, m_DebugOverlayLines.back(), true); for (std::map::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it) { m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = ((it->second.flags & FLAG_MOVING) ? movingColor : defaultColor); SimRender::ConstructSquareOnGround(GetSimContext(), it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.clearance.ToFloat(), it->second.clearance.ToFloat(), 0, m_DebugOverlayLines.back(), true); } for (std::map::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it) { m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = defaultColor; float a = atan2f(it->second.v.X.ToFloat(), it->second.v.Y.ToFloat()); SimRender::ConstructSquareOnGround(GetSimContext(), it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.hw.ToFloat()*2, it->second.hh.ToFloat()*2, a, m_DebugOverlayLines.back(), true); } m_DebugOverlayDirty = false; } for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i) collector.Submit(&m_DebugOverlayLines[i]); }