Index: ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.cpp (revision 22015) +++ ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.cpp (revision 22016) @@ -1,1321 +1,1326 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2019 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 "ICmpRallyPointRenderer.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpFootprint.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/helpers/Render.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/system/Component.h" #include "graphics/Overlay.h" #include "graphics/TextureManager.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "renderer/Renderer.h" struct SVisibilitySegment { bool m_Visible; size_t m_StartIndex; size_t m_EndIndex; // inclusive SVisibilitySegment(bool visible, size_t startIndex, size_t endIndex) : m_Visible(visible), m_StartIndex(startIndex), m_EndIndex(endIndex) {} bool operator==(const SVisibilitySegment& other) const { return (m_Visible == other.m_Visible && m_StartIndex == other.m_StartIndex && m_EndIndex == other.m_EndIndex); } bool operator!=(const SVisibilitySegment& other) const { return !(*this == other); } bool IsSinglePoint() const { return (m_StartIndex == m_EndIndex); } }; class CCmpRallyPointRenderer : public ICmpRallyPointRenderer { // import some types for less verbosity typedef WaypointPath Path; typedef PathGoal Goal; typedef ICmpRangeManager::CLosQuerier CLosQuerier; typedef SOverlayTexturedLine::LineCapType LineCapType; public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_PlayerColorChanged); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_TurnStart); componentManager.SubscribeToMessageType(MT_Destroy); componentManager.SubscribeToMessageType(MT_PositionChanged); } DEFAULT_COMPONENT_ALLOCATOR(RallyPointRenderer) protected: /// Display position of the rally points. Note that this are merely the display positions; they not necessarily the same as the /// actual positions used in the simulation at any given time. In particular, we need this separate copy to support /// instantaneously rendering the rally point markers/lines when the user sets one in-game (instead of waiting until the /// network-synchronization code sets it on the RallyPoint component, which might take up to half a second). std::vector m_RallyPoints; /// Full path to the rally points as returned by the pathfinder, with some post-processing applied to reduce zig/zagging. std::vector > m_Path; /// Visibility segments of the rally point paths; splits the path into SoD/non-SoD segments. std::deque > m_VisibilitySegments; bool m_Displayed; ///< Should we render the rally points and the path lines? (set from JS when e.g. the unit is selected/deselected) bool m_SmoothPath; ///< Smooth the path before rendering? std::vector m_MarkerEntityIds; ///< Entity IDs of the rally point markers. size_t m_LastMarkerCount; player_id_t m_LastOwner; ///< Last seen owner of this entity (used to keep track of ownership changes). std::wstring m_MarkerTemplate; ///< Template name of the rally point markers. /// Marker connector line settings (loaded from XML) float m_LineThickness; CColor m_LineColor; CColor m_LineDashColor; LineCapType m_LineStartCapType; LineCapType m_LineEndCapType; std::wstring m_LineTexturePath; std::wstring m_LineTextureMaskPath; std::string m_LinePassabilityClass; ///< Pathfinder passability class to use for computing the (long-range) marker line path. CTexturePtr m_Texture; CTexturePtr m_TextureMask; /// Textured overlay lines to be used for rendering the marker line. There can be multiple because we may need to render /// dashes for segments that are inside the SoD. std::vector > m_TexturedOverlayLines; /// Draw little overlay circles to indicate where the exact path points are? bool m_EnableDebugNodeOverlay; std::vector > m_DebugNodeOverlays; public: static std::string GetSchema() { return "Displays a rally point marker where created units will gather when spawned" "" "special/rallypoint" "0.75" "round" "square" "" "default" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "0255" "" "" "0255" "" "" "0255" "" "" "" "" "flat" "round" "sharp" "square" "" "" "" "" "flat" "round" "sharp" "square" "" "" "" "" ""; } virtual void Init(const CParamNode& paramNode); virtual void Deinit() { } virtual void Serialize(ISerializer& UNUSED(serialize)) { // do NOT serialize anything; this is a rendering-only component, it does not and should not affect simulation state } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) { Init(paramNode); // The dependent components have not been deserialized, so the color is loaded on first SetDisplayed } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_PlayerColorChanged: { const CMessagePlayerColorChanged& msgData = static_cast (msg); CmpPtr cmpOwnership(GetEntityHandle()); if (!cmpOwnership || msgData.player != cmpOwnership->GetOwner()) break; UpdateLineColor(); ConstructAllOverlayLines(); } break; case MT_RenderSubmit: { PROFILE("RallyPoint::RenderSubmit"); if (m_Displayed && IsSet()) { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); } } break; case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); // Ignore destroyed entities if (msgData.to == INVALID_PLAYER) break; // Required for both the initial and capturing players color UpdateLineColor(); // Support capturing, even though RallyPoint is typically deleted then UpdateMarkers(); ConstructAllOverlayLines(); } break; case MT_TurnStart: { UpdateOverlayLines(); // check for changes to the SoD and update the overlay lines accordingly } break; case MT_Destroy: { - for (std::vector::iterator it = m_MarkerEntityIds.begin(); it < m_MarkerEntityIds.end(); ++it) - { - if (*it != INVALID_ENTITY) - { - GetSimContext().GetComponentManager().DestroyComponentsSoon(*it); - *it = INVALID_ENTITY; - } - } + Reset(); } break; case MT_PositionChanged: { // Unlikely to happen in-game, but can occur in atlas // Just recompute the path from the entity to the first rally point RecomputeRallyPointPath_wrapper(0); } break; } } /* * Must be called whenever m_Displayed or the size of m_RallyPoints change, * to determine whether we need to respond to render messages. */ void UpdateMessageSubscriptions() { bool needRender = m_Displayed && IsSet(); GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender); } virtual void AddPosition_wrapper(const CFixedVector2D& pos) { AddPosition(pos, false); } virtual void SetPosition(const CFixedVector2D& pos) { if (!(m_RallyPoints.size() == 1 && m_RallyPoints.front() == pos)) { m_RallyPoints.clear(); AddPosition(pos, true); // Don't need to UpdateMessageSubscriptions here since AddPosition already calls it } } virtual void UpdatePosition(u32 rallyPointId, const CFixedVector2D& pos) { if (rallyPointId >= m_RallyPoints.size()) return; m_RallyPoints[rallyPointId] = pos; UpdateMarkers(); // Compute a new path for the current, and if existing the next rally point RecomputeRallyPointPath_wrapper(rallyPointId); if (rallyPointId+1 < m_RallyPoints.size()) RecomputeRallyPointPath_wrapper(rallyPointId+1); } virtual void SetDisplayed(bool displayed) { if (m_Displayed != displayed) { m_Displayed = displayed; // Set color after all dependent components are deserialized if (displayed && m_LineColor.r < 0) { UpdateLineColor(); ConstructAllOverlayLines(); } // move the markers out of oblivion and back into the real world, or vice-versa UpdateMarkers(); // Check for changes to the SoD and update the overlay lines accordingly. We need to do this here because this method // only takes effect when the display flag is active; we need to pick up changes to the SoD that might have occurred // while this rally point was not being displayed. UpdateOverlayLines(); UpdateMessageSubscriptions(); } } virtual void Reset() { + for (entity_id_t& componentId : m_MarkerEntityIds) + { + if (componentId != INVALID_ENTITY) + { + GetSimContext().GetComponentManager().DestroyComponentsSoon(componentId); + componentId = INVALID_ENTITY; + } + } + + m_MarkerEntityIds.clear(); + m_LastOwner = INVALID_PLAYER; + m_LastMarkerCount = 0; m_RallyPoints.clear(); RecomputeAllRallyPointPaths(); UpdateMessageSubscriptions(); } /** * Returns true if at least one display rally point is set; i.e., if we have a point to render our marker/line at. */ bool IsSet() const { return !m_RallyPoints.empty(); } void UpdateColor() { UpdateLineColor(); ConstructAllOverlayLines(); } private: /** * Helper function for AddPosition_wrapper and SetPosition. */ void AddPosition(CFixedVector2D pos, bool recompute) { m_RallyPoints.push_back(pos); UpdateMarkers(); if (recompute) RecomputeAllRallyPointPaths(); else RecomputeRallyPointPath_wrapper(m_RallyPoints.size()-1); UpdateMessageSubscriptions(); } /** * Helper function to set the line color to its owner's color. */ void UpdateLineColor(); /** * Repositions the rally point markers; moves them outside of the world (ie. hides them), or positions them at the currently * set rally points. Also updates the actor's variation according to the entity's current owning player's civilization. * * Should be called whenever either the position of a rally point changes (including whether it is set or not), or the display * flag changes, or the ownership of the entity changes. */ void UpdateMarkers(); /** * Recomputes all the full paths from this entity to the rally point and from the rally point to the next, and does all the necessary * post-processing to make them prettier. * * Should be called whenever all rally points' position changes. */ void RecomputeAllRallyPointPaths(); /** * Recomputes the full path for m_Path[ @p index], and does all the necessary post-processing to make it prettier. * * Should be called whenever either the starting position or the rally point's position changes. */ void RecomputeRallyPointPath_wrapper(size_t index); /** * Recomputes the full path from this entity/the previous rally point to the next rally point, and does all the necessary * post-processing to make it prettier. This doesn't check if we have a valid position or if a rally point is set. * * You shouldn't need to call this method directly. */ void RecomputeRallyPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& cmpFootprint, CmpPtr cmpPathfinder); /** * Checks for changes to the SoD to the previously saved state, and reconstructs the visibility segments and overlay lines to * match if necessary. Does nothing if the rally point lines are not currently set to be displayed, or if no rally point is set. */ void UpdateOverlayLines(); /** * Sets up all overlay lines for rendering according to the current full path and visibility segments. Splits the line into solid * and dashed pieces (for the SoD). Should be called whenever the SoD has changed. If no full path is currently set, this method * does nothing. */ void ConstructAllOverlayLines(); /** * Sets up the overlay lines for rendering according to the full path and visibility segments at @p index. Splits the line into * solid and dashed pieces (for the SoD). Should be called whenever the SoD of the path at @p index has changed. */ void ConstructOverlayLines(size_t index); /** * Removes points from @p coords that are obstructed by the originating building's footprint, and links up the last point * nicely to the edge of the building's footprint. Only needed if the pathfinder can possibly return obstructed tile waypoints, * i.e. when pathfinding is started from an obstructed tile. */ void FixFootprintWaypoints(std::vector& coords, CmpPtr cmpPosition, CmpPtr cmpFootprint) const; /** * Get the point on the footprint edge that's as close from "start" as possible. */ void GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr cmpPosition, CmpPtr cmpFootprint) const; /** * Returns a list of indices of waypoints in the current path (m_Path[index]) where the LOS visibility changes, ordered from * building/previous rally point to rally point. Used to construct the overlay line segments and track changes to the SoD. */ void GetVisibilitySegments(std::deque& out, size_t index) const; /** * Simplifies the path by removing waypoints that lie between two points that are visible from one another. This is primarily * intended to reduce some unnecessary curviness of the path; the pathfinder returns a mathematically (near-)optimal path, which * will happily curve and bend to reduce costs. Visually, it doesn't make sense for a rally point path to curve and bend when it * could just as well have gone in a straight line; that's why we have this, to make it look more natural. * * @p coords array of path coordinates to simplify * @p maxSegmentLinks if non-zero, indicates the maximum amount of consecutive node-to-node links that can be joined into a * single link. If this value is set to e.g. 1, then no reductions will be performed. A value of 3 means that * at most 3 consecutive node links will be joined into a single link. * @p floating whether to consider nodes who are under the water level as floating on top of the water */ void ReduceSegmentsByVisibility(std::vector& coords, unsigned maxSegmentLinks = 0, bool floating = true) const; /** * Helper function to GetVisibilitySegments, factored out for testing. Merges single-point segments with its neighbouring * segments. You should not have to call this method directly. */ static void MergeVisibilitySegments(std::deque& segments); void RenderSubmit(SceneCollector& collector); }; REGISTER_COMPONENT_TYPE(RallyPointRenderer) void CCmpRallyPointRenderer::Init(const CParamNode& paramNode) { m_Displayed = false; m_SmoothPath = true; m_LastOwner = INVALID_PLAYER; m_LastMarkerCount = 0; m_EnableDebugNodeOverlay = false; UpdateLineColor(); // --------------------------------------------------------------------------------------------- // load some XML configuration data (schema guarantees that all these nodes are valid) m_MarkerTemplate = paramNode.GetChild("MarkerTemplate").ToString(); const CParamNode& lineDashColor = paramNode.GetChild("LineDashColor"); m_LineDashColor = CColor( lineDashColor.GetChild("@r").ToInt()/255.f, lineDashColor.GetChild("@g").ToInt()/255.f, lineDashColor.GetChild("@b").ToInt()/255.f, 1.f ); m_LineThickness = paramNode.GetChild("LineThickness").ToFixed().ToFloat(); m_LineTexturePath = paramNode.GetChild("LineTexture").ToString(); m_LineTextureMaskPath = paramNode.GetChild("LineTextureMask").ToString(); m_LineStartCapType = SOverlayTexturedLine::StrToLineCapType(paramNode.GetChild("LineStartCap").ToString()); m_LineEndCapType = SOverlayTexturedLine::StrToLineCapType(paramNode.GetChild("LineEndCap").ToString()); m_LinePassabilityClass = paramNode.GetChild("LinePassabilityClass").ToUTF8(); // --------------------------------------------------------------------------------------------- // load some textures if (CRenderer::IsInitialised()) { CTextureProperties texturePropsBase(m_LineTexturePath); texturePropsBase.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsBase.SetMaxAnisotropy(4.f); m_Texture = g_Renderer.GetTextureManager().CreateTexture(texturePropsBase); CTextureProperties texturePropsMask(m_LineTextureMaskPath); texturePropsMask.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsMask.SetMaxAnisotropy(4.f); m_TextureMask = g_Renderer.GetTextureManager().CreateTexture(texturePropsMask); } } void CCmpRallyPointRenderer::UpdateMarkers() { player_id_t previousOwner = m_LastOwner; for (size_t i = 0; i < m_RallyPoints.size(); ++i) { if (i >= m_MarkerEntityIds.size()) m_MarkerEntityIds.push_back(INVALID_ENTITY); if (m_MarkerEntityIds[i] == INVALID_ENTITY) { // no marker exists yet, create one first CComponentManager& componentMgr = GetSimContext().GetComponentManager(); // allocate a new entity for the marker if (!m_MarkerTemplate.empty()) { m_MarkerEntityIds[i] = componentMgr.AllocateNewLocalEntity(); if (m_MarkerEntityIds[i] != INVALID_ENTITY) m_MarkerEntityIds[i] = componentMgr.AddEntity(m_MarkerTemplate, m_MarkerEntityIds[i]); } } // the marker entity should be valid at this point, otherwise something went wrong trying to allocate it if (m_MarkerEntityIds[i] == INVALID_ENTITY) LOGERROR("Failed to create rally point marker entity"); CmpPtr markerCmpPosition(GetSimContext(), m_MarkerEntityIds[i]); if (markerCmpPosition) { if (m_Displayed && IsSet()) { markerCmpPosition->MoveTo(m_RallyPoints[i].X, m_RallyPoints[i].Y); } else { markerCmpPosition->MoveOutOfWorld(); // hide it } } // set rally point flag selection based on player civilization CmpPtr cmpOwnership(GetEntityHandle()); if (!cmpOwnership) continue; player_id_t ownerId = cmpOwnership->GetOwner(); if (ownerId == INVALID_PLAYER || (ownerId == previousOwner && m_LastMarkerCount >= i)) continue; m_LastOwner = ownerId; CmpPtr cmpPlayerManager(GetSystemEntity()); // cmpPlayerManager should not be null as long as this method is called on-demand instead of at Init() time // (we can't rely on component initialization order in Init()) if (!cmpPlayerManager) continue; CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(ownerId)); if (!cmpPlayer) continue; CmpPtr cmpVisualActor(GetSimContext(), m_MarkerEntityIds[i]); if (cmpVisualActor) cmpVisualActor->SetVariant("civ", CStrW(cmpPlayer->GetCiv()).ToUTF8()); } m_LastMarkerCount = m_RallyPoints.size() - 1; } void CCmpRallyPointRenderer::UpdateLineColor() { CmpPtr cmpOwnership(GetEntityHandle()); if (!cmpOwnership) return; player_id_t owner = cmpOwnership->GetOwner(); if (owner == INVALID_PLAYER) return; CmpPtr cmpPlayerManager(GetSystemEntity()); if (!cmpPlayerManager) return; CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(owner)); if (!cmpPlayer) return; m_LineColor = cmpPlayer->GetDisplayedColor(); } void CCmpRallyPointRenderer::RecomputeAllRallyPointPaths() { m_Path.clear(); m_VisibilitySegments.clear(); m_TexturedOverlayLines.clear(); //// /////////////////////////////////////////////// if (m_EnableDebugNodeOverlay) m_DebugNodeOverlays.clear(); //// ////////////////////////////////////////////// if (!IsSet()) return; // no use computing a path if the rally point isn't set CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return; // no point going on if this entity doesn't have a position or is outside of the world CmpPtr cmpFootprint(GetEntityHandle()); CmpPtr cmpPathfinder(GetSystemEntity()); for (size_t i = 0; i < m_RallyPoints.size(); ++i) { RecomputeRallyPointPath(i, cmpPosition, cmpFootprint, cmpPathfinder); } } void CCmpRallyPointRenderer::RecomputeRallyPointPath_wrapper(size_t index) { if (!IsSet()) return; // no use computing a path if the rally point isn't set CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return; // no point going on if this entity doesn't have a position or is outside of the world CmpPtr cmpFootprint(GetEntityHandle()); CmpPtr cmpPathfinder(GetSystemEntity()); RecomputeRallyPointPath(index, cmpPosition, cmpFootprint, cmpPathfinder); } void CCmpRallyPointRenderer::RecomputeRallyPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& cmpFootprint, CmpPtr cmpPathfinder) { while (index >= m_Path.size()) { std::vector tmp; m_Path.push_back(tmp); } m_Path[index].clear(); while (index >= m_VisibilitySegments.size()) { std::deque tmp; m_VisibilitySegments.push_back(tmp); } m_VisibilitySegments[index].clear(); // Find a long path to the goal point -- this uses the tile-based pathfinder, which will return a // list of waypoints (i.e. a Path) from the goal to the foundation/previous rally point, where each // waypoint is centered at a tile. We'll have to do some post-processing on the path to get it smooth. Path path; std::vector& waypoints = path.m_Waypoints; CFixedVector2D start(cmpPosition->GetPosition2D()); Goal goal = { Goal::POINT, m_RallyPoints[index].X, m_RallyPoints[index].Y }; if (index == 0) GetClosestsEdgePointFrom(start,m_RallyPoints[index], cmpPosition, cmpFootprint); else { start.X = m_RallyPoints[index-1].X; start.Y = m_RallyPoints[index-1].Y; } cmpPathfinder->ComputePath(start.X, start.Y, goal, cmpPathfinder->GetPassabilityClass(m_LinePassabilityClass), path); // Check if we got a path back; if not we probably have two markers less than one tile apart. if (path.m_Waypoints.size() < 2) { m_Path[index].emplace_back(start.X.ToFloat(), start.Y.ToFloat()); m_Path[index].emplace_back(m_RallyPoints[index].X.ToFloat(), m_RallyPoints[index].Y.ToFloat()); return; } else if (index == 0) { // sometimes this ends up not being optimal if you asked for a long path, so improve. CFixedVector2D newend(waypoints[waypoints.size()-2].x,waypoints[waypoints.size()-2].z); GetClosestsEdgePointFrom(newend,newend, cmpPosition, cmpFootprint); waypoints.back().x = newend.X; waypoints.back().z = newend.Y; } else { // make sure we actually start at the rallypoint because the pathfinder moves us to a usable tile. waypoints.back().x = m_RallyPoints[index-1].X; waypoints.back().z = m_RallyPoints[index-1].Y; } // pathfinder makes us go to the nearest passable cell which isn't always what we want waypoints[0].x = m_RallyPoints[index].X; waypoints[0].z = m_RallyPoints[index].Y; // From here on, we choose to represent the waypoints as CVector2D floats to avoid to have to convert back and forth // between fixed-point Waypoint/CFixedVector2D and various other float-based formats used by interpolation and whatnot. // Since we'll only be further using these points for rendering purposes, using floats should be fine. for (Waypoint& waypoint : waypoints) m_Path[index].emplace_back(waypoint.x.ToFloat(), waypoint.z.ToFloat()); // add the start position // m_Path[index].emplace_back(m_RallyPoints[index].X.ToFloat(), m_RallyPoints[index].Y.ToFloat()); // Post-processing // Linearize the path; // Pass through the waypoints, averaging each waypoint with its next one except the last one. Because the path // goes from the marker to this entity/the previous flag and we want to keep the point at the marker's exact position, // loop backwards through the waypoints so that the marker waypoint is maintained. // TODO: see if we can do this at the same time as the waypoint -> coord conversion above for(size_t i = m_Path[index].size() - 2; i > 0; --i) m_Path[index][i] = (m_Path[index][i] + m_Path[index][i-1]) / 2.0f; // if there's a footprint and this path starts from this entity, remove any points returned by the pathfinder that may be on obstructed footprint tiles //if (index == 0 && cmpFootprint) // FixFootprintWaypoints(m_Path[index], cmpPosition, cmpFootprint); // Eliminate some consecutive waypoints that are visible from eachother. Reduce across a maximum distance of approx. 6 tiles // (prevents segments that are too long to properly stick to the terrain) ReduceSegmentsByVisibility(m_Path[index], 6); // Debug overlays if (m_EnableDebugNodeOverlay) { while (index >= m_DebugNodeOverlays.size()) m_DebugNodeOverlays.emplace_back(); m_DebugNodeOverlays[index].clear(); } if (m_EnableDebugNodeOverlay && m_SmoothPath) { // Create separate control point overlays so we can differentiate when using smoothing (offset them a little higher from the // terrain so we can still see them after the interpolated points are added) for (CVector2D& point : m_Path[index]) { SOverlayLine overlayLine; overlayLine.m_Color = CColor(1.0f, 0.0f, 0.0f, 1.0f); overlayLine.m_Thickness = 2; SimRender::ConstructSquareOnGround(GetSimContext(), point.X, point.Y, 0.2f, 0.2f, 1.0f, overlayLine, true); m_DebugNodeOverlays[index].push_back(overlayLine); } } if (m_SmoothPath) // The number of points to interpolate goes hand in hand with the maximum amount of node links allowed to be joined together // by the visibility reduction. The more node links that can be joined together, the more interpolated points you need to // generate to be able to deal with local terrain height changes. SimRender::InterpolatePointsRNS(m_Path[index], false, 0, 4); // no offset, keep line at its exact path // find which point is the last visible point before going into the SoD, so we have a point to compare to on the next turn GetVisibilitySegments(m_VisibilitySegments[index], index); // build overlay lines for the new path ConstructOverlayLines(index); } void CCmpRallyPointRenderer::ConstructAllOverlayLines() { m_TexturedOverlayLines.clear(); for (size_t i = 0; i < m_Path.size(); ++i) ConstructOverlayLines(i); } void CCmpRallyPointRenderer::ConstructOverlayLines(size_t index) { // We need to create a new SOverlayTexturedLine every time we want to change the coordinates after having passed it to the // renderer, because it does some fancy vertex buffering thing and caches them internally instead of recomputing them on every // pass (which is only sensible). while (index >= m_TexturedOverlayLines.size()) { std::vector tmp; m_TexturedOverlayLines.push_back(tmp); } m_TexturedOverlayLines[index].clear(); if (m_Path[index].size() < 2) return; LineCapType dashesLineCapType = SOverlayTexturedLine::LINECAP_ROUND; // line caps to use for the dashed segments (and any other segment's edges that border it) for (std::deque::const_iterator it = m_VisibilitySegments[index].begin(); it != m_VisibilitySegments[index].end(); ++it) { const SVisibilitySegment& segment = (*it); if (segment.m_Visible) { // does this segment border on the building or rally point flag on either side? bool bordersBuilding = (segment.m_EndIndex == m_Path[index].size() - 1); bool bordersFlag = (segment.m_StartIndex == 0); // construct solid textured overlay line along a subset of the full path points from startPointIdx to endPointIdx SOverlayTexturedLine overlayLine; overlayLine.m_Thickness = m_LineThickness; overlayLine.m_SimContext = &GetSimContext(); overlayLine.m_TextureBase = m_Texture; overlayLine.m_TextureMask = m_TextureMask; overlayLine.m_Color = m_LineColor; overlayLine.m_Closed = false; // we should take care to only use m_LineXCap for the actual end points at the building and the rally point; any intermediate // end points (i.e., that border a dashed segment) should have the dashed cap // the path line is actually in reverse order as well, so let's swap out the start and end caps overlayLine.m_StartCapType = (bordersFlag ? m_LineEndCapType : dashesLineCapType); overlayLine.m_EndCapType = (bordersBuilding ? m_LineStartCapType : dashesLineCapType); overlayLine.m_AlwaysVisible = true; // push overlay line coordinates ENSURE(segment.m_EndIndex > segment.m_StartIndex); for (size_t j = segment.m_StartIndex; j <= segment.m_EndIndex; ++j) // end index is inclusive here { overlayLine.m_Coords.push_back(m_Path[index][j].X); overlayLine.m_Coords.push_back(m_Path[index][j].Y); } m_TexturedOverlayLines[index].push_back(overlayLine); } else { // construct dashed line from startPointIdx to endPointIdx; add textured overlay lines for it to the render list std::vector straightLine; straightLine.push_back(m_Path[index][segment.m_StartIndex]); straightLine.push_back(m_Path[index][segment.m_EndIndex]); // We always want to the dashed line to end at either point with a full dash (i.e. not a cleared space), so that the dashed // area is visually obvious. This requires some calculations to see what size we should make the dashes and clears for them // to fit exactly. float maxDashSize = 3.f; float maxClearSize = 3.f; float dashSize = maxDashSize; float clearSize = maxClearSize; float pairDashRatio = (dashSize / (dashSize + clearSize)); // ratio of the dash's length to a (dash + clear) pair's length float distance = (m_Path[index][segment.m_StartIndex] - m_Path[index][segment.m_EndIndex]).Length(); // straight-line distance between the points // See how many pairs (dash + clear) of unmodified size can fit into the distance. Then check the remaining distance; if it's not exactly // a dash size's worth (which it probably won't be), then adjust the dash/clear sizes slightly so that it is. int numFitUnmodified = floor(distance/(dashSize + clearSize)); float remainderDistance = distance - (numFitUnmodified * (dashSize + clearSize)); // Now we want to make remainderDistance equal exactly one dash size (i.e. maxDashSize) by scaling dashSize and clearSize slightly. // We have (remainderDistance - maxDashSize) of space to distribute over numFitUnmodified instances of (dashSize + clearSize) to make // it fit, so each (dashSize + clearSize) pair needs to adjust its length by (remainderDistance - maxDashSize)/numFitUnmodified // (which will be positive or negative accordingly). This number can then be distributed further proportionally among the dash's // length and the clear's length. // we always want to have at least one dash/clear pair (i.e., "|===| |===|"); also, we need to avoid division by zero below. numFitUnmodified = std::max(1, numFitUnmodified); float pairwiseLengthDifference = (remainderDistance - maxDashSize)/numFitUnmodified; // can be either positive or negative dashSize += pairDashRatio * pairwiseLengthDifference; clearSize += (1 - pairDashRatio) * pairwiseLengthDifference; // ------------------------------------------------------------------------------------------------ SDashedLine dashedLine; SimRender::ConstructDashedLine(straightLine, dashedLine, dashSize, clearSize); // build overlay lines for dashes size_t numDashes = dashedLine.m_StartIndices.size(); for (size_t i=0; i < numDashes; i++) { SOverlayTexturedLine dashOverlay; dashOverlay.m_Thickness = m_LineThickness; dashOverlay.m_SimContext = &GetSimContext(); dashOverlay.m_TextureBase = m_Texture; dashOverlay.m_TextureMask = m_TextureMask; dashOverlay.m_Color = m_LineDashColor; dashOverlay.m_Closed = false; dashOverlay.m_StartCapType = dashesLineCapType; dashOverlay.m_EndCapType = dashesLineCapType; dashOverlay.m_AlwaysVisible = true; // TODO: maybe adjust the elevation of the dashes to be a little lower, so that it slides underneath the actual path size_t dashStartIndex = dashedLine.m_StartIndices[i]; size_t dashEndIndex = dashedLine.GetEndIndex(i); ENSURE(dashEndIndex > dashStartIndex); for (size_t n = dashStartIndex; n < dashEndIndex; n++) { dashOverlay.m_Coords.push_back(dashedLine.m_Points[n].X); dashOverlay.m_Coords.push_back(dashedLine.m_Points[n].Y); } m_TexturedOverlayLines[index].push_back(dashOverlay); } } } //// ////////////////////////////////////////////// if (m_EnableDebugNodeOverlay) { while (index >= m_DebugNodeOverlays.size()) { std::vector tmp; m_DebugNodeOverlays.push_back(tmp); } for (size_t j = 0; j < m_Path[index].size(); ++j) { SOverlayLine overlayLine; overlayLine.m_Color = CColor(1.0f, 1.0f, 1.0f, 1.0f); overlayLine.m_Thickness = 1; SimRender::ConstructCircleOnGround(GetSimContext(), m_Path[index][j].X, m_Path[index][j].Y, 0.075f, overlayLine, true); m_DebugNodeOverlays[index].push_back(overlayLine); } } //// ////////////////////////////////////////////// } void CCmpRallyPointRenderer::UpdateOverlayLines() { // We should only do this if the rally point is currently being displayed and set inside the world, otherwise it's a massive // waste of time to calculate all this stuff (this method is called every turn) if (!m_Displayed || !IsSet()) return; // see if there have been any changes to the SoD by grabbing the visibility edge points and comparing them to the previous ones std::deque > newVisibilitySegments; for (size_t i = 0; i < m_Path.size(); ++i) { std::deque tmp; newVisibilitySegments.push_back(tmp); GetVisibilitySegments(newVisibilitySegments[i], i); } // Check if the full path changed, then reconstruct all overlay lines, otherwise check if a segment changed and update that. if (m_VisibilitySegments.size() != newVisibilitySegments.size()) { m_VisibilitySegments = newVisibilitySegments; // save the new visibility segments to compare against next time ConstructAllOverlayLines(); } else { for (size_t i = 0; i < m_VisibilitySegments.size(); ++i) { if (m_VisibilitySegments[i] != newVisibilitySegments[i]) { // The visibility segments have changed, reconstruct the overlay lines to match. NOTE: The path itself doesn't // change, only the overlay lines we construct from it. m_VisibilitySegments[i] = newVisibilitySegments[i]; // save the new visibility segments to compare against next time ConstructOverlayLines(i); } } } } void CCmpRallyPointRenderer::GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr cmpPosition, CmpPtr cmpFootprint) const { ENSURE(cmpPosition); ENSURE(cmpFootprint); // grab the shape and dimensions of the footprint entity_pos_t footprintSize0, footprintSize1, footprintHeight; ICmpFootprint::EShape footprintShape; cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight); // grab the center of the footprint CFixedVector2D center = cmpPosition->GetPosition2D(); switch (footprintShape) { case ICmpFootprint::SQUARE: { // in this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively. // the building's footprint could be rotated any which way, so let's get the rotation around the Y axis // and the rotated unit vectors in the X/Z plane of the shape's footprint // (the Footprint itself holds only the outline, the Position holds the orientation) fixed s, c; // sine and cosine of the Y axis rotation angle (aka the yaw) fixed a = cmpPosition->GetRotation().Y; sincos_approx(a, s, c); CFixedVector2D u(c, -s); // unit vector along the rotated X axis CFixedVector2D v(s, c); // unit vector along the rotated Z axis CFixedVector2D halfSize(footprintSize0/2, footprintSize1/2); CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(start - center, u, v, halfSize); result = center + footprintEdgePoint; break; } case ICmpFootprint::CIRCLE: { // in this case, both footprintSize0 and 1 indicate the circle's radius // Transform target to the point nearest on the edge. CFixedVector2D centerVec2D(center.X, center.Y); CFixedVector2D centerToLast(start - centerVec2D); centerToLast.Normalize(); result = centerVec2D + (centerToLast.Multiply(footprintSize0)); break; } } } void CCmpRallyPointRenderer::FixFootprintWaypoints(std::vector& coords, CmpPtr cmpPosition, CmpPtr cmpFootprint) const { ENSURE(cmpPosition); ENSURE(cmpFootprint); // ----------------------------------------------------------------------------------------------------- // TODO: nasty fixed/float conversions everywhere // grab the shape and dimensions of the footprint entity_pos_t footprintSize0, footprintSize1, footprintHeight; ICmpFootprint::EShape footprintShape; cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight); // grab the center of the footprint CFixedVector2D center = cmpPosition->GetPosition2D(); // ----------------------------------------------------------------------------------------------------- switch (footprintShape) { case ICmpFootprint::SQUARE: { // in this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively. // the building's footprint could be rotated any which way, so let's get the rotation around the Y axis // and the rotated unit vectors in the X/Z plane of the shape's footprint // (the Footprint itself holds only the outline, the Position holds the orientation) fixed s, c; // sine and cosine of the Y axis rotation angle (aka the yaw) fixed a = cmpPosition->GetRotation().Y; sincos_approx(a, s, c); CFixedVector2D u(c, -s); // unit vector along the rotated X axis CFixedVector2D v(s, c); // unit vector along the rotated Z axis CFixedVector2D halfSize(footprintSize0/2, footprintSize1/2); // starting from the start position, check if any points are within the footprint of the building // (this is possible if the pathfinder was started from a point located within the footprint) for(int i = (int)(coords.size() - 1); i >= 0; i--) { const CVector2D& wp = coords[i]; if (Geometry::PointIsInSquare(CFixedVector2D(fixed::FromFloat(wp.X), fixed::FromFloat(wp.Y)) - center, u, v, halfSize)) { coords.erase(coords.begin() + i); } else { break; // point no longer inside footprint, from this point on neither will any of the following be } } // add a point right on the edge of the footprint (nearest to the last waypoint) so that it links up nicely with the rest of the path CFixedVector2D lastWaypoint(fixed::FromFloat(coords.back().X), fixed::FromFloat(coords.back().Y)); CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(lastWaypoint - center, u, v, halfSize); // relative to the shape origin (center) CVector2D footprintEdge((center.X + footprintEdgePoint.X).ToFloat(), (center.Y + footprintEdgePoint.Y).ToFloat()); coords.push_back(footprintEdge); } break; case ICmpFootprint::CIRCLE: { // in this case, both footprintSize0 and 1 indicate the circle's radius for(int i = (int)(coords.size() - 1); i >= 0; i--) { const CVector2D& wp = coords[i]; fixed pointDistance = (CFixedVector2D(fixed::FromFloat(wp.X), fixed::FromFloat(wp.Y)) - center).Length(); if (pointDistance <= footprintSize0) { coords.erase(coords.begin() + i); } else { break; // point no longer inside footprint, from this point on neither will any of the following be } } // add a point right on the edge of the footprint so that it links up nicely with the rest of the path CVector2D centerVec2D(center.X.ToFloat(), center.Y.ToFloat()); CVector2D centerToLast(coords.back() - centerVec2D); coords.push_back(centerVec2D + (centerToLast.Normalized() * footprintSize0.ToFloat())); } break; } } void CCmpRallyPointRenderer::ReduceSegmentsByVisibility(std::vector& coords, unsigned maxSegmentLinks, bool floating) const { CmpPtr cmpPathFinder(GetSystemEntity()); CmpPtr cmpTerrain(GetSystemEntity()); CmpPtr cmpWaterManager(GetSystemEntity()); ENSURE(cmpPathFinder && cmpTerrain && cmpWaterManager); if (coords.size() < 3) return; // The basic idea is this: starting from a base node, keep checking each individual point along the path to see if there's a visible // line between it and the base point. If so, keep going, otherwise, make the last visible point the new base node and start the same // process from there on until the entire line is checked. The output is the array of base nodes. std::vector newCoords; StationaryOnlyObstructionFilter obstructionFilter; entity_pos_t lineRadius = fixed::FromFloat(m_LineThickness); pass_class_t passabilityClass = cmpPathFinder->GetPassabilityClass(m_LinePassabilityClass); newCoords.push_back(coords[0]); // save the first base node size_t baseNodeIdx = 0; size_t curNodeIdx = 1; float baseNodeY; entity_pos_t baseNodeX; entity_pos_t baseNodeZ; // set initial base node coords baseNodeX = fixed::FromFloat(coords[baseNodeIdx].X); baseNodeZ = fixed::FromFloat(coords[baseNodeIdx].Y); baseNodeY = cmpTerrain->GetExactGroundLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y); if (floating) baseNodeY = std::max(baseNodeY, cmpWaterManager->GetExactWaterLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y)); while (curNodeIdx < coords.size()) { ENSURE(curNodeIdx > baseNodeIdx); // this needs to be true at all times, otherwise we're checking visibility between a point and itself entity_pos_t curNodeX = fixed::FromFloat(coords[curNodeIdx].X); entity_pos_t curNodeZ = fixed::FromFloat(coords[curNodeIdx].Y); float curNodeY = cmpTerrain->GetExactGroundLevel(coords[curNodeIdx].X, coords[curNodeIdx].Y); if (floating) curNodeY = std::max(curNodeY, cmpWaterManager->GetExactWaterLevel(coords[curNodeIdx].X, coords[curNodeIdx].Y)); // find out whether curNode is visible from baseNode (careful; this is in 2D only; terrain height differences are ignored!) bool curNodeVisible = cmpPathFinder->CheckMovement(obstructionFilter, baseNodeX, baseNodeZ, curNodeX, curNodeZ, lineRadius, passabilityClass); // since height differences are ignored by CheckMovement, let's call two points visible from one another only if they're at // roughly the same terrain elevation curNodeVisible = curNodeVisible && (fabsf(curNodeY - baseNodeY) < 3.f); // TODO: this could probably use some tuning if (maxSegmentLinks > 0) // max. amount of node-to-node links to be eliminated (unsigned subtraction is valid because curNodeIdx is always > baseNodeIdx) curNodeVisible = curNodeVisible && ((curNodeIdx - baseNodeIdx) <= maxSegmentLinks); if (!curNodeVisible) { // current node is not visible from the base node, so the previous one was the last visible point from baseNode and should // hence become the new base node for further iterations. // if curNodeIdx is adjacent to the current baseNode (which is possible due to steep height differences, e.g. hills), then // we should take care not to stay stuck at the current base node if (curNodeIdx > baseNodeIdx + 1) { baseNodeIdx = curNodeIdx - 1; } else { // curNodeIdx == baseNodeIdx + 1 baseNodeIdx = curNodeIdx; curNodeIdx++; // move the next candidate node one forward so that we don't test a point against itself in the next iteration } newCoords.push_back(coords[baseNodeIdx]); // add new base node to output list // update base node coordinates baseNodeX = fixed::FromFloat(coords[baseNodeIdx].X); baseNodeZ = fixed::FromFloat(coords[baseNodeIdx].Y); baseNodeY = cmpTerrain->GetExactGroundLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y); if (floating) baseNodeY = std::max(baseNodeY, cmpWaterManager->GetExactWaterLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y)); } curNodeIdx++; } // we always need to add the last point back to the array; if e.g. all the points up to the last one are all visible from the current // base node, then the loop above just ends and no endpoint is ever added to the list. ENSURE(curNodeIdx == coords.size()); newCoords.push_back(coords[coords.size() - 1]); coords.swap(newCoords); } void CCmpRallyPointRenderer::GetVisibilitySegments(std::deque& out, size_t index) const { out.clear(); if (m_Path[index].size() < 2) return; CmpPtr cmpRangeMgr(GetSystemEntity()); player_id_t currentPlayer = GetSimContext().GetCurrentDisplayedPlayer(); CLosQuerier losQuerier(cmpRangeMgr->GetLosQuerier(currentPlayer)); // go through the path node list, comparing each node's visibility with the previous one. If it changes, end the current segment and start // a new one at the next point. bool lastVisible = losQuerier.IsExplored( (fixed::FromFloat(m_Path[index][0].X) / (int) TERRAIN_TILE_SIZE).ToInt_RoundToNearest(), (fixed::FromFloat(m_Path[index][0].Y) / (int) TERRAIN_TILE_SIZE).ToInt_RoundToNearest() ); size_t curSegmentStartIndex = 0; // starting node index of the current segment for (size_t k = 1; k < m_Path[index].size(); ++k) { // grab tile indices for this coord int i = (fixed::FromFloat(m_Path[index][k].X) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (fixed::FromFloat(m_Path[index][k].Y) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); bool nodeVisible = losQuerier.IsExplored(i, j); if (nodeVisible != lastVisible) { // visibility changed; write out the segment that was just completed and get ready for the new one out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, k - 1)); //curSegmentStartIndex = k; // new segment starts here curSegmentStartIndex = k - 1; lastVisible = nodeVisible; } } // terminate the last segment out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, m_Path[index].size() - 1)); MergeVisibilitySegments(out); } void CCmpRallyPointRenderer::MergeVisibilitySegments(std::deque& segments) { // Scan for single-point segments; if they are inbetween two other segments, delete them and merge the surrounding segments. // If they're at either end of the path, include them in their bordering segment (but only if those bordering segments aren't // themselves single-point segments, because then we would want those to get absorbed by its surrounding ones first). // first scan for absorptions of single-point surrounded segments (i.e. excluding edge segments) size_t numSegments = segments.size(); // WARNING: FOR LOOP TRICKERY AHEAD! for (size_t i = 1; i < numSegments - 1;) { SVisibilitySegment& segment = segments[i]; if (segment.IsSinglePoint()) { // since the segments' visibility alternates, the surrounding ones should have the same visibility ENSURE(segments[i-1].m_Visible == segments[i+1].m_Visible); segments[i-1].m_EndIndex = segments[i+1].m_EndIndex; // make previous segment span all the way across to the next segments.erase(segments.begin() + i); // erase this segment ... segments.erase(segments.begin() + i); // and the next (we removed [i], so [i+1] is now at position [i]) numSegments -= 2; // we removed 2 segments, so update the loop condition // in the next iteration, i should still point to the segment right after the one that got expanded, which is now // at position i; so don't increment i here } else { ++i; } } ENSURE(numSegments == segments.size()); // check to see if the first segment needs to be merged with its neighbour if (segments.size() >= 2 && segments[0].IsSinglePoint()) { int firstSegmentStartIndex = segments.front().m_StartIndex; ENSURE(firstSegmentStartIndex == 0); ENSURE(!segments[1].IsSinglePoint()); // at this point, the second segment should never be a single-point segment segments.erase(segments.begin()); segments.front().m_StartIndex = firstSegmentStartIndex; } // check to see if the last segment needs to be merged with its neighbour if (segments.size() >= 2 && segments[segments.size()-1].IsSinglePoint()) { int lastSegmentEndIndex = segments.back().m_EndIndex; ENSURE(!segments[segments.size()-2].IsSinglePoint()); // at this point, the second-to-last segment should never be a single-point segment segments.erase(segments.end()); segments.back().m_EndIndex = lastSegmentEndIndex; } // -------------------------------------------------------------------------------------------------------- // at this point, every segment should have at least 2 points for (size_t i = 0; i < segments.size(); ++i) { ENSURE(!segments[i].IsSinglePoint()); ENSURE(segments[i].m_EndIndex > segments[i].m_StartIndex); } } void CCmpRallyPointRenderer::RenderSubmit(SceneCollector& collector) { // we only get here if the rally point is set and should be displayed for (size_t i = 0; i < m_TexturedOverlayLines.size(); ++i) { for (size_t j = 0; j < m_TexturedOverlayLines[i].size(); ++j) { if (!m_TexturedOverlayLines[i][j].m_Coords.empty()) collector.Submit(&m_TexturedOverlayLines[i][j]); } } if (m_EnableDebugNodeOverlay && !m_DebugNodeOverlays.empty()) { for (size_t i = 0; i < m_DebugNodeOverlays.size(); ++i) for (size_t j = 0; j < m_DebugNodeOverlays[i].size(); ++j) collector.Submit(&m_DebugNodeOverlays[i][j]); } }